RubyでYOLOv7を動かしたった


![fuga.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/44d704e6-a9ff-ff53-ebe2-8d1d2aef8a3a.png)

最近の若者はRubyとかやらん。

なので、数学や機械学習のことが全くわからないおっさんがRubyでYOLOv7やってみた記事を書く。
ONNX Runtimeがあるので、推論だけならだいたい何でも動く。

https://github.com/ankane/onnxruntime-ruby

（3年ぐらい前に onnxruntimeでYOLOv3をやった記事を書いたので流用）

https://qiita.com/kojix2/items/c113bb44c33083c3bb8c

こちらを経由して（しなくてもいいが）

https://github.com/ibaiGorordo/ONNX-YOLOv7-Object-Detection

こちらのプロジェクトからONNXのモデルをダウンロード。これの管理人が@PINTO さん。

https://github.com/PINTO0309/PINTO_model_zoo/tree/main/307_YOLOv7

GithubにQiitaへのリンクが貼ってあるQiitaユーザーの鏡。ありがとうございます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/17260acb-da0d-129c-fdc3-e9cc38909c22.png)


ここで、post-process_merged をつかってポストプロセス実装済みモデルをダウンロードするのがポイント。
じゃないと post-process を自分で実装する必要が出てきて面倒くさい。

```sh
git clone https://github.com/PINTO0309/PINTO_model_zoo
cd PINTO_model_zoo/307_YOLOv7
chmod +x download_single_batch_post-process_merged.sh
./download_single_batch_post-process_merged.sh
```

すると、モデルがダウンロードされる。今回はメインっぽい `yolov7_post_640x640.onnx` を使う。
Ruby版のonnxruntimeをインストール。

```
gem install onnxruntime
```

するだけ。[ただしGPU使いたい人は自分で設定する必要がある。](https://github.com/ankane/onnxruntime-ruby#gpu-support)

ONNXのモデルをチェックするために、Netronを準備しておく。

https://github.com/lutzroeder/netron

Netronでモデルを開いて、inputとoutputがどんな感じかぐらい確認しておくと良いでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/dabd2346-6a07-ebc5-eff6-10e4afa4edbb.png)

わからん。おっさんにわかるのは、INPUTとOUTPUTの行列のシェイプだけ。

おっさんはめんどうくさがりなので、前回のスクリプトの使い回しで [mini_magick](https://github.com/minimagick/minimagick)使うけど、最近はWatosonさんがメンテナンスしている[rmagick](https://github.com/rmagick/rmagick)もいいらしいです。

実際に動かしてみると、機械学習の推論よりも、mini_magickの方が遅い。他のツールを使った方がいいかもしれない。まぁその気になればWebカメラで、移した画面をRuby/TkやRuby/GTKでリアルタイムに表示する、みたいなこともやれる。やれるけど面倒くさいのでやらない。おっさんになると何もかもが面倒くさいのである。そのうちRubyに対するこだわりすら面倒くさくなってPythonを使い始めるかもしれない。（そう思ってから10年近く経過しているので、たぶんPythonをメインにする日はこない）

```ruby:yolo.rb
require 'mini_magick'
require 'numo/narray'
require 'onnxruntime'

SFloat = Numo::SFloat

input_path = ARGV[0]
output_path = ARGV[1]

model = OnnxRuntime::Model.new('yolov7_post_640x640.onnx')

labels = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train',
          'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter',
          'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant',
          'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
          'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite',
          'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
          'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana',
          'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
          'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table',
          'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
          'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock',
          'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']

# preprocessing
img = MiniMagick::Image.open(input_path)
img.combine_options do |b|
  b.resize '640x640!'
  b.gravity 'center'
  b.background 'transparent'
# b.extent '640x640' # 面倒なので縦横比も気にしない
end
img_data = SFloat.cast(img.get_pixels)
img_data /= 255.0
image_data = img_data.transpose(2, 0, 1)
                     .expand_dims(0)
                     .to_a # NArray -> Array

# inference
output = model.predict({ images: image_data })

# postprocessing
scores, indices = output.values

# visualization
img = MiniMagick::Image.open(input_path)
img.colorspace 'gray'
scores.zip(indices).each do |score, i|
  cl = i[1] # cl はクラス
  hue = cl * 100 / 80.0
  label = labels[cl]
  score = score[0]
  p "draw box"
  y1 = i[2] * img.height / 640
  x1 = i[3] * img.width / 640
  y2 = i[4] * img.height / 640
  x2 = i[5] * img.width / 640
  img.combine_options do |c|
    c.draw        "rectangle #{x1}, #{y1}, #{x2}, #{y2}"
    c.fill        "hsla(#{hue}%, 20%, 80%, 0.25)"
    c.stroke      "hsla(#{hue}%, 70%, 60%, 1.0)"
    c.strokewidth (score * 3).to_s
  end
  # draw text
  img.combine_options do |c|
    c.draw "text #{x1}, #{y1 - 5} \"#{label}\""
    c.fill 'white'
    c.pointsize 18
  end
end

img.write output_path

```

これで `ruby yolo.rb A.png B.png` などとすればいい。途中ちょろっと出てくるNArrayっていうのはRubyでNumPyに相当するやつです。
Wikipediaから拾ってきた画像を、投げる。

![hoge2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/27918de5-a3d2-3288-f2d5-d66aac89342d.png)

↓　こんな感じになる。

![hoge3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/282807e1-c8f8-3f0f-e7f9-b6ff2b1c28fe.png)

YOLOv3の頃よりもさらに精度が向上しているのが見て取れる。
mini_magickの方がyoloよりずっと遅い。そのぐらいyoloが高速。ONNXバインディングがうごく言語なら、どんなマイナーな言語でも同じことができるはず。

この記事は以上です。