特定の色を検出して追従するロボット メモ

[以前](https://qiita.com/shi78ge/items/d4e8189094588b86a7b2) カメラの画像処理によるライントレースカーを作ったことがあり、その応用で「前方の特定の色を検出して追従する車」が作れるのではないかと思い、実際に作ってみました。
<blockquote class="twitter-tweet"><a href="https://twitter.com/hashtag/%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4?src=hash&amp;ref_src=twsrc%5Etfw"> <a href="https://t.co/yzy1QrbtIs">pic.twitter.com/yzy1QrbtIs</a> <a href="https://twitter.com/shi78ge/status/1303697544246042624?ref_src=twsrc%5Etfw">September 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
一旦ソースコードだけですが公開しておきます。
どういう考え方で制御しているかなど、また気が向いたら追記していきます。

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

#正転・逆転制御用GPIO
R_ctrl = 17
L_ctrl = 27

#PWM制御用GPIO
R_PWM = 12
L_PWM = 13

#GPIO設定
GPIO.setwarnings(False) 
GPIO.setmode(GPIO.BCM)
GPIO.setup(R_ctrl, GPIO.OUT)
GPIO.setup(L_ctrl, GPIO.OUT)

#PWM設定
pi = pigpio.pi()
pi.set_mode(R_PWM, pigpio.OUTPUT)
pi.set_mode(L_PWM, pigpio.OUTPUT)

duty_S = 4 #直進PWM duty比
duty_T = 4 #旋回PWM duty比
freq = 700 #PWM周波数
sleep_time_S = 0.05 #モーター動作待ち 直進
sleep_time_T = 0.15 #モーター動作待ち 旋回

#↓画像処理関係設定↓
camera = cv2.VideoCapture(0)   # カメラCh.(ここでは0)を指定

#カメラ画素数（実環境に合わせて修正）
camera_H = 480 #幅
camera_W = 640 #高さ

B_width = 150 #ブロックエリア幅

T_th = 1000  #曲がり判定しきい値
N_th = 60000 #近づき判定しきい値
F_th = 50000 #離れ判定しきい値

#二値化設定
th = 50 #閾値
i_max = 255 #閾値超えたら置き換える値

#青色検出関数
def detect_blue_color(img):
    # HSV色空間に変換
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 青色のHSVの値域1
    hsv_min = np.array([90, 64, 0])
    hsv_max = np.array([150,255,255])

    # 青色領域のマスク
    mask = cv2.inRange(hsv, hsv_min, hsv_max)

    # マスキング処理
    masked_img = cv2.bitwise_and(img, img, mask=mask)

    return masked_img

#HSV画像のグレースケール化関数
def hsv2gray(img):
    
    (h,s,v) = cv2.split(img) #h,s,v に分割
    s[:] = 0 #彩度を0に
    merge = cv2.merge((h, s, v))
    rgb = cv2.cvtColor(merge, cv2.COLOR_HSV2RGB) #hsv→rgb変換
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY) #rgb→グレースケール化
    
    return gray

#モーター制御関数
def motorctrl(R, L, frequency ,duty ,sleep_time):

    #正転・逆転指示
    GPIO.output(R_ctrl, R)
    GPIO.output(L_ctrl, L)

    #指定時間だけモーター回転
    pi.hardware_PWM(R_PWM, frequency, duty*100000)
    pi.hardware_PWM(L_PWM, frequency, duty*100000)
    time.sleep(sleep_time)

    #停止
    pi.hardware_PWM(R_PWM, freq, 000000)
    pi.hardware_PWM(L_PWM, freq, 000000)


while True:
    ret, frame = camera.read()    #フレームを取得
    blue_frame = detect_blue_color(frame) #青色検出
    gray_frame = hsv2gray(blue_frame) #青色をグレースケール化
    ret, bi_frame = cv2.threshold(gray_frame, th, i_max, cv2.THRESH_BINARY) #グレースケールを二値化

    #↓ブロックエリア描画設定（描画させないならコメントアウトでOK）
    #線は青色で描画している（二値化画像では白で表示される）
    cv2.rectangle(bi_frame,(0,camera_H),(B_width,camera_H),(255,0,0),1) #左ブロックエリア
    cv2.rectangle(bi_frame,(camera_W - B_width,0),(camera_W,camera_H),(255,0,0),1) #右ブロックエリア

    #↓エリア設定（領域計算用）[縦,横]
    LB = bi_frame[0:camera_H, 0:B_width] #左ブロックエリア
    RB = bi_frame[0:camera_H, camera_W - B_width:camera_W] #右ブロックエリア
    CA = bi_frame[0:camera_H, 0:camera_W] #画面全体

    #↓各領域の面積計算
    Det_LB = cv2.countNonZero(LB) #左ブロックエリアの白ピクセルカウント
    Det_RB = cv2.countNonZero(RB) #右ブロックエリアの白ピクセルカウント
    Det_CA = cv2.countNonZero(CA) #画面全体の白ピクセルカウント
    
    print("Det_LB: " +str(Det_LB) + "  Det_CA: " +str(Det_CA) +"  Det_RB: " +str(Det_RB))
    
    #↓画像を表示（表示不要であればコメントアウト。表示しないほうが処理早い）
    cv2.imshow('original', frame) #元画像を表示
    #cv2.imshow('detect_blue', blue_frame) #青色検出画像を表示
    cv2.imshow('blue_white', bi_frame) #青色→グレースケール→二値化画像を表示

    #↓制御部分
    #ちょうどいい距離の時は止まっておく
    if (N_th -5000) > Det_CA > (F_th + 5000):
        motorctrl(1,1,freq,0,sleep_time_S)

    #離れすぎたら近づく
    elif Det_CA < F_th:
        print("離れすぎを検出しました")
        motorctrl(1,1,freq,duty_S,sleep_time_S)

    #近すぎたら離れる
    elif Det_CA > N_th:
        print("近すぎを検出しました")
        motorctrl(0, 0, freq, duty_S, sleep_time_S)

    #左にずれたら右旋回
    elif Det_LB > T_th:
        print("左ブロックエリア侵入を検出しました")
        motorctrl(1, 0, freq, duty_T, sleep_time_T)

    #右にずれたら左旋回
    elif Det_RB > T_th:
        print("右ブロックエリア侵入を検出しました")
        motorctrl(0, 1, freq, duty_T, sleep_time_T)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        print("エスケープが押されました")
        break

# 撮影用オブジェクトとウィンドウの解放
camera.release()
cv2.destroyAllWindows()

```
やってみて思ったのは、とりあえず原理的にできることはわかりましたが、あまり実用的ではないのかなと思いました。
理由
・検出対象と同系色の物体も検出してしまう
・光の当たり方で色の見え方が変わる
等、ノイズに弱いから

フィルタかけたりで工夫はできるのかもしれませんが、どこまでできるのかな、という感じです。
やっぱりもっと形とか、対象の特徴を捉えて追従させるのがいいんだと思います。
単純なロジックなので、処理的には軽いのかもしれません（他の手法をやったことないので比較できていません）