こんにちは。
コンピュータビジョン(『ロボットの眼』開発)が専門の”はやぶさ”@Cpp_Learningです。
前回『ChainerCVとYoloを使ったリアルタイム物体検出ソフトを作る』という記事を書きました↓
本記事では、より高性能な物体検出アルゴリズムのFCISを使い、ChainerCVとFCISで『カメラ・動画対応!”高性能”物体検出ソフト』の作り方を説明します。ソースコードだけ見せて!という人は目次から『FCIS_Chainer_Video.py開発』の項目に飛んでください。
また、
という人にも分かるように『深層学習による画像処理の種類』を先に説明しますので、こちらも不要な場合は飛ばして下さいな。
なお、本記事の最後に『”はやぶさ”からのお願い』という項目がありますので、もしお時間あれば、最後まで読んで頂けると嬉しいです(*・ω・)ノ♪
Contents
深層学習による画像処理の種類
はじめに深層学習による画像処理の種類について簡単に説明します。
画像分類
『画像分類』とは、画像に移っている物体が、事前に学習した物体のどれに一番近いかを分類する処理のことです↓
【一致度リスト】
- フクロウ:99%
- 鳥:95%
- 猫:77%
- 犬:42%
- 靴:90%
- 人:86%
”フクロウの画像”を事前に学習していれば、フクロウとの一致率が一番高く出ますが、フクロウ・はやぶさ・インコ・ニワトリなどを一括りで”鳥の画像”として学習させた場合は、鳥との一致度が一番高く出ます。(この場合、そもそも”フクロウ”として学習していないので、フクロウとの一致度は算出されません。)
このように学習させるデータ(訓練データ)と訓練データの分類(ラベル付け)の仕方によって結果が変わってきます。
画像分類とは、訓練データと訓練データのラベルの付け方により、一致度の結果が変わります。
物体検出
上記した『画像分類』では、物体が画像上のどの位置にいるかまでは判断できません。物体の位置まで把握したい場合は、Yoloなどの物体検出アルゴリズムを採用します。
物体の位置を検出できるため、バウンディングボックス(アンカーボックスとも呼ぶ)で物体を囲うことができます。また、物体の分類(ラベル名)と一致度も算出されます。
一致度に閾値を設定することで、バウンディングボックスの描画なし(検出なし)とすることもできます。
今回の場合、靴周辺にはバウンディングボックスが描画されませんでした。
深層学習を使わない物体検出もありますが、深層学習を使った物体検出の方が基本的には高性能です。以降でも『物体検出』という言葉が出てきますが、全て深層学習を使った『物体検出』と思って頂いて構いません。
『画像分類』同様、訓練データとラベルの付け方により、一致度の結果が変わります。
物体の位置まで検出したい場合は、物体検出アルゴリズムを採用します。
セマンティック・セグメンテーション
上記した『物体検出』には様々な手法がありますが、良くも悪くも物体の位置を大まかにしか検出できません。
目的次第では、画素(pixel)単位で『分類』を行いたい場合があります。そんなときは、『セマンティック・セグメンテーション』を使います。実際どんなものかは、言葉で説明するより見た方が早いですね↓
上が入力画像、下が処理後の結果画像です。画素(pixel)1つ1つを分類し色付けすることができます。
今回は自動運転を意識して学習された”学習済みモデル”を採用してみました。
人・自動車・道路・壁を構成する画素(pixel)がちゃんと分類(色分け)されていますね。
(この”学習済みモデル”はフクロウ等の動物の学習はしていないので、動物のラベル(色分け)は存在しません!)
『セマンティック・セグメンテーション』の意味は↑の通りですが、拡大解釈で『シーン理解』という意味で使われることもあります。
確かに、”あるシーンを切り取った絵”=”画像”と呼ぶなら、その”画像”を構成する画素(pixel)1つ1つを意味付けして分類できる⇒『シーンを理解した』といえそうです。
今回の入力画像の場合は、『このシーンに人・自動車・道路・壁が存在することを理解できた』といえます。自動運転やロボットの遠隔操作に使えそうですね!
画像を画素(pixel)単位で分類したい(シーンを理解したい)場合は、セマンティック・セグメンテーションを使う。
インスタンス・セグメンテーション
『セマンティック・セグメンテーション』は画素(pixel)単位で分類(シーン理解)できる素晴らしい技術ですが、同じラベルの物体が重なっている場合、物体同士の境界が分からなくなります。
例えば、『セマンティック・セグメンテーション』の入力画像を見ないで、結果画像だけを見たとき、自動車が何台あるか分かりますか?
見方によっては、リムジンのような長ーーい自動車が路肩に2台、遠方に大きなバンが1台あるように見えます。
目的次第では、『物体検出』のように物体1つ1つの位置を検出し、かつ物体を画素(pixel)単位で『分類』したいときがあります。そんなときに使うのが『インスタンス・セグメンテーション』です。これも言葉で説明するより見た方が早いですね↓
『物体検出』のように物体1つ1つの位置を検出し、かつ物体を構成している画素(pixel)を分類(色分け)できています。
今回の場合、一部検出できなかった自動車もありますが、リムジンのような長ーーい自動車があるという勘違いはしないはずです。
また、この図ではバウンディングボックスが描画してありますが、ないものもあります。
- インスタンス:実体と説明することが多いが、この場合はオブジェクト…つまり”物体”と考える方が(個人的には)しっくりきます
- インスタンス・セグメンテーション:物体(単位)で意味付けする/分類する
以上から『インスタンス・セグメンテーション』は『物体検出』と『セマンティック・セグメンテーション』のハイブリッドと考えると、しっくりくると思います。
物体1つ1つの位置を検出し、かつ物体を構成している画素(pixel)を分類したい場合は、インスタンス・セグメンテーションを使います。
まとめ -深層学習による画像処理の種類-
以上が『深層学習による画像処理の種類』でした。ただし、この分野は発展途上です。私が知らない技術やイノベーションにより新しい手法が開発される可能性も高いです。
『深層学習による画像処理』に興味がある方は、本記事に書いたものが全てと思わず、常にリサーチし続けて頂ければと思います(*・ω・)ノ♪
また、上記した4つ画像処理なら…
と勘違いしそうですが、ケースバイケースだと考えています。
第2部で『インスタンス・セグメンテーションによる物体検出ソフト』の作り方を説明しますが、実際に作ってみると”ある問題”が発生することが分かりました。。
”ある問題”というのは、本記事の最後『”はやぶさ”からのお願い』とも関係があるので、気になる方は続きをどうぞ!
(第1部 完)
━━━━━━━━━━━━━━━━
以降から第2部…に入る前にコーヒーブレイクを兼ねた”余談”を書きましたので、お時間ある人は読んで頂ければと思います(もちろん読み飛ばしOK!です)。
余談 -言葉の定義について- (読み飛ばしOK!)
『インスタンス・セグメンテーション』のところで、”しっくり”という言葉を使いました。
と思った人がいるかもしれないので、簡単に補足します。
言葉の正確な定義を理解することはとても重要ですが、英語を無理やり和訳すると”しっくり”こない表現になることがあります。ニュアンスは伝わるのに、日本語で上手く表現できない!という経験ありませんか?
日本語⇒英語でも似たような問題があります。今でこそ浸透していますが、日本語の『もったいない』を正確に表現する英語はなかったそうです。なので、意味も言葉もそのまま輸入した『MOTTAINAI』という言葉が誕生しました。
今回の場合、『インスタンス』という単語を「物体」と表現しました。正確な言葉の定義とは少し違いますが、個人的に違和感なく”しっくりきた”表現「物体」を採用しました。
AI(人工知能)などの抽象的な概念についても同じことが言えます。
など、言葉の定義を議論したがる人がいますが、それってそんなに重要なことかな?
重要なのは中身のアルゴリズムをきちんと理解して…
という解決策を考えられることだとディープラーニングお兄さんは思うのです(*・ω・)ノ♪
繰り返しますが、言葉の正確な定義を理解することはとても重要です。しかし、それを気にし過ぎて、前に進めなくなるのは『MOTTAINAI』と思うのです。
チームメンバーが使う言葉の定義がバラバラなのは問題なので、一度メンバーで話し合って、言葉の定義を確認したら、もう言葉の定義については必要以上に追求せず…
…と思うディープラーニングお兄さんでした!
(余談 完)
以降から 第2部 -FCISによる”高性能”物体検出ソフト作り方- を説明します。続きが気になる人は読んでみて下さいな。
FCISによる”高性能”物体検出ソフトの開発手順
本記事の冒頭で以下の記事を紹介しました↓
Yoloは興味ない…という人も↑の記事を一読して頂けると嬉しいです。環境構築・開発手順・ソースコードの構造まで共通しているため、以降からは↑で説明した部分を割愛して、ポイントを絞った説明にしたいと思います。
FCISとは
FCISとは、Fully Convolutional Instance-aware Semantic Segmentationの略称です。
Instance-aware Semantic Segmentationは第1部で説明した通りです。
Fully Convolutionalまたは Fully Convolutional Networksというのは、CNN(Convolutional Neural Network)の全てがConvolutional Layerのみで構成されている…という意味です。
横文字だらけで頭が痛くなりそうですね。。
要するに、『畳み込み層のみで構成されたニューラルネットワークでインスタンス・セグメンテーション』という意味です。(言葉の正確な定義は一度は伝えておかねば!)
FCSI:畳み込み層のみで構成されたNNでインスタンス・セグメンテーション
本稿ではFCISの詳細説明を割愛しますが、勉強したい人のために論文載せておきますね↓
FCIS_Chainer_Video.py開発
第2部の冒頭で説明した通り、環境構築から開発手順まで全てYoloの記事で説明済みなので割愛し、いきなりベースになるソフトを紹介!
ChainerでFCISを動かすサンプルソースは、ChainerCV公式Githubから取得できます。このサンプルソースとYoloの記事で作った『物体検出ソフト』を組み合わせて、以下のソースコード”FCIS_Chainer_Video.py”を作りました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
import argparse import matplotlib.pyplot as plt import cv2 import numpy as np from timeit import default_timer as timer # from chainercv.datasets import voc_bbox_label_names, voc_semantic_segmentation_label_colors from chainercv.datasets import sbd_instance_segmentation_label_names from chainercv.experimental.links import FCISResNet101 from chainercv.utils import mask_to_bbox from chainercv.visualizations.colormap import voc_colormap from chainercv.visualizations import vis_instance_segmentation def main(): parser = argparse.ArgumentParser() parser.add_argument('--gpu', type=int, default=-1) parser.add_argument('--pretrained-model', default='sbd') parser.add_argument('video') args = parser.parse_args() model = FCISResNet101( n_fg_class=20, pretrained_model=args.pretrained_model) if args.gpu >= 0: chainer.cuda.get_device_from_id(args.gpu).use() model.to_gpu() if args.video == "0": vid = cv2.VideoCapture(0) else: vid = cv2.VideoCapture(args.video) if not vid.isOpened(): raise ImportError("Couldn't open video file or webcam.") # Compute aspect ratio of video vidw = vid.get(cv2.CAP_PROP_FRAME_WIDTH) vidh = vid.get(cv2.CAP_PROP_FRAME_HEIGHT) # vidar = vidw / vidh print(vidw) print(vidh) accum_time = 0 curr_fps = 0 fps = "FPS: ??" prev_time = timer() frame_count = 1 while True: ret, frame = vid.read() if ret == False: print("Done!") return # BGR -> RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # Result image result = frame.copy() # (H, W, C) -> (C, H, W) img = np.asarray(rgb, dtype = np.float32).transpose((2, 0, 1)) # Object Detection masks, labels, scores = model.predict([img]) mask, label, score = masks[0], labels[0], scores[0] bbox = mask_to_bbox(mask) colors = voc_colormap(list(range(1, len(mask) + 1))) # For Colors n_inst = len(bbox) instance_colors = voc_colormap(list(range(1, n_inst + 1))) instance_colors = np.array(instance_colors) # For Mask _, H, W = mask.shape canvas_img = np.zeros((H, W, 4), dtype=np.uint8) alf_img = np.zeros((H, W, 1), dtype=np.uint8) if len(bbox) != 0: # for i, bb in enumerate(bbox): for i, (bb, msk) in enumerate(zip(bbox, mask)): # print(i) lb = label[i] conf = score[i].tolist() ymin = int(bb[0]) xmin = int(bb[1]) ymax = int(bb[2]) xmax = int(bb[3]) class_num = int(lb) # Draw box # cv2.rectangle(result, (xmin, ymin), (xmax, ymax), (0,255,0), 2) text = sbd_instance_segmentation_label_names[class_num] + " " + ('%.2f' % conf) print(text) # text_pos 1 test_x = round(xmax-xmin/2)-30 test_y = round(ymax-ymin/2)-30 text_top = (test_x, test_y-10) text_bot = (test_x + 80, test_y + 5) text_pos = (test_x + 5, test_y) # text_pos 2 # text_top = (xmin, ymin - 10) # text_bot = (xmin + 80, ymin + 5) # text_pos = (xmin + 5, ymin) # Draw label cv2.rectangle(result, text_top, text_bot, (255,255,255), -1) cv2.putText(result, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 0, 0), 1) # Draw msk 1 color = instance_colors[i % len(instance_colors)] rgba = np.append(color, 0.7 * 255) # alpha=0.7 if ymax > ymin and xmax > xmin: canvas_img[msk] = rgba mask_img = np.asarray(canvas_img) tmp_bgr = cv2.split(result) mask_result = cv2.merge(tmp_bgr + [alf_img]) mask_result = cv2.addWeighted(mask_result, 1, mask_img, 0.5, 0) # Draw msk 2 # rgba = np.append((0,255,0), 0.7 * 255) # alpha=0.7 # if ymax > ymin and xmax > xmin: # canvas_img[msk] = rgba # mask_img = np.asarray(canvas_img) # tmp_bgr = cv2.split(result) # mask_result = cv2.merge(tmp_bgr + [alf_img]) # mask_result = cv2.addWeighted(mask_result, 1, mask_img, 0.5, 0) # Calculate FPS curr_time = timer() exec_time = curr_time - prev_time prev_time = curr_time accum_time = accum_time + exec_time curr_fps = curr_fps + 1 if accum_time > 1: accum_time = accum_time - 1 fps = "FPS:" + str(curr_fps) curr_fps = 0 # Draw FPS in top right corner cv2.rectangle(result, (590, 0), (640, 17), (0, 0, 0), -1) cv2.putText(result, fps, (595, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1) # Draw Frame Number cv2.rectangle(result, (0, 0), (50, 17), (0, 0, 0), -1) cv2.putText(result, str(frame_count), (0, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 255, 255), 1) # Output Result cv2.imshow("BBOX Result", result) cv2.imshow("Mask img", mask_img) cv2.imshow("Fcis Result", mask_result) # For Debug print("===== BBOX Result =====") print(type(result)) print(result.shape) print(type(result.shape)) print("===== Mask img =====") print(type(mask_img)) print(mask_img.shape) print(type(mask_img.shape)) # Stop Processing if cv2.waitKey(1) & 0xFF == ord('q'): break frame_count += 1 if __name__ == '__main__': main() |
FCIS_Chainer_Video.pyのポイント解説
Yoloの記事で作った『物体検出ソフト』と↑のソースコードの共通点が”多い”ことが分かると思います。むしろ構造が同じといっても過言じゃないと思います。
なので、共通点以外のポイントのみ説明します。
今回はセグメンテーションなので、物体を構成する画素(pixel)を色分けするためのマスク画像を生成します。マスク画像に関連のあるフローは以下の通りです。
- コードの63~67行目でインスタンス・セグメンテーションの演算
- コードの74~77行目でマスク画像の定義
- コードの115~123行目でマスク画像をオーバーレイ
以下がオーバーレイのイメージです。
マスク画像を生成して入力画像にオーバーレイ
エラーレポート
ChaineCVのサンプルソースは、OpenCV未使用ですが、カメラ・動画に対応するためOpenCVを採用しました。
なので、マスク画像のオーバーレイ処理もOpenCV仕様に変更したのですが…最初にコードを書いたとき、エラーが出ました。
原因を調べてみると、マスク画像が4チャンネル(以下 ch)だったので、その他の3ch画像とはオーバーレイできないことが分かりました。
コードの160~169行目にch確認用のコードを消さずに残しておきます。(不要な場合は消して下さい。)
一般的にカラー画像はRGB成分の3chですが、透明成分を加えた4ch画像もあります。
下図マスク画像の背景は”黒”に見えますが、PC上では”透明”な背景として処理されています。
対策として、3chのフクロウ画像を4chに拡張してから、オーバーレイしました。改めて、マスク画像に関連のあるフローおよびソースコードを確認してみて下さい。
- RGB成分に加え、透明成分を持つ4ch画像があります
- ch数の異なる画像はオーバーレイできない
FCIS_Chainer_Video.pyの使い方
”FCIS_Chainer_Video.py”の使い方もYoloの記事で作った『物体検出ソフト』とほとんど同じですが、一応デフォルト設定時での起動は以下の通りです。
デフォルト設定での起動例
python FCIS_Chainer_Video.py 動画ファイルパス
動画ファイルパスのところを”0”にするとUSBカメラやノートPC内蔵カメラの映像を使って『インスタンス・セグメンテーション』を実行します。
動作確認
FCIS_Chainer_Video.pyを動作確認した映像のキャプチャ画像が以下です。
【おまけ】FCIS_Chainer_Video.pyのカスタム例
今回もカスタム例を少し紹介したいと思います。
box追加と/色の変更
今回もモデルをしてくれたフクロウの”くるる”@kururu_owlちゃんから一言…
だそうです。言われてみれば。。
という”くるる”ちゃんのお願いを断れるわけがない!!
ソースコードの115~123行目がマスクの色設定なので、この部分を少し いじって、125~132行目に単色モードを追加しました。
- Draw msk 1:デフォルト(インスタンスごとに色が変化)
- Draw msk 2:単色(緑)
好きな方を選んでコメントアウトしてください。
本記事の第1部をじーーと見つめながら”くるる”ちゃんが一言
ロックオンみたで「かっこよかったなぁ」・「かっこいいのにね~」を繰り返す”くるる”が今日も可愛い!
ソースコードの92~93行目にバウンディングボックス描画用のコードを埋め込んでおいたので、必要に応じてコメントアウトしてください。
また、前回同様バウンディングボックスの左上にラベル表示するコードも埋め込んでおきました。
- text_pos 1:デフォルト(インスタンスの中心にラベル表示)
- text_pos 2:バウンディングボックスの左上にラベル表示
好きな方を選んでコメントアウトしてください。
バウンディングボックスあり/単色モードで動かしたときのキャプチャ画像が以下です。
バブルスライムって毒あるやつじゃ…
(第2部 完)
”はやぶさ”からのお願い
最後までお付き合い頂き、ありがとうございました!”お願い”については、ここまで読んで薄々気づいている人もいると思いますが、出力結果の映像がない…
第8世代インテルCoreプロセッサ(Core i5–
そのため、録画映像が作成できず、本記事では出力結果のキャプチャ画像のみ載せました。GPUマシンさえあれば…
本記事で説明したソースコード”FCIS_Chainer_Video.py”は以下の環境で動作確認しています。
- Chainer 4.3.0
- ChainerCV 0.10.0
- OpenCV 3.4.1
↑に加えCUDA環境も整えれば、ソースコードの変更なしでGPUを使った動作確認ができるはずです!私の代わりに、このソースコードで遊んでください!!
もし、お手数でなければTwitterやブログ等で「GPUならリアルタイムで動いたよー」などと書いてくれると、本当に嬉しい!
(欲を言えば、このソースコードがちゃんと動くところを見たいし、出力結果の映像をシェアさせてほしいけど、そこまでは図々しいお願いはできない…)
今回のような”処理速度の問題”があるため『インスタンス・セグメンテーション』が最も優れているという評価は難しいかと…
今回の件に限らず『精度と処理速度のトレードオフ関係』に悩んでるエンジニアは多いのではないでしょうか?
良い落としどころを見つけたいですね!
(完)
追記:補足 -専門用語について-
本記事で特に説明もなく、画素(pixel)やオーバーレイという単語を使ってきましたが…
という人は下記の記事を読んでみて下さいな↓
以上