自動運転AIを画像認識でカスタムしてみた（Donkey Car Simulator）

## 1. はじめに
　[前回の記事](https://qiita.com/gashu/items/1e0f9ceae5e97b85db73)ではDonkey Carシミュレータを用いて深層強化学習による自動運転を実装しました。あのときは、用意されていたデフォルトの報酬設計をほぼほぼ使用していたため、用意された環境下で最適な走りをすることしかできませんでした。
　ここで改めて実感したのは、深層強化学習において「報酬設計がすべてを決める」と言っても過言ではないということです。どんな行動が良いのかを報酬を通じてエージェントに伝える以上、報酬がズレていれば、学習される行動も当然ズレてしまいます。
　今回はそこを踏まえて、車線認識に基づく報酬設計を導入し、狙い通りの・知的な走行を実現することを目指しました。


## 2. 車線認識による報酬設計のカスタム
　今回Donkey carの趣旨とはズレてしまうかもしれませんが、真ん中にある黄色車線をトレースするような自動運転を作ってみたいと思います。(AGVのようなものをイメージしました)
 　次のような構想で報酬設計を行います。
  * **黄色車線上を走っている＆速度が出ている場合に報酬を獲得**
  * **黄色車線上から大きく外れたら報酬は0**
  * **コースから大きく外れたらゲームオーバー**

イメージ↓
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/c245248b-88ef-4c0b-a996-05e3936c6b8f.png" width="300">

 　ここで必要となってくることが、**画像処理による車線認識**です。OpenCVやディープラーニングを使ったYOLOなどがありますが、今回はシンプルなOpenCVを使っていきます。

### OpenCVによる車線認識
取得できる前方カメラ画像はBGRのカラー画像となっています。
そして以下のような流れで車線を検出します。また「黄色車線上を走っていることだけがわかればいい」という処理だけにとどめます。

1. 色空間の変換 (HSVに変換)
2. 黄色部分だけを抽出
3. エッジ検出 (Canny法)
4. Hough変換による直線検出

実際に取得した画像で実装・検証してみたいと思います。
~~~python
import cv2
import numpy as np
import glob

# 中央値を取得する関数（偶数の場合は小さい方を返す）
def median_in_list(lst):
    sorted_lst = sorted(lst)
    n = len(sorted_lst)
    mid = n // 2

    if n % 2 == 0:
        return sorted_lst[mid - 1]  # 偶数なら中央の小さい方
    else:
        return sorted_lst[mid]  # 奇数ならそのまま中央

# 画像ファイルを昇順で取得
image_files = sorted(glob.glob(r'ファイル名'))

# フレーム表示の遅延時間（ミリ秒単位、30FPS相当）
frame_delay = int(1/30 * 1000)

# 画像ファイルを1枚ずつ処理
for file in image_files:
    img = cv2.imread(file)

    # BGR画像をHSVに変換
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 黄色領域のHSV範囲（調整可能）
    lower_yellow = np.array([14, 100, 70])   # 下限：色相14、彩度100、輝度70
    upper_yellow = np.array([24, 255, 225])  # 上限：色相24、彩度255、輝度225

    # 黄色マスクの作成
    mask = cv2.inRange(hsv_img, lower_yellow, upper_yellow)

    # エッジ検出（Canny法）
    edges = cv2.Canny(mask, 50, 150)

    # Hough変換で直線検出
    lines = cv2.HoughLines(edges, 1, np.pi/180, 10)

    # y=110のときのx座標をリストに格納
    x_list = []
    y_target = 110

    try:
        for line in lines:
            rho, theta = line[0]
            a = np.cos(theta)
            b = np.sin(theta)
            x = (rho - y_target * b) / a
            x_list.append(x)

        # x座標の中央値を取得し、整数化
        x_median = int(median_in_list(x_list))

        # 中央点を緑の円で描画
        cv2.circle(img, (x_median, y_target), 5, (0, 255, 0), 1)

        # ウィンドウ設定と表示
        cv2.namedWindow("img", cv2.WINDOW_NORMAL)
        cv2.resizeWindow("img", 160, 120)
        cv2.imshow("img", img)

    except Exception as e:
        # エラーがあっても画像を表示
        cv2.namedWindow("img", cv2.WINDOW_NORMAL)
        cv2.resizeWindow("img", 160, 120)
        cv2.imshow("img", img)
        print(file)  # エラーが出た画像ファイル名を出力

    # 'q'キーで中断
    if cv2.waitKey(frame_delay) & 0xFF == ord('q'):
        break

# ウィンドウをすべて閉じる
cv2.destroyAllWindows()
~~~

それぞれのパラメーターは実際の画像を使いながら調整を行いました。
取得する座標は(x,y)のy=110の時の座標を取得します。またハフ変換で複数算出された直線は(x,y)のy=110の時の座標で、中央値となるxの値を算出するようにしてあります。

実装してみた映像です。検出した座標に緑色のサークルをつけて可視化しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/cc92fbd8-59c4-455b-89a3-aa7afbb399bb.gif" width="200">

いい感じに検出できているのではないでしょうか？緑のサークルが黄色レーンをしっかりと追っていることが確認できました。割とそこそこのカーブがあるコースでの画像を使ったのですがうまくいってそうです！これを使って報酬設計に組み込んでいきたいと思います。

### 報酬設計

~~~python
def reward(self, obs, forward_vel):
    reward = 0
    hsv_img = cv2.cvtColor(obs, cv2.COLOR_RGB2HSV)

    # 黄色領域を抽出
    mask = cv2.inRange(hsv_img, lower_yellow, upper_yellow)

    # エッジ検出
    edges = cv2.Canny(mask, 50, 150)

    # ハフ変換で直線検出
    lines = cv2.HoughLines(edges, 1, np.pi/180, 10)

    x_list = []
    y_target = 110  # y=110のライン上のx座標を取得

    try:
        for line in lines:
            rho, theta = line[0]
            a = np.cos(theta)
            b = np.sin(theta)

            x = (rho - y_target * b) / a
            x_list.append(x)

        # 中央付近のx座標を取得
        x_center = int(median_in_list(x_list))

        # 報酬設計
        if forward_vel > 0:
            if 64 <= x_center <= 96:
                reward += 2
                reward += forward_vel
            elif 32 <= x_center < 64 or 96 < x_center <= 128:
                reward += 1
                reward += forward_vel / 2

    except Exception:
        pass  # ラインが見つからない場合などは何もしない

    return reward
~~~
報酬設計となります。ポイントは、
* x_centerの値が64~96の場合(車線がほぼほぼ画面中央上にある場合)
  * **報酬 + (2 + 速度の値)**
* x_centerの値が32~64もしくは96~128の場合(車線が中央より少しそれた場合)
  * **報酬 + (1 + 速度の値/2 )**
* それ以外は0ポイント
* 車線から大きく外れてゲームオーバーとなった場合
    * -100ポイント

としました。

この報酬設計を使って強化学習を行っていきます！
学習のアルゴリズムは[前回の記事](https://qiita.com/gashu/items/1e0f9ceae5e97b85db73)同様です。

## 3.学習結果
500回ほどトレーニングして、エージェントの走行には明確な変化が現れました。従来のような「コース内をなんとなく走る」動きから、工場のAGVのように車線の中央を丁寧にトレースする走りへと進化しました。

**今回のカスタム報酬設計での自動運転↓**
<iframe width="560" height="315" src="https://www.youtube.com/embed/q-VkVrPXQTA?si=s7frgGd06XSzeLUI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

[前回記事](https://qiita.com/gashu/items/1e0f9ceae5e97b85db73)でのデフォルトの報酬設計での自動運転(比較のため置いています)↓
<iframe width="560" height="315" src="https://www.youtube.com/embed/ud9TgcnVzO0?si=9WsaZkVna40kWx7u" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

獲得報酬の結果です。
学習が進むごとに学んでいっていることがわかります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/e5c651f8-57d5-42a4-99c0-23d763a1d617.png" width="350">
やはり、コツをつかむと一気に成長していきますね。

ひとまず狙い通りの黄色車線上を走るということができるようになりました！
全く脱線する様子もなく、連続したカーブでは減速もうまく使いながら走っています。

### 課題
しかしながらいくつか課題が見えました。
* イレギュラーな要素があると脱線する
　今回トレーニングで使用したコースは非常にシンプルで、OpenCVで比較的簡単に車線検出できるようなコースでした。しかしながら、ちょっと複雑な他のコースで試走してみると、どのような走りをしたらいいのかわからなくなり脱線していました。(このコースではトレーニングしていません)。このようにイレギュラーな要素がある場合は、今回のOpenCVでのプログラムでは対応できないので、それに対応できる処理を報酬設計を組み込む必要があります。(障害物や鋭角なカーブなども含みます)。この辺はもう少し勉強が必要で、YOLOなどを使ったほうがいいかもしれません。

 <iframe width="400" height="315" src="https://www.youtube.com/embed/OswlNHG20aA?si=8LuD86cpPVHp_VAH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

　ただ、今回１つのコースレイアウトでしかトレーニングしていないものの、**普通にほかのコースレイアウトにも対応**できてて驚きました。しかも、**脱線しても元に戻ろうとする行動**も見れてディープラーニングの凄さを実感しました。


 この辺の課題は次の記事のネタとしてやっていこうと思います。

 ## まとめ
 今回、車線認識を報酬設計に組み込むことで、従来の「なんとなくコースを走る」だけだった走行が、車線中央を意識してトレースするスマートな挙動へと大きく進化しました。
 深層強化学習は「報酬がすべて」と言われることがありますが、今回の取り組みを通じて、改めてどの行動を良しとするか（＝報酬設計）がエージェントの性格を決定づけることを実感しました。
 また現実世界は、もっと複雑な要素が多数に存在するような場所で運転をするので、簡単には実現できないことが容易に想像ができます。複雑な環境下でも学習ができるようになるにはどうすればいいかは今後も勉強していきたいです。

## 参考

https://qiita.com/gashu/items/1e0f9ceae5e97b85db73