ラズベリーパイとWEBカメラを組み合わせて、ライントレースカーを製作しました。
ベース車両の製作記はこちらです。ハード編、ソフト編
##経緯
ライントレースカーと言えば、照度センサーなどを使うのが一般的な方法と思います。
自分も最初はそれを考えていたのですが、部品調達やハードをいじるのが面倒だったので、既存のハードが使えないかと考え、昔買ったWEBカメラを使って、画像処理でやってみることにしました。
##完成形
完成したライントレースカーが動作しているところです。
pic.twitter.com/4ILHlJPWz2 May 5, 2020
##ソースコード
モーター制御のところ関数にできますよね…
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()
##解説及び所感
上の写真のように、ブロックエリアという架空のエリアを設定して、その中にラインが入ったことを検出して、車両の向きを補正しています。
ラインの検出にはcountNonZeroというメソッドを使っています。
これは、黒ではないピクセルの数をカウントするというメソッドです。
これを使うために、取得画像を二値化する時に白黒を反転させ、黒いラインが白くなるようにしています。
画像をトリミングしているのもポイントかもしれません。
画像処理のライントレースカーの事例を調べていたら、そういうやり方が出ていたので、パクらせて頂きました。
あとはひたすら、地道な調整の世界でした。
sleep_time1 = 0.05 #直進時のモーター動作時間
sleep_time2 = 0.15 #旋回時のモーター動作時間
直進時と旋回時で、モーターの動作時間を変えたりしているのがポイントです。
直進が速いと、ラインを踏み越えてしまったりします。
他にも、ブロックエリアの位置や広さを変えるなど、調整事項は色々ありそうです。
正直、まだ完璧ではないです。
たぶんですが、ライントレースカーでは、センサーを極力車両の中央に、タイヤの軸に近いところに設置するほうが、制御が楽なんだと思います。
今回の車両ではカメラをフロント部分に設置したのですが、旋回した時に結構ブレるので、それで制御が難しくなっていると思います。
やっぱり、カメラはこのような用途より、前方を撮影してレーンキープ用に使うというのが理にかなっているんだろうな。
その場合、道の勾配で見え方が変わったりするから、カメラ二つでステレオ的な処理が必要になるのか。画像処理は奥が深いです。
##参考サイト
1.Python/OpenCVでWebカメラの情報をリアルタイム表示
2.Python/OpenCVで画像の二値化をする方法
3.Raspberry Pi + Python 3 に OpenCV 3 をなるべく簡単にインストールする
4.ラズパイでpython3にopencvを入れたらエラーが出た【対処法】
5.Python,OpenCVで二値画像から白と黒の面積比を算出