画像処理によるライントレースカー製作記

ラズベリーパイとWEBカメラを組み合わせて、ライントレースカーを製作しました。
ベース車両の製作記はこちらです。[ハード編](https://qiita.com/shi78ge/items/e4d9dd90dc8274431e45)、[ソフト編](https://qiita.com/shi78ge/items/882d77ee6f78d63c0b0c)

##経緯
ライントレースカーと言えば、照度センサーなどを使うのが一般的な方法と思います。
自分も最初はそれを考えていたのですが、部品調達やハードをいじるのが面倒だったので、既存のハードが使えないかと考え、昔買ったWEBカメラを使って、画像処理でやってみることにしました。

##完成形
完成したライントレースカーが動作しているところです。
<blockquote class="twitter-tweet"> <a href="https://t.co/4ILHlJPWz2">pic.twitter.com/4ILHlJPWz2</a> <a href="https://twitter.com/shi78ge/status/1257674182155878400?ref_src=twsrc%5Etfw">May 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##ソースコード
モーター制御のところ関数にできますよね…

```python
import cv2
import time
import RPi.GPIO as GPIO
import pigpio

#モーターの正転/逆転制御GPIO
GPIO.setwarnings(False) 
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)
GPIO.setup(27, GPIO.OUT)

#モーター回転速度制御GPIO(PWM)
gpio_pin0 = 12
gpio_pin1 = 13
pi = pigpio.pi()
pi.set_mode(gpio_pin0, pigpio.OUTPUT)

#モーター制御パラメータ
duty1 = 4  #直進時のDuty比
duty2 = 4  #旋回時のDuty比
freq = 700
sleep_time1 = 0.05  #直進時のモーター動作時間
sleep_time2 = 0.15  #旋回時のモーター動作時間

#カメラ設定
camera = cv2.VideoCapture(0)   
th = 50    # 二値化の閾値
i_max = 255

#カメラ画像のトリミングサイズ設定
trim_y = 180
trim_h = 30

#左ブロックエリア設定
LB_x1,LB_x2 = 200,210
LB_y1,LB_y2 = 0,trim_h

#右ブロックエリア設定
RB_x1,RB_x2 = 400,410
RB_y1,RB_y2 = 0,trim_h


while True:
    ret, frame = camera.read()    #フレームを取得
    frame = frame[trim_y:trim_y + trim_h,]  #画像をトリミング
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #グレースケール化
    ret, frame = cv2.threshold(frame, th, i_max, cv2.THRESH_BINARY_INV) #二値化（白黒反転）
    cv2.rectangle(frame,(LB_x1,LB_y1),(LB_x2,LB_y2),(0,0,255),1) #左ブロックエリア描画
    cv2.rectangle(frame,(RB_x1,RB_y1),(RB_x2,RB_y2),(0,0,255),1) #右ブロックエリア描画
    
    LB = frame[LB_y1:LB_y2,LB_x1:LB_x2] #左ブロックエリアのフレームをセット
    RB = frame[RB_y1:RB_y2,RB_x1:RB_x2] #右ブロックエリアのフレームをセット
 
    Det_LB = cv2.countNonZero(LB) #左ブロックエリアの白ピクセルカウント
    Det_RB = cv2.countNonZero(RB) #右ブロックエリアの白ピクセルカウント
    print("Det_LB: " +str(Det_LB) +" Det_RB: " +str(Det_RB))
 
    cv2.imshow('camera', frame)             # フレームを画面に表示
  
    # キー操作があればwhileループを抜ける
    if cv2.waitKey(1) & 0xFF == ord('q'):
        print("エスケープが押されました")
        break

    #左右のブロックエリアへの接触を検出したら、停止線と判定して停止する
    elif Det_LB > 0 and Det_RB > 0:
        print("停止線への接触を検出しました")        
        GPIO.output(17, GPIO.HIGH)
        GPIO.output(27, GPIO.HIGH)
        pi.hardware_PWM(gpio_pin0, freq, 000000)
        pi.hardware_PWM(gpio_pin1, freq, 000000)
        break
    
    #LB接触→右旋回
    elif Det_LB > 0:
        print("左ブロックエリアへの接触を検出しました")      
        GPIO.output(17, GPIO.HIGH)
        GPIO.output(27, GPIO.LOW)
        pi.hardware_PWM(gpio_pin0, freq, duty2*100000)
        pi.hardware_PWM(gpio_pin1, freq, duty2*100000)
        time.sleep(sleep_time2)
        pi.hardware_PWM(gpio_pin0, freq, 000000)
        pi.hardware_PWM(gpio_pin1, freq, 000000)
        
    #RB接触→左旋回
    elif Det_RB > 0:
        print("右ブロックエリアへの接触を検出しました")
        GPIO.output(17, GPIO.LOW)
        GPIO.output(27, GPIO.HIGH)
        pi.hardware_PWM(gpio_pin0, freq, duty2*100000)
        pi.hardware_PWM(gpio_pin1, freq, duty2*100000)
        time.sleep(sleep_time2)
        pi.hardware_PWM(gpio_pin0, freq, 000000)
        pi.hardware_PWM(gpio_pin1, freq, 000000)
        
    #ブロックエリアへの接触なし→前進
    else :
        GPIO.output(17, GPIO.HIGH)
        GPIO.output(27, GPIO.HIGH)
        pi.hardware_PWM(gpio_pin0, freq, duty1*100000)
        pi.hardware_PWM(gpio_pin1, freq, duty1*100000)
        time.sleep(sleep_time1)
        pi.hardware_PWM(gpio_pin0, freq, 000000)
        pi.hardware_PWM(gpio_pin1, freq, 000000)
        
# 撮影用オブジェクトとウィンドウの解放
camera.release()
cv2.destroyAllWindows()

```

##解説及び所感
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/0e971d80-c07a-2ea2-a384-695c42ad3464.png)
上の写真のように、ブロックエリアという架空のエリアを設定して、その中にラインが入ったことを検出して、車両の向きを補正しています。
ラインの検出にはcountNonZeroというメソッドを使っています。
これは、黒ではないピクセルの数をカウントするというメソッドです。
これを使うために、取得画像を二値化する時に白黒を反転させ、黒いラインが白くなるようにしています。

画像をトリミングしているのもポイントかもしれません。
画像処理のライントレースカーの事例を調べていたら、そういうやり方が出ていたので、パクらせて頂きました。

あとはひたすら、地道な調整の世界でした。

```python
sleep_time1 = 0.05  #直進時のモーター動作時間
sleep_time2 = 0.15  #旋回時のモーター動作時間
```
直進時と旋回時で、モーターの動作時間を変えたりしているのがポイントです。
直進が速いと、ラインを踏み越えてしまったりします。
他にも、ブロックエリアの位置や広さを変えるなど、調整事項は色々ありそうです。
正直、まだ完璧ではないです。

たぶんですが、ライントレースカーでは、センサーを極力車両の中央に、タイヤの軸に近いところに設置するほうが、制御が楽なんだと思います。
今回の車両ではカメラをフロント部分に設置したのですが、旋回した時に結構ブレるので、それで制御が難しくなっていると思います。

やっぱり、カメラはこのような用途より、前方を撮影してレーンキープ用に使うというのが理にかなっているんだろうな。
その場合、道の勾配で見え方が変わったりするから、カメラ二つでステレオ的な処理が必要になるのか。画像処理は奥が深いです。

##参考サイト
1.[Python/OpenCVでWebカメラの情報をリアルタイム表示](https://watlab-blog.com/2019/09/22/webcamera-realtime/)
2.[Python/OpenCVで画像の二値化をする方法](https://watlab-blog.com/2019/05/25/opencv-binary/)
3.[Raspberry Pi + Python 3 に OpenCV 3 をなるべく簡単にインストールする](https://qiita.com/masaru/items/658b24b0806144cfeb1c)
4.[ラズパイでpython3にopencvを入れたらエラーが出た【対処法】](https://qiita.com/XM03/items/48463fd910470b226f22)
5.[Python,OpenCVで二値画像から白と黒の面積比を算出](https://techtech-sorae.com/pythonopencv%E3%81%A7%E4%BA%8C%E5%80%A4%E7%94%BB%E5%83%8F%E3%81%8B%E3%82%89%E7%99%BD%E3%81%A8%E9%BB%92%E3%81%AE%E9%9D%A2%E7%A9%8D%E6%AF%94%E3%82%92%E7%AE%97%E5%87%BA/)