Airsimの画像 API

今回はAirsimの画像APIの記事を翻訳したものを記事にいたしました。C++でも同様なことができますが今回自分はPythonのコードのみを記載しております。C++をお使いの方は元の記事のコードをお使いください。
元のURLはこちらになります。https://microsoft.github.io/AirSim/image_apis/

# 画像 API
AirSim API に慣れていない場合は、まず一般的な API ドキュメントをお読みください。

# 単一の画像を取得する
「0」という名前のカメラから単一の画像を取得するサンプル コードを次に示します。 戻り値は、png 形式のイメージのバイト数です。 非圧縮形式やその他の形式、利用可能なカメラを取得するには、次のセクションを参照してください。

``` :Python
import airsim #pip install airsim

# for car use CarClient() 
client = airsim.MultirotorClient()

png_image = client.simGetImage("0", airsim.ImageType.Scene)
# do something with image
```

# より柔軟な画像の取得
simGetImage API よりも使用するのが少し複雑なsimGetImagesAPIでは単一の API 呼び出しで、左カメラ ビュー、右カメラ ビュー、および左カメラからの深度イメージを取得できます。 simGetImages API を使用すると、圧縮されていない画像だけでなく、浮動小数点のシングル チャネル画像 (3 チャネル (RGB)、各 8 ビットの代わりに) を取得することもできます。

```:Python
import airsim #pip install airsim

# for car use CarClient() 
client = airsim.MultirotorClient()

responses = client.simGetImages([
    # png format
    airsim.ImageRequest(0, airsim.ImageType.Scene), 
    # uncompressed RGB array bytes
    airsim.ImageRequest(1, airsim.ImageType.Scene, False, False),
    # floating point uncompressed image
    airsim.ImageRequest(1, airsim.ImageType.DepthPlanar, True)])

 # do something with response which contains image data, pose, timestamp etc
```

# NumPy で AirSim イメージを使用する
画像操作に numpy を使用する場合は、圧縮されていない RGB 画像を取得してから、次のように numpy に変換する必要があります。

```:Python
responses = client.simGetImages([airsim.ImageRequest("0", airsim.ImageType.Scene, False, False)])
response = responses[0]

# get numpy array
img1d = np.fromstring(response.image_data_uint8, dtype=np.uint8) 

# reshape array to 4 channel image array H X W X 4
img_rgb = img1d.reshape(response.height, response.width, 3)

# original image is fliped vertically
img_rgb = np.flipud(img_rgb)

# write to png 
airsim.write_png(os.path.normpath(filename + '.png'), img_rgb) 
```

## Quick Tips
+ API simGetImage はバイナリ文字列リテラルを返します。つまり、単純にバイナリ ファイルにダンプして .png ファイルを作成できます。 ただし、それ以外の方法で処理したい場合は、airsim.string_to_uint8_array 関数を便利に使用できます。 これは、バイナリ文字列リテラルを NumPy uint8 配列に変換します。
+ API simGetImages は、単一の呼び出しで任意のカメラから複数の画像タイプのリクエストを受け入れることができます。 画像が png 圧縮、RGB 非圧縮、または float 配列のいずれであるかを指定できます。 PNG 圧縮イメージの場合、バイナリ文字列リテラルを取得します。 float 配列の場合、float64 の Python リストを取得します。下記の関数を使用してこの float 配列をNumPy2D 配列に変換できます。

```:Python
airsim.list_to_2d_float_array(response.image_data_float, response.width, response.height)
```
airsim.write_pfm() 関数を使用して、float 配列を .pfm ファイル (Portable Float Map 形式)[^1] に保存することもできます。

+ いずれかの画像 API の呼び出しと同期して位置と方向の情報を照会する場合は、client.simPause(True) と client.simPause(False) を使用してシミュレーションを一時停止し、画像 API を呼び出して、 これにより、イメージ API 呼び出しの直後に物理状態が同じままであることを確認できます。

# すぐに実行できる完全な例
## Python
すぐに実行できる完全なサンプル コードについては、マルチローター用の AirSimClient プロジェクトまたは HelloCar サンプルのサンプル コードを参照してください。 このコードは、画像をファイルに保存したり、numpy を使用して画像を操作したりするなどの単純なアクティビティも示しています。

# 利用可能なカメラ
これらは、各車両ですでに利用可能なデフォルトのカメラです。 これらとは別に、車両にカメラを追加したり、設定を通じてどの車両にも取り付けられていない外部カメラを追加したりできます。

## Car
車のカメラには、API 呼び出しで次の名前でアクセスできます: front_center、front_right、front_left、fpv、および back_center。 ここで FPV カメラは車内のドライバーの頭の位置です。

## Multirotor
ドローンのカメラには、API 呼び出しで次の名前を使用してアクセスできます: front_center、front_right、front_left、bottom_center、および back_center。

## Computer Vision Mode
カメラ名はマルチローターと同じです。

## カメラ名の下位互換性
AirSim v1.2 より前は、名前の代わりに ID 番号を使用してカメラにアクセスしていました。 下位互換性のために、上記のカメラ名に対して次の ID 番号を上記と同じ順序で引き続き使用できます: 「0」、「1」、「2」、「3」、「4」。 さらに、カメラ名 "" を使用して、通常はカメラ "0" であるデフォルト カメラにアクセスすることもできます。

# "Computer Vision" Mode
AirSim は、いわゆる「コンピューター ビジョン」モードで使用できます。 このモードでは、物理エンジンは無効になり、ビークルはなく、カメラだけが表示されます (ビークルが必要で、キネマティクスが必要ない場合は、物理エンジン ExternalPhysicsEngine でマルチローター モードを使用できます)。 キーボードを使用して移動できます (キーのヘルプを表示するには F1 を使用します)。 記録ボタンを押すと、画像を連続的に生成できます。 または、API を呼び出してカメラを移動し、画像を撮影することもできます。

このモードを有効にするには、Documents\AirSim フォルダー (Linux では ~/Documents/AirSim) にある settings.json を編集し、次の値がルート レベルに存在することを確認します。

```:json
{
  "SettingsVersion": 1.2,
  "SimMode": "ComputerVision"
}
```
カメラを動かして画像をキャプチャする Python コードの例を次に示します。

このモードは、UnrealCV プロジェクトから着想を得た

## コンピュータビジョンモードでポーズを設定する
API を使用して環境内を移動するには、simSetVehiclePose API を使用できます。 この API は位置と方向を取得し、目に見えない車両のフロントセンターカメラにカメラの位置を設定します。 残りのすべてのカメラは、相対位置を維持しながら移動します。 位置 (または方向) を変更したくない場合は、位置 (または方向) のコンポーネントを浮動小数点の nan 値に設定するだけです。 simGetVehiclePose を使用すると、現在のポーズを取得できます。またsimGetGroundTruthKinematics を使用して、運動の運動量を取得することもできます。 セグメンテーション API、コリジョン API、カメラ API など、車両に固有ではない他の多くの API も利用できます。

## Camera APIs
simGetCameraInfo は、指定されたカメラのポーズ (ワールド フレーム、NED 座標、SI 単位) と FOV (度) を返します。 使用例をご覧ください。

simSetCameraPose は、入力ポーズを相対位置と NED フレームのクォータニオンの組み合わせとして取りながら、指定されたカメラのポーズを設定します。 便利な airsim.to_quaternion() 関数を使用すると、ピッチ、ロール、ヨーをクォータニオンに変換できます。 たとえば、同じ位置を維持しながら camera-0 を 15 度のピッチに設定するには、次を使用できます。

```:Python
camera_pose = airsim.Pose(airsim.Vector3r(0, 0, 0), airsim.to_quaternion(0.261799, 0, 0))  #PRY in radians
client.simSetCameraPose(0, camera_pose);
```
simSetCameraFov を使用すると、実行時にカメラの視野を変更できます。
simSetDistortionParams、simGetDistortionParams を使用すると、歪みパラメータ K1、K2、K3、P1、P2 を設定および取得できます。
すべてのカメラ API は、API 固有のパラメーター以外に、camera_name(str)、 vehicle_name(str)、および external(bool、外部カメラを示す) の 3 つの共通パラメーターを受け取ります。 カメラと車両名は、特定のカメラを取得するために使用されます。外部が true に設定されている場合、車両名は無視されます。 これらの API の使用例については、external_camera.py も参照してください。

## Gimbal[^2]
設定を使用して、任意のカメラのピッチ、ロール、またはヨーの安定化を設定できます。
使用例をご覧ください。

## 解像度とカメラ パラメータの変更
解像度、FOV などを変更するには、settings.json を使用できます。 たとえば、以下の settings.json への追加は、シーン キャプチャのパラメーターを設定し、上記の "Computer Vision" モードを使用します。 設定を省略した場合は、以下のデフォルト値が使用されます。 詳細については、設定ドキュメントを参照してください。 ステレオ カメラを使用している場合、現在、左右の距離は 25 cm に固定されています。

```:json
{
  "SettingsVersion": 1.2,
  "CameraDefaults": {
      "CaptureSettings": [
        {
          "ImageType": 0,
          "Width": 256,
          "Height": 144,
          "FOV_Degrees": 90,
          "AutoExposureSpeed": 100,
          "MotionBlurAmount": 0
        }
    ]
  },
  "SimMode": "ComputerVision"
}
```
## さまざまな画像タイプでのピクセル値の意味とは？
### Available ImageType Values
```
  Scene = 0, 
  DepthPlanar = 1, 
  DepthPerspective = 2,
  DepthVis = 3, 
  DisparityNormalized = 4,
  Segmentation = 5,
  SurfaceNormals = 6,
  Infrared = 7,
  OpticalFlow = 8,
  OpticalFlowVis = 9
```

### DepthPlanar と DepthPerspective
通常は深度画像を float として取得し (つまり、pixels_as_float = true に設定)、ImageRequest で ImageType = DepthPlanar または ImageType = DepthPerspective を指定します。 ImageType = DepthPlanar の場合、カメラ プレーンの深度を取得します。つまり、カメラに対してプレーン平行なすべてのポイントは同じ深度になります。 ImageType = DepthPerspective の場合、そのピクセルに当たる投影光線を使用して、カメラから深度を取得します。 ユースケースによっては、プランナー深度またはパースペクティブ深度が必要なグラウンド トゥルース イメージになる場合があります。 たとえば、ポイント クラウドを生成するために、depth_image_proc などの ROS パッケージに透視深度をフィードできる場合があります。 または、プランナー深度は、SGM などのステレオ アルゴリズムによって生成される推定深度画像との互換性が高くなる場合があります。

### DepthVis
ImageRequest で ImageType = DepthVis を指定すると、奥行きの視覚化に役立つ画像が得られます。 この場合、各ピクセル値は、メートル単位のカメラ プレーンの深度に応じて、黒から白に補間されます。 真っ白なピクセルは深度 100m 以上を意味し、真っ黒なピクセルは深度 0m を意味します。
### DisparityNormalized
通常、視差画像をフロートとして取得します (つまり、pixels_as_float = true を設定し、ImageRequest で ImageType = DisparityNormalized を指定します)。この場合、各ピクセルは (Xl - Xr)/Xmax であり、0 から 1 の間の値に正規化されます。

### Segmentation
ImageRequest で ImageType = Segmentation を指定すると、シーンのグラウンド トゥルース セグメンテーションを提供する画像が取得されます。 起動時に、AirSim は環境で使用可能な各メッシュに値 0 から 255 を割り当てます。 この値は、パレット内の特定の色にマップされます。 各オブジェクト ID の RGB 値は、このファイルにあります。

API を使用して、特定のメッシュに特定の値 (0 ～ 255 の範囲に制限) を割り当てることができます。 たとえば、以下の Python コードは、ブロック環境で「Ground」というメッシュのオブジェクト ID を 20 に設定するため、セグメンテーション ビューでその色を変更します。

```
success = client.simSetSegmentationObjectID("Ground", 20);
```
戻り値は、メッシュが見つかったかどうかを知らせるブール型です。

ブロックのような一般的な Unreal 環境には通常、同じオブジェクトで構成される他の多くのメッシュ (「Ground_2」、「Ground_3」など) があることに注意してください。 これらすべてのメッシュにオブジェクト ID を設定するのは面倒なので、AirSim は正規表現もサポートしています。 たとえば、以下のコードは、名前が「ground」で始まるすべてのメッシュ (大文字と小文字を区別しない) を 1 行で 21 に設定します。
```
success = client.simSetSegmentationObjectID("ground[\w]*", 21, True);
```
正規表現マッチングを使用して少なくとも 1 つのメッシュが見つかった場合、戻り値は true です。

セグメンテーション画像の正確な RGB 値を確実に取得するために、この API を使用して非圧縮画像をリクエストすることをお勧めします。

```:Python
responses = client.simGetImages([ImageRequest(0, AirSimImageType.Segmentation, False, False)])
img1d = np.fromstring(response.image_data_uint8, dtype=np.uint8) #get numpy array
img_rgb = img1d.reshape(response.height, response.width, 3) #reshape array to 3 channel image array H X W X 3
img_rgb = np.flipud(img_rgb) #original image is fliped vertically

#find unique colors
print(np.unique(img_rgb[:,:,0], return_counts=True)) #red
print(np.unique(img_rgb[:,:,1], return_counts=True)) #green
print(np.unique(img_rgb[:,:,2], return_counts=True)) #blue  
```
すぐに実行できる完全な例は、segmentation.py にあります。
### Unsetting object ID
オブジェクトの ID を -1 に設定して、セグメンテーション イメージに表示されないようにすることができます。

### メッシュ名を見つける方法は?
目的のグラウンド トゥルース セグメンテーションを取得するには、Unreal 環境のメッシュの名前を知る必要があります。 これを行うには、Unreal Editor で Unreal Environment を開き、World Outliner を使用して関心のあるメッシュの名前を調べる必要があります。 たとえば、以下のエディタの右側のパネルにブロック環境の地面のメッシュ名が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961001/53067604-8769-ffaf-87cb-1a4d0f39f9d1.png)

Unreal Editor で Unreal Environment を開く方法がわからない場合は、ソースからビルドするためのガイドに従ってみてください。

関心のあるメッシュを決定したら、それらの名前を書き留め、上記の API を使用してそれらのオブジェクト ID を設定します。 オブジェクト ID の生成動作を変更するために使用できる設定はほとんどありません。
### オブジェクト ID の色の変更
現在、このパレットのように各オブジェクト ID の色が固定されています。 オブジェクト ID の色を目的の値に変更する機能を間もなく追加する予定です。 それまでの間、お気に入りの画像エディターでセグメンテーション画像を開いて、関心のある RGB 値を取得できます。
### 起動オブジェクト ID
最初に、AirSim は UStaticMeshComponent または ALandscapeProxy タイプの環境で見つかった各オブジェクトにオブジェクト ID を割り当てます。 次に、メッシュ名または所有者名 (設定に応じて) を使用し、それを小文字にし、数字と句読点を削除するために ASCII 97 未満のすべての文字を削除し、すべての文字の int 値を合計し、モジュロ 255 を使用してオブジェクト ID を生成します。 つまり、同じアルファベット文字を持つすべてのオブジェクトは同じオブジェクト ID を取得します。 このヒューリスティックはシンプルで、多くの Unreal 環境で効果的ですが、必要なものではない場合があります。 その場合は、上記の API を使用して、オブジェクト ID を目的の値に変更してください。 この動作を変更できる設定はほとんどありません。
### メッシュのオブジェクト ID の取得
simGetSegmentationObjectID API を使用すると、特定のメッシュ名のオブジェクト ID を取得できます。

### 赤外線
現在、これはオブジェクト ID からグレー スケール 0 ～ 255 への単なるマップです。 そのため、オブジェクト ID 42 のメッシュは、カラー (42、42、42) で表示されます。 オブジェクト ID の設定方法の詳細については、セグメンテーション セクションを参照してください。 通常、この画像タイプにノイズ設定を適用して、よりリアルな効果を得ることができます。 私たちはまだ他の赤外線アーティファクトの追加に取り組んでおり、貢献は大歓迎です。

### OpticalFlow and OpticalFlowVis
これらのイメージ タイプは、カメラの視点によって認識された動きに関する情報を返します。 OpticalFlow は、チャネルがそれぞれ vx と vy に対応する 2 チャネル イメージを返します。 OpticalFlowVis は OpticalFlow に似ていますが、フロー データを RGB に変換して、より「視覚的な」出力を実現します。

## Example Code
車両の位置をランダムな位置と方向に設定してから画像を取得する完全な例は、GenerateImageGenerator.hpp にあります。 この例では、指定された数のステレオ イメージとグラウンド トゥルース視差イメージを生成し、pfm 形式で保存します。

# 補足
[^1]: PFMファイルは一ファイル形式に関連付けられており、Microsoft CorporationのMicrosoft Windows Font Viewerを用いて表示できます
[^2]: ジンバルとは手ぶれ補正装置の一種