ETロボコン デベロッパー部門 アドバンストクラス 北海道地区予選に出場してきました

はじめに

2019年令・和元年の夏に,ETロボコン デベロッパー部門 アドバンストクラス 北海道地区予選に出場してきました.今回は,ETロボコンの説明と担当した箇所についてご説明します .

ETロボコンって?

公式HPには,開催目的として下記を掲げていました

技術教育機会の提供。5年後、15年後に世界をリードするエンジニアの育成を目指し、若手、初級エンジニア、および中級エンジニア向けに、分析・設計モデリング開発、製品サービスの企画開発にチャレンジする機会を提供する。

私はいくつかの競技内容の中から,デベロッパー部門 アドバンストクラスに参加しました.概要は下記の通りです.

プライマリークラスの経験者および応用学習者向け。技術を応用できるスキルを磨く機会を提供します。競技、コースの難易度が高いクラスです。競技の仕様変更にも対応します。

まとめると,共通のロボットを使用し走行タイムを競う競技です! 当日の様子は,Youtubeにアップロードされております.

https://www.youtube.com/watch?v=GSO2xvYsO6k

担当箇所

最終的な走行タイムは,コースを走行したタイムとボーナスタイムによって算出されます.私はボーナスタイムに関係するブロックビンゴを担当しました.ブロックビンゴとは,1から8までのブロックビンゴサークル上にロボットを用いてカラーブロックを運搬してビンゴを完成させるというものです.また,数字カードに対応するブロックビンゴサークル上に黒のカラーブロックを運搬すると,よりボーナスタイムを獲得できます

f:id:bob2525yokoyama:20190927225233j:plain

課題

ビンゴを完成させるためには,下記の課題を解決する必要があります

  1. カラーブロックが置かれている場所を把握する
  2. カラーブロックの色を識別する
  3. ボーナスナンバーを識別する
  4. 1-3をWebカメラから送られてくる動画を用いて行う

また,これらを高速かつ正確に行う必要があります.そこで,物体検出に強いCNNのうちYOLOを採用して解決することを目指しました.

解決方法

YOLO

YOLOについて

YOLOは,Redmonらが提案した物体検出アルゴリズムです.YOLOは始めに,入力画像をS*Sのグリッドに分割します.次に各グリッドのセルに対して,バウンディングボックスの座標とそれが物体である度合いを示すスコアを予測します.これと同時に,バウンディングボックスに対する条件付きクラス確率を算出します.これらの結果を組み合わせることにより,バウンディングボックス,クラス名,クラス確率を出力します.このように物体の検出と識別を同時に行うことにより,処理の高速化を実現しています.これまでにYOLOは,初期版のv1,精度と速度を向上させたv2,少し速度を落とした代わりに精度を向上させたv3が存在します.これらのうち,v3を使用してモデルを作成しました.

データセット

機械学習はデータセットの作成が本当に大変です.一言でいうとまんべんなく正解とするデータを集める必要があります.色々な参考書を読んだり,論文・記事を読みながらチームメンバと考えました.その結果,下記のようなデータセットが仕上がりました.

  • 当日を想定したアングルで撮影した画像と,汎化性能を向上させるための画像が半々.
  • 各クラス元画像は約300枚ずつ
  • かさ増し方法はガンマ補正,コントラストの調整,平滑化・ガウシアンフィルタ,ヒストグラムの均一化
  • 最終的な枚数は,各クラス2700枚

具体的な収集方法ですが,愚直に1枚ずつ撮影するのでは気が遠くなります.そこで,半自動的にデータセットを作成するツールを開発しました.ツールの概要は下記のとおりです.

f:id:bob2525yokoyama:20190927225358p:plain

これにより,作成のコストが大幅に低下します.コツはいくつかありますが,最も心がけたのは「不適切な結果を削除する」ことです.中途半端なモデルを用いて検出された結果ですので,ノイズや不正解が多く含まれます.ここで注目すべきは,正解ではあるがスコアが低い結果です.下記は,人間の目で見れば正解だと分かります.しかし,その時点でのモデルでは,3割ほどの自信しかないのです.つまり,このような結果こそウィークポイントであり,学習が足りない正解データだと考えました.

f:id:bob2525yokoyama:20190928132521j:plain

ハイパーパラメータ

ハイパーパラメータは下記の通りです.

ブロック

項目
クラス数 8
フィルター数 39
最大イテレーション 1000
バッチサイズ 64
訓練とテストの比率 7:3

ボーナスナンバー

項目
クラス数 5
フィルター数 30
最大イテレーション 10000
バッチサイズ 64
訓練とテストの比率 7:3

YOLOの作者によると,最大イテレーション数はクラス数*2000がよいとされています.しかし,何度も繰り返した結果,ボーナスナンバーは700~850の間で収束し,カラーブロックは2500付近で収束していたため,今回はこのような値にしました.

モデルを評価した結果

ブロック

Iteration TP FP FN map iou f recall
500 1066 3989 13427 10.15 13.69
600 3482 4710 11011
700 8110 9987 6383
800 10251 10285 4242 56.3 33.07 0.59 0.71
900 12182 7128 2311 77.94 43.52 0.72 0.84
1000 12166 3471 2327 81.53 55.37 0.81 0.84

f:id:bob2525yokoyama:20190927231206p:plain

ボーナスナンバー

Iteration TP FP map iou f recall
800 1231 1209 56.22 33.27 0.6 0.73
810 878 1154 47.44 28.47 0.47 0.52
820 1286 589 76.34 46.14 0.72 0.76
830 1385 728 70.45 43.51 0.73 0.82
840 1358 834 77.77 41.63 0.7 0.8
850 1395 645 73.52 45.13 0.75 0.83

f:id:bob2525yokoyama:20190927231224p:plain

上記の結果から,ボーナスナンバーは820,カラーブロックは900を採用しました.ところがここで,悲しいお知らせがあります.実は自分のチームは去年もYOLOを使っており,今年のモデルは去年のモデルの7割程度の精度しか出せませんでした....データセットやかさ増し方法,設定ファイルも引き継がれたのですが,去年の再現すら出来ず..何が足りなかったのか...

感想

正直,去年のモデルを超えられなかったことは悔しいです.去年のメンバに聞いても,「渡した以上のことはしていない.どんな魔法があったのか...」とww ちなみに数字カードとカラーブロックのモデルを分けた理由ですが,単純にデータセットの数が大きく異なり,同一のモデルとして作成するのが困難だったためです.なら,同じにしろよーと言われそうですが,ボーナスナンバーは見え方が固定されていることから,データセットが少なくてもOKだと考えました.もっと言うと,数字カードの識別にYOLOが必要?と言われそうですが,多分不要です.頂点を検出してアフィン変換やホモグラフィ変化を行った後に,特徴点による類似度算出でもいけそうです.ただ,今回は「機械学習を使うと評価を高くつけます」という事前アナウンスがあったため,YOLOを使用しました.

結果を補完・調整する

前処理

まず,検出前に行った前処理について説明します.

前処理 実施したか
ガンマ補正 NO
輝度のヒストグラム平坦化 YES
鮮鋭化 / アンシャープマスキング NO
背景差分 NO
平均画像 YES

ガンマ補正:全体的な明るさを調整すれば,精度があがるか?

ヒストグラムの平坦化:試走会2の結果を見ると,カメラ動画全体に明るさにムラがあった.なので,ヒストグラムを平坦化してならしてみたい.ただし,equalizeHistはグレー(一次元のみ)なので,YCbCrに変換してY(輝度)を当ててやる必要がある

背景差分:ブロックが置かれていない画像を背景情報として使用すると,上手く行けばブロックや数字のみを抽出して検出精度を挙げられるかもしれない

鮮鋭化 / アンシャープマスキング:カメラ動画はボケボケ.特に遠くのブロックは境界線が曖昧に映っている.そこで,鮮鋭化 / アンシャープマスキングを行ってやるとシャープネスにできるので,もしかしたら精度向上に繋げられるかもしれない

平均画像:1枚の画像に比べて,ノイズが低減され精度があがるか?

という仮設の元試した結果,輝度のヒストグラム平坦化と平均画像を採用しました.実施しなかった処理のうちガンマ補正と鮮鋭化は,見え方が完全に異なる状態になってしまい,逆効果となりました.背景差分に関しては,2値画像に対して行わず他の手段も試すべきでした.事実,HSVに着目して背景差分を行っていたチームもいました.

課題

作成したモデルを使っても誤検出・識別することが多々あります.加えて,検出数が足りないこともあります.今回の場合,極端な話精度100%が求められます.そのため,YOLOによる結果を補完・調整する必要がありました.これらをするに辺り課題は下記の通りです.

  • 検出漏れのブロックを拾う必要がある
  • 誤識別した色を調整する

課題

作成したモデルを使っても誤検出・識別することが多々あります.加えて,検出数が足りないこともあります.今回の場合,極端な話精度100%が求められます.そのため,YOLOによる結果を補完・調整する必要がありました.これらをするに辺り課題は下記の通りです.

  • 検出漏れのブロックを拾う必要がある
  • 誤識別した色を調整する

(ボーナスナンバーについては,補完する手段が思いつかなかったので割愛します)

課題の解決方法

本番は1分間のキャリブレーションタイムが設けられます.この時間を利用して予めブロックが置かれる場所に点を打ち,点の色情報を利用しました. まず,打った点とカラーブロックを対応付ける方法をご説明します.

  1. 打った点が,検出したバウンディングボックスの内側に存在するか調べる
  2. 内側に存在する場合,対応付ける
@classmethod
    # 対応付け
    def _association_logic(self, color_object_model, calibration_model):
        # YOLOで検出したオブジェクトを囲む短形を作成
        rect = np.array(
            [
                [color_object_model.left, color_object_model.top],
                [color_object_model.left, color_object_model.bottom],
                [color_object_model.right, color_object_model.bottom],
                [color_object_model.right, color_object_model.top],
            ]
        )

        if (
            # 座標が四角形の内側にあるかどうか
            cv.pointPolygonTest(
                rect,
                (calibration_model.position_x, calibration_model.position_y),
                False,
            )
            >= 0
        ):
            calibration_model.model = color_object_model

続いて,誤識別を調整します.誤識別は赤と黄に多く見られたため,下記のようにしました.具体的には,予め赤色と黄色を定義しておき,3次元の2点間距離を使用しました.

class FixColor:
    white = [255, 255, 255]
    red = [5, 5, 60]
    blue = [72, 51, 25]
    yellow = [0, 70, 100]
    green = [39, 57, 24]

# 色の誤識別を修正する
def fix_color(self, object_models):
    for object_model in object_models:
        # 中心位置取得
        center = tuple(
            np.array(
                [
                    int(object_model.clip_image.shape[0] * 0.5),
                    int(object_model.clip_image.shape[1] * 0.5),
                ]
            )
        )
        try:
            # opencvはbgr
            r = int(object_model.clip_image[center][2])
            g = int(object_model.clip_image[center][1])
            b = int(object_model.clip_image[center][0])
            vec_a = np.array([b, g, r])

        except Exception as e:
            print(e)
            return object_models

        distance_red = np.linalg.norm(vec_a - self.fix_color_lists.red)
        distance_yellow = np.linalg.norm(vec_a - self.fix_color_lists.yellow)
        if object_model.class_id == 2:  # 黄
            # 3次元の2点間の距離を算出し,短い方の距離に修正する
            if distance_red < distance_yellow:
                object_model.class_id = 0
                object_model.label = "red"
            else:
                pass
        if object_model.class_id == 0:  # 赤
            # 3次元の2点間の距離を算出し,短い方の距離に修正する
            if distance_red > distance_yellow:
                object_model.class_id = 2
                object_model.label = "yellow"
            else:
                pass

    return object_models

最後に,検出漏れのブロックを拾います.これについても,同様に予め色を定義しておき,3次元の2点間距離を使用しました.

結果

結果の例をいくつかご紹介しておきます.

f:id:bob2525yokoyama:20190928010630p:plain

f:id:bob2525yokoyama:20190928010116p:plain

感想

補完・調整方法としては,悪くなかったと思います,ただし,色の定義はもっと色々考えていました.例えば,様々なケースで獲得した結果からSVMを作成するとか,連立方程式からRGBを求めるなどです.ん〜時間が足りなかった!!

当日結果

1本目:検出・識別結果
  • ボーナスナンバー識別 ×
  • ブロックサークル上の黒 ×
  • その他 OK
2本目:検出・識別結果
  • 1つだけ青を緑と誤識別
  • その他 OK

当日を踏まえてこうしてたら良かったなと思ったこと

数字識別

CNNではなくて,特徴点やテンプレートマッチングの方が良かったと思います.全く検出されないということは一番マズイです.CNNでやるにしても,照明の当たり方によって,ボーナスナンバーが欠けて見えるというケースも考慮すべきでした.

ブロック検出,識別

当日のご識別は,補完・調整方法によるものではなくYOLOそのものによる誤識別なので,データセットをより潤沢にすべきだったと感じました.また,複数の補完・調整方法を用意しておき,当日の環境に応じて適切な方法を選択することも視野にいれるべきでした.

総括

 M2にしてETロボコンへ始めて参加しました.さらには,全期間を通して研究・学会発表と並行して準備,本番が行われました.正直な感想,地獄です.ただ,研究とロボコンの担当箇所の分野が被っていたことから,やりやすく片方で得た知見をもう一方に活かすということが出来たので良かったです.加えて,モデリング作業は,今後のエンジニア人生に対して確実にプラスとなりました.特に,1つの課題に対して順序立てて解決するフローは,全てに応用できる経験だと思います.また,機械学習・画像処理についても勉強できたと思います.

 個人的な反省点は,アルゴリズムや様々な変数の値を変えて調整できるように,ドキュメントを詳細にまとめておくべきでした.また,クラス間もより疎結合にして,単体テストをしやすいようにすべきでした.ただこれらは反省点であるものの,日頃興味関心を持って勉強している箇所であったことから,自分なりによくできた箇所でもあったと思います.

(後半投げやりな文章になってしまったので,後日修正するかもしれません)

文献