レーザポインタを物体検出するためのYOLO学習データの作成

#はじめに
レーザポインタがカメラ内に入ると、そこを向くカメラを作っています。
ここではレーザの位置を検出する方法について記載します。

#やったこと
レーザポインタの画像を生成し、darknet用の学習データを作成、yolov3用のweightsファイルを作成しました。

#背景
当初はOpenCVでHUV変換し、今回は緑のレーザなので、緑の色相で彩度と明度が高いものを検出する予定でした。
しかしながら、いざ検出を行おうとすると以下の問題がありなかなかうまくいきませんでした。

- レーザポインタは周辺に比べ明るすぎるため、カメラでは色相が判別できない（白に見える）
- 白になってしまうと、フィルタしようにも明度の高い場所が全てひっかかってしまう
- 明度をキーとすると、カメラの露出が自動調整されるたびに判定基準がかわってしまう
- レーザポインタは小さいため、ガラスなどの反射部分と判定が難しい
結果的に、テレビに画面だの、ペットボトルだの、白い本の背表紙だのカーテンだのがあると、閾値を調整してもマスクした結果でレーザーポインタと区別をつけられませんでした。
このため、レーザポインタを機械学習することで誤検出を減らすことにしました。

#学習データの作成
かといって、手で学習データをつくるのは面倒くさいのでカメラとレーザの位置関係を固定することで学習データの作成を自動化しました。
自動化には、

 - カメラとレーザポインタを固定し、カメラの必ず特定位置にレーザーポインタが入るようにしました。
 - 期待された位置範囲のみでレーザーポインタを記録、その位置情報のみをわたすことで、他の領域にレーザポインタっぽいなにかが映り込んだときに、それがレーザポインタで”ない”ことを学習させました。

![IMG_20200606_175436.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/fac9bae1-7e99-4813-cd8b-6220c176cf07.jpeg)
カメラとレーザポインタを固定する治具

こうすることで、レーザポインタは画面の固定された点に現れるようになります。
写真では上にうつってるのがレーザポインタを出してる本体、緑が実際のポインタの指し示す点です。
この例では、レーザーポインタ自身とその反射された反射像がうつっていますが、反射象はあるべき位置にはないために、レーザポインタ本体のみが学習データに反映されます。
![img_3-48.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/d52ce3b4-7a8e-ec16-eb9f-dc504abd584b.jpeg)
これを部屋中振り回して画面内に問題となるペットボトルだの本棚だの、テレビだの、反射象が入り込んでいるビデオを作ります。
ビデオの撮影方法はなんでもいいですが、カメラは同じものを使わないと、学習データでカメラも複数学習する必要がでるかもしれません。私はMAC上にカメラをつなぎかえて以下のスクリプトで作成しました。# 1フレームとって、5フレーム廃棄してます。学習データを散らばらせるためです。

```python3:rec.py
# -*- coding: utf-8 -*-
import cv2

cap = cv2.VideoCapture(1)
fps = 30

cap.set( cv2.CAP_PROP_FRAME_WIDTH,1280)
cap.set( cv2.CAP_PROP_FRAME_HEIGHT,720)
cap.set( cv2.CAP_PROP_FPS,10)
W = cap.get(cv2.CAP_PROP_FRAME_WIDTH);
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT);

size = (int(W), int(H))
print( size )

# 出力する動画ファイルの設定
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
video = cv2.VideoWriter('/Users/satoshi/py/room3.avi', fourcc, 10.0, size)

while (cap.isOpened()):
    for i in range(1,5):
        ret, frame = cap.read()
    cv2.imshow('frame', frame)

    video.write(frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
video.release()
cv2.destroyAllWindows()
```

ビデオをとったあと、今度は別のスクリプトで学習データを作成しました



```python3:createData.py
# Following is the fixed position of the laser pointer.
# Depends on the tool to fix the position of Camera&Laserpointer.
# Depends on the camera resolution.
XS=550
XE=600
YS=310
YE=390


cap = cv2.VideoCapture("/Users/satoshi/py/output.avi")
#cap = cv2.VideoCapture("/Users/satoshi/py/room2.avi")

W = cap.get(cv2.CAP_PROP_FRAME_WIDTH);
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT);

size = (int(W), int(H))
print( size )

imNum=0

while (cap.isOpened()):
    ret, frame = cap.read()
#    cv2.rectangle(frame , (XS,YS),(XE,YE),(255,255,0),3)

#  Split each channel.
    try:
        frame_bk=frame.copy()
        r = frame[:,:,0]
        g = frame[:,:,1]
        b = frame[:,:,2]

        mask = np.zeros(r.shape, dtype=np.uint8)
    # mask to find the possible laser
        gt=220
        rt=220
        bt=220
        found=0
        while found!=1:
            found=0
            objectslist=[]
            mask[ ( g>gt)&(r>rt)&(b>bt) ] = 255
    #        mask[ ( g>200 )&(r>200) ] = 255
            contours ,hierarchy= cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
            fWrote=0
            objectslist=[]
            for i in range(0, len(contours)):
                if len(contours[i]) > 0:
                    # Finding the contours which is showing possible laser
                    area = cv2.contourArea( contours[i] )
                    # Laser pointer should be small
                    if (area > 800) or ( area<1) :
                        continue
                    rect = contours[i]
                    x, y, w, h = cv2.boundingRect(rect)

                    if ( x > XS ) & ( x < XE ) & ( y > YS ) & ( y < YE ) & ( w/h < 2 ) & ( w/h > 0.5 ) :
                        xf=x
                        yf=y
                        wf=w
                        hf=h
                        found+=1
            if found==0:
                break
            if found>=2:
                gt+=10
                rt+=10
                bt+=10
                if gt>255 :
                    break
        if found==1 :
            cv2.rectangle(frame, (xf-10, yf-10), (xf + wf+10, yf + hf+10), (0,0,0), 3)
            cv2.rectangle(mask, (xf, yf), (xf + wf, yf + hf), (255, 255, 255), 1)
            font = cv2.FONT_HERSHEY_SIMPLEX
            cv2.putText(mask,str(area),(xf,yf-10), font, 0.5,(255,255,255),1,cv2.LINE_AA)
            cv2.putText(frame,str(area),(xf,yf-10), font, 0.5,(0,0,0),2,cv2.LINE_AA)
            objectslist.append( "0 "+str( (xf+(wf/2)) /W)+" "+str( (yf+(hf/2)) /H)+" "+str((wf+20)/W)+" "+str((hf+20)/H) )
            imNum+=1

            with open( "/root/jpg1/img_1-"+str(imNum)+".txt" ,mode="w") as f:
                f.writelines( objectslist )
                f.write('\n')
            cv2.imwrite( "/root/jpg1/img_1-"+str(imNum)+".jpg" , frame_bk )

            # 画面表示
            frames=cv2.resize( frame , (640,360))
            cv2.imshow('frame', frames)

            masks=cv2.resize( mask , (640,360))

            cv2.imshow('mask', masks)


        # キー入力待機
    except:
        print("Exeption")
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 終了処理
cap.release()
#video.release()
cv2.destroyAllWindows()

```
スクリプトは
    mask[ ( g>220)&(r>220)&(b>220) ]
でマスクした画像から
面積が800以下の多角形を探し、多角形のうち、それを囲む四角形が、治具で固定した中央部分に一つだけ入ったときに学習用のJPEGデータとTEXTを作成しています。
治具で固定した中央部分は
XS=550
XE=600
YS=310
YE=390
の範囲で指定しています。# 治具が微妙にゆがんでいるために、1280x760のカメラの中央から少しずれています。
これ以外の領域は、レーザーポインタっぽいのがあっても全て学習データ上には記録しないことで、最終的な学習データが余計なものをはじけるようにします。
以下はスクリプトが作成しているマスクデータで、この中央の点のみを学習データとして記録しています。
![ezgif.com-video-to-gif(2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/3a3c17b1-917d-ef37-6aec-3e9c2a24fc51.gif)
中央に固定されて動いているのがレーザーポインタ
中央の点は、カメラとレーザーポインタの平衡位置が5cmほどはなれているために距離によって、縦方向だけ少し移動します。

ちなみに、以下は中央部分という条件をはずしたデータで、四角部分がレーザーポインタっぽく見えてるやつです。
普通にHSVとかRGBでマスクしてポインタを単純に探すと、これだけ余計なものが検出される可能性があります。
機械学習により、本来の中央のレーザポインタと、ここにある余計な資格部分を区別することを期待しています。
![ezgif.com-video-to-gif(3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/33e3021c-8431-5c5b-1f71-441527767885.gif)


このようなビデオを、カメラの露出と、ホワイトバランスが変わりそうな条件をいくつか振って記録して学習データを作成しました。

# 学習の準備
以下を参考にさせていただきました。
[Yolo v3を用いて自前のデータを学習させる + Yolo v3 & opencv のインストール方法付き(Ubuntu 16.04, Opencv 3.3, Conda)](https://qiita.com/harmegiddo/items/c3db5fd567fa4c6cc9fb)

ファイル書き出し部分のファイル名をかえてファイルがかぶらないようにしながら、ビデオ何本かでデータを作ります。
スクリプトからはdataset内に必要な画像データと、画像内のレーザーポインタの位置と領域の大きさを示すデータが書き出されます。
上記の例では /root/jpg1内に全て書き出されます。

ここからはxavierNX上で学習させていますが、
[Google Colabを使って学習](https://qiita.com/sowd0726/items/13c53505cf94f6ff67cf)も書きましたので、どちらかが利用できます。

ファイルができたら 以下で使うdatasetsディレクトリに移動します。# 以下の例では /root/work/train/cfg/task/datasets
その後以下のスクリプトを実行することで、学習データのリストを含むtrian.txtとテストデータのリストを含むtest.txtが作成されます。

```bash:createdta.sh
ls -l *.jpg > traindata.txt
sed -i -e 's/^.*img_/img_/' traindata.txt
sed -i -e 's/^/\/root\/work\/train\/cfg\/task\/datasets\//' traindata.txt
grep -v 1.jpg traindata.txt > train.txt
grep 1.jpg traindata.txt > test.txt
```

train.txtとtest.txtができあがるので、2つのファイルを上記の例では
/root/work/train/cfg/task
に移動します。
元々ビデオで作成しているので、となりあった番号は同じような画像となるので、通し番号で10毎に1枚をテスト用にしています。
ディレクトリ構造などは、基本的に
[Yolo v3を用いて自前のデータを学習させる + Yolo v3 & opencv のインストール方法付き(Ubuntu 16.04, Opencv 3.3, Conda)](https://qiita.com/harmegiddo/items/c3db5fd567fa4c6cc9fb)
を参考にしました。
~~yolov3-voc.cfgは上記と違っているので、全てのfilters= をクラスが1なので18に変更~~
すみません、うそつきました、[convolutional]って書いてあるブロック毎に各レイアのコンフィグなので、これのうちの最終のやつのfultersだけを18に変更してください。具体的には、[yolo]のブロックの上にある[convolutional]ブロックのfiltersだけを18に変更してください。計3カ所です。
同様にすべてのclassesを1に変更しました。（こっちも3カ所）
また、利用メモリと速度に効く先頭部分にあるbatch とsubdivisionをそれぞれ、私の場合だとxavierNXでやったときは、bach=4 , subdivision=2　でやりました。(6/7追記）変更しないと1,1になっているので、データが発散することがあります。
その後yolov3-voc.cfgを
/root/work/train/cfg/task
に移動
class.txt は

```text:class.txt
laser
```
の1行だけで、同様に
/root/work/train/cfg/task
におきます。

datasets.dataは

```datasets.data
classes= 1
train  = /root/work/train/cfg/task/train.txt
valid  = /root/work/train/cfg/task/test.txt
names = /root/work/train/cfg/task/class.txt
backup = /root/work/train/cfg/task/backup
```
とし、全て
/root/work/train/cfg/task
におき、
さらに、
/root/work/train/cfg/task/backupディレクトリを作成します。
その状態で　darkntは/root/work/trainにあるものとすると、
darknet detector train ./cfg/task/datasets.data  cfg/task/yolov3-voc.cfg
で学習が始まります。
xavierNXだと、2時間強で2000イテレーション走り、私のデータではlossrateが0.3をきりました。
結果は、1000イテレーション毎に作成したbackupディレクトリにつくられていくので、私はロスレートが0.3をきったところんでCTRL-Cしました。

# 実行
voc.dataをtrain ディレクトに作成

```voc.data
train ディレクトリ内に
classes= 1
train  = dummy
valid  = dummy
names = ./cfg/task/class.txt
backup = dummy
eval=voc
```
でもって、train ディレクトリから以下で動作します。

```voc.data
./darknet detector demo ./voc.data ./cfg/task/yolov3-voc.cfg ./cfg/task/backup/yolov3-voc_2000.weights
```
yolov3-voc_2000.weights は適当にイテレーション数に合わせて変更してください。
<img width="918" alt="スクリーンショット 2020-06-06 19.24.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/dba49a0f-c20c-e00c-68fa-6283c8a85f6d.png">

# 予定
1，[USB-IOのコントロール](https://qiita.com/drafts/80cd26217e724a7a0206)
2，[USB-IO経由でステッピングモータのコントロール](https://qiita.com/sowd0726/items/ee50f3a2ec8598b9a0eb)
3，レーザポインタを物体検出するための学習データの作成 ーいまここ
4，レーザポインタを追いかけるカメラ