オープンソース運転シミュレーター『CARLA』を使って機械学習等で使えそうな交通3D映像を作成してみる

# オープンソース運転シミュレーター『CARLA』を使って機械学習等で使えそうな交通3D映像を作成してみる

こんにちは、NTTテクノクロス遠藤です。

交通映像を使った映像推論は鉄板な一方、いざやろうとする学習用の映像の調達に苦しむことがよくあると思います。
特に、以下のような条件が絡んでくるとフリーの動画ではカバーしきれなくなります。
- 特定の画角/台数のカメラを使いたい
- 任意のシチュエーションを作りたい

そんな時に使える映像生成の1案として、今回はCARLA Simulatorを使った映像の生成について紹介します。

## 対象読者
- 「既存モデル(Yoloなど)を使った推論／検証」を行いたいが、良いデータがない/任意のものを作成したい
- CARLAを使ったカメラ映像の取得について知りたい
- CARLAのカメラでfpsや解像度の調整ができるのか知りたい


## CARLAとは？なぜCARLA

CARLAはUnreal Engine(以降UEと表記します)をベースとした自動運転研究用のオープンソースシミュレータで、多様なシチュエーションの交通映像を手軽に作成できます。
本来だと自動運転アプリの検証が大きな目的のようですが、
今回はそれ以外も含めた交通映像に対する機械学習/推論を目的とした、データセットとしての映像の取得に絞って扱ってみました。

コミュニティも活発で、英語も含めるとインターネット上の情報が多いという点も魅力的です。
今回はUE4ベースのものを使用しますが、UE5版が進行しており、そちらは映像の進化が半端なさそうなのでとても期待しています。

## 実際に作成できる映像のサンプル

[公式のYoutube](https://www.youtube.com/watch?v=tWAg1fIDNCg)でも映像が公開されています

今回は、車載カメラではなく任意の位置にカメラを生成して映像を取得していきます。
※gif変換をした都合上画像が荒くなっていますが、画質は任意に設定可能です。
![camera1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1513661/02c99c70-7b27-677a-1582-3be52670304c.gif)


## 環境
今回、サーバとクライアントは同一端末で動かしています。  
高解像度のカメラを入れるとかなり重くなります。
カメラ1台くらいだと公式推奨値でも動作しましたが、カメラの台数を増やす場合はGPUがしっかり積まれたサーバを使うのがよさそうです。
- サーバ
  - OS:Widows 10
  - メモリ:32GB
  - GPU：Intel(R) Iris(R) Xe Graphics 16GB
- CARLA バージョン
  - サーバ：0.9.12
  - クライアント：0.9.12

※公式の推奨値
https://carla.readthedocs.io/en/0.9.12/start_quickstart/#BeforeYouBegin


## 環境構築について
環境構築については、公式サイトもしくはほかの方の記事をご参照ください。
Windowsの場合、ダウンロードしたexeを実行するだけで動くかと思います。

※クライアント側のセットアップも必要となりますのでご注意ください(quickstart内、Install client library)

https://carla.readthedocs.io/en/0.9.12/start_quickstart/

## カメラ映像の取得

まずは、カメラの配置と映像の取得を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1513661/27f7924f-4881-073b-c438-8bcefe45c170.png)

### カメラを配置して映像を取得するスクリプト

※一部プログラムはCARLA公式のサンプルスクリプトを参考にさせていただいています。
　広範なサンプルが用意されており非常に使いやすいです。
　https://github.com/carla-simulator/carla/tree/ue4-dev/PythonAPI/examples

```python
import glob
import os
import os.path
import sys
import time
import carla
import os
import queue
import cv2
import numpy as np
from datetime import datetime

try:
    sys.path.append(glob.glob('../carla/dist/carla-*%d.%d-%s.egg' % (
        sys.version_info.major,
        sys.version_info.minor,
        'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])
except IndexError:
    pass

import carla

# カメラから出力された画像をフォルダに格納する関数
def save_image(image, folder, frame_number):
    capture = np.reshape(np.copy(image.raw_data), (image.height, image.width, 4))
    filename = os.path.join(folder, f"frame_{frame_number:06d}.png")
    image.save_to_disk(filename)  



def main():
    # サーバへの接続
    client = carla.Client("localhost",2000)
    client.set_timeout(10.0)

    cameras = []
    try:
        world = client.get_world()
        traffic_manager = client.get_trafficmanager(8000)
        settings = world.get_settings()

        # 同期モードに設定(カメラの処理に時間がかかるため、処理が終わるまでワールドを待機させられるようにする)
        traffic_manager.set_synchronous_mode(True)
        settings.synchronous_mode = True
        settings.fixed_delta_seconds = 0.0333
       
        # カメラオブジェクトの生成
        # step1 ブループリントの取得/設定
        camera_bp = world.get_blueprint_library().find('sensor.camera.rgb')
        camera_bp.set_attribute('image_size_x', '1920')
        camera_bp.set_attribute('image_size_y', '1080')

        # カメラ位置を指定しスポーン(カメラはSpectatorなどで確認した任意の位置を指定)
        position = carla.Transform(carla.Location(x=-74.0, y=120.0, z=15.0), carla.Rotation(pitch=-23.0, yaw=23.0, roll=0.000000))

        camera = world.spawn_actor(camera_bp, position)
        cameras.append(camera)

        # 出力先フォルダを定義
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        cwd = os.getcwd()
        base_output_dir = os.path.join(cwd, f"carla_output_{timestamp}")
        os.makedirs(base_output_dir, exist_ok=True)

        # カメラが毎フレームで取得した画像が格納されるキューを作成
        q = queue.Queue()

        # カメラのコールバックを指定
        # ここでは単に、カメラが取得した画像をキューに格納する
        camera.listen(lambda image, q=q: q.put(image))

        # 同期モードでworld.tick()を使いながら、シミュレーションを進行させる
        frame_number=0
        while True:
            world.tick()

            # 毎フレームでキューに格納された画像を画像処理用関数に引き渡す
            image = q.get()
            save_image(image, os.path.join(base_output_dir, f"camera_1"), frame_number)
            frame_number += 1

    finally:   
        # 使い終わったオブジェクトは破棄する。残ると次回のシミュレーション実行に影響あり(CARLAを再起動すれば問題ないが)
        settings = world.get_settings()
        settings.synchronous_mode = False
        settings.fixed_delta_seconds = None
        world.apply_settings(settings)

        for i in range(len(cameras)):
            cameras[i].destroy()
        # オブジェクトの破壊を待つ
        time.sleep(0.5)

if __name__ == '__main__':

    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        print('\ndone.')
```



### 解説

#### step1:同期モードに設定

カメラを動かすとシミュレーションに非常に時間がかかるため、
同期モードで動かすことが推奨されます。
- 同期モード：
    - クライアント側で意図的にシミュレーションを進行させる(world.tick()を使うことで、fixed_delta_time分シミュレーションが進行)
- 非同期モード：
    - クライアント側の動作にかかわらずシミュレーションが進行する

```python
        # 同期モードに設定(カメラの処理に時間がかかるため、処理が終わるまでワールドを待機させられるようにする)
        traffic_manager.set_synchronous_mode(True)
        settings.synchronous_mode = True
        settings.fixed_delta_seconds = 0.0333
```

#### step2:カメラの生成
カメラのブループリント(設計書のようなもの)を取得して、画像サイズを定義します。
この時、画角や被写界深度、ブレのon/off等様々な設定が可能です。(便利)
https://carla.readthedocs.io/en/0.9.12/ref_sensors/#rgb-camera

この時、ついでに出力先のフォルダも作成してあげましょう。
[補足①カメラ位置の決め方](#補足カメラ位置の決め方)

```python
        # カメラオブジェクトの生成
        # step1 ブループリントの取得/設定
        camera_bp = world.get_blueprint_library().find('sensor.camera.rgb')
        camera_bp.set_attribute('image_size_x', '1920')
        camera_bp.set_attribute('image_size_y', '1080')

        # カメラ位置を指定しスポーン(カメラはSpectatorなどで確認した任意の位置を指定)
        position = carla.Transform(carla.Location(x=-74.0, y=120.0, z=15.0), carla.Rotation(pitch=-23.0, yaw=23.0, roll=0.000000))

        camera = world.spawn_actor(camera_bp, position)
        cameras.append(camera)

        # 出力先フォルダを定義
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        cwd = os.getcwd()
        base_output_dir = os.path.join(cwd, f"carla_output_{timestamp}")
        os.makedirs(base_output_dir, exist_ok=True)
```

#### step3:カメラからの出力を受け取る準備
カメラはフレームごとに画像を出力してくれますが、受け取るためにはコールバック関数の紐づけが必要です。
ここではキューを作成し、そのキューに出力結果を格納するという実装をします。

```python

        # カメラが毎フレームで取得した画像が格納されるキューを作成
        q = queue.Queue()

        # カメラのコールバックを指定
        # ここでは単に、カメラが取得した画像をキューに格納する
        camera.listen(lambda image, q=q: q.put(image))
```

#### step4:1フレームずつシミュレーションを進行させつつ、画像処理を行う。
同期モードではクライアント側で意図的にシミュレーションを進行させます。
ここでは、毎フレームでイメージのデキュー、画像処理関数への引き渡しが終わったことを確認してから次のフレームに進むようにしています。

※なお、コールバック関数で直接画像保存を行ってもよいのですが、複数カメラを作成する場合などはキューを配列に入れるなどして後から処理した方が諸々やりやすいです。

```python
        # 同期モードでworld.tick()を使いながら、シミュレーションを進行させる
        frame_number=0
        while True:
            world.tick()

            # 毎フレームでキューに格納された画像を画像処理用関数に引き渡す
            image = q.get()
            save_image(image, os.path.join(base_output_dir, f"camera_1"), frame_number)
            frame_number += 1

```

上手くいけば、出力先フォルダに以下のようなフレームごとの画像ファイルが蓄積されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1513661/81662dfa-e08e-2152-81e0-bd11402c8073.png)


#### step5:連続画像を映像変換する

今のままだと、あくまで連続したpngの画像がたまっている状態です。
何かしら好みのライブラリ等を使い、映像変換しましょう。

[補足②連続画像の映像変換](#補足連続画像の映像変換)

## 車を動かした状態で映像を撮る

ここまで映像を取得する方法に集中したため、このままではただの交差点が流れるのみです。
簡単のため、ここではお手軽にCARLAの公式サンプルを使用したいと思います。
https://github.com/carla-simulator/carla/blob/238f84aa4fc1858fad33da8aec7a750c016012e6/PythonAPI/examples/generate_traffic.py

1. generate_traffic.pyを起動する
1. そのまま、今回作成したスクリプトを起動する

これで、generate_traffic.pyで生成された自動走行の車両をカメラでとらえることができます。
(冒頭のサンプル映像)

## 終わりに

本記事ではカメラからの映像取得に絞ってしまったので、以下のような部分は紹介できませんでした。
CARLAの機能としては実現可能な部分ですので、いずれQiitaでまとめようと思います！
- 任意のシチュエーションの作成
- 自動走行させる車両のパラメータチューニング
- 時間帯/天候/Mapの指定

最後まで読んでいただき、ありがとうございました。

### 補足達

#### 補足①カメラ位置の決め方
今回はT字の交差点にカメラを設置してみました。
私がカメラ位置を決めるときは、
- ①Spectatorをぐりぐり動かしていい感じの位置を見つける
- ⇒座標を出力
というやり方をとっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1513661/609541be-bd5a-f7ce-ab15-1872b1932cff.png)

```python
# Spectatorの位置座標の出力スクリプト
from importlib.util import spec_from_file_location
import carla 

def main():
    # Connect to the CARLA server
    client = carla.Client('localhost', 2000)
    client.set_timeout(10.0)
    world = client.get_world()
    spectator = world.get_spectator()
    print(spectator.get_transform())

if __name__ == '__main__':
    main()
```

```
出力結果
Transform(Location(x=-74.858490, y=120.652077, z=15.410233), Rotation(pitch=-23.934654, yaw=23.589977, roll=0.000000))
```

#### 補足②出力画像の加工及び保存

今回は、CARLAのimageオブジェクトのsave_to_deskメソッドを使用した単純な実装としました。

```python
def save_image(image, folder, frame_number):
    capture = np.reshape(np.copy(image.raw_data), (image.height, image.width, 4))
    filename = os.path.join(folder, f"frame_{frame_number:06d}.png")
    image.save_to_disk(filename)  

```

OpenCVを使うことで、以下のようにjpeg変換して保存するようなことも可能です。
映像の要件によってカスタマイズしてみてください。
```python
def save_image(image, folder, frame_number):
    array = np.frombuffer(image.raw_data, dtype=np.dtype("uint8"))
    array = np.reshape(array, (image.height, image.width, 4))
    array = array[:, :, :3]
    array = array[:, :, ::-1]  # BGR to RGB
    filename = os.path.join(folder, f"frame_{frame_number:06d}.jpg")
    cv2.imwrite(filename, array, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
```



#### 補足③連続画像の映像変換

ffmpegが使いやすかったです。
私の場合、[こちらのQiitaの記事](https://qiita.com/livlea/items/a94df4667c0eb37d859f)を参考にさせていただき、以下のようなスクリプトをコマンドラインから実行していました。

```sh
cd "出力先ディレクトリ"/camera_1
ffmpeg -r 30 -i frame_%06d.png -vcodec libx264 -pix_fmt yuv420p -r 30 out.mp4

```