※当サイトはPRを含みます

【画像処理】鳥瞰図変換(Bird’s Eye View Transformation)

鳥瞰図変換とは?

鳥瞰図変換(Bird’s Eye View, BEV)とは、カメラで撮影された画像を、上空から見下ろしたような視点に変換する技術です。特に自動運転やロボットビジョンの分野で使用され、車両周辺の環境を俯瞰的に把握するために活用されます。

参考

鳥瞰図変換の仕組み

透視投影と逆透視投影

一般的なカメラは透視投影により3D空間の情報を2D画像に変換します。このため、遠くの物体は小さく、近くの物体は大きく見えます。

鳥瞰図変換では、逆透視投影(Inverse Perspective Mapping, IPM)を適用し、2D画像をカメラの位置や角度を考慮してワールド座標系へ再投影します。これにより、地面に平行な対象を正しいスケールで表示できます。

https://www.ridgerun.com/post/case-study-using-libpanorama-to-generate-birds-eye-view-scenes

画像座標系とワールド座標系

画像座標系はカメラセンサー上の2D座標系であり、ピクセル単位で表されます。一方、ワールド座標系は3D空間における座標系で、実際の距離(メートルなど)で表現されます。

変換を行うには、カメラの内部パラメータと外部パラメータを用いて、画像座標系からワールド座標系へマッピングを行います。

必要なパラメータ

カメラの内部パラメータ(Intrinsic Parameters)

カメラの内部パラメータは、カメラの光学特性を定義するもので、以下の情報を含みます。

  • 焦点距離 (fx, fy)
  • 画像の中心座標 (u0, v0)
  • 画像の歪み補正パラメータ(今回は省略)

これらはキャリブレーションによって取得され、カメラ行列 K として表現されます。

K = np.array([
    [fx,  0, u0, 0],
    [ 0, fy, v0, 0],
    [ 0,  0,  1, 0],
    [ 0,  0,  0, 1]
])

カメラの外部パラメータ(Extrinsic Parameters)

外部パラメータは、カメラの位置と向きを表す情報です。

  • 位置 (x, y, z)
  • 回転 (roll, pitch, yaw)

これらを用いて、カメラの姿勢を示す回転行列 R と、位置を示す並進行列 T を作成し、最終的な RT 行列を計算します。

R_veh2cam = np.transpose(rotation_from_euler(roll, pitch, yaw))
T_veh2cam = translation_matrix((-x, -y, -z))
R = np.array([
    [0., -1., 0., 0.],
    [0.,  0., -1., 0.],
    [1.,  0.,  0., 0.],
    [0.,  0.,  0., 1.]
])
RT = R @ R_veh2cam @ T_veh2cam

この R は、カメラ座標系から車両座標系(もしくはその逆)への座標変換を行う回転行列です。具体的には、座標軸の向きを変えるために用いられます。

  • X軸(カメラ座標系の右方向)は、新しい座標系では Z軸(前方向) にマッピングされる。
  • Y軸(カメラ座標系の下方向)は、新しい座標系では X軸(左方向) にマッピングされる(符号反転)。
  • Z軸(カメラ座標系の前方向)は、新しい座標系では Y軸(下方向) にマッピングされる(符号反転)。

この回転行列 R を適用することで、カメラで取得した座標を車両座標系に合わせることができます。

参考

変換対象の平面

変換する対象の平面は、鳥瞰図を生成する際に必要な基準となります。平面のパラメータには以下が含まれます。

  • col, row: 変換後の画像サイズ
  • scale: 解像度(1ピクセルあたりの距離)
  • x, y, z: 平面の位置

元画像の情報をあらかじめ作成した空白の平面に移していく処理になります。

class Plane:
    """
    Defines a plane in the world
    """

    def __init__(self, x, y, z, roll, pitch, yaw,
                 col, row, scale):
        self.x, self.y, self.z = x, y, z
        self.roll, self.pitch, self.yaw = roll, pitch, yaw

        self.col, self.row = col, row
        self.scale = scale

        self.xyz = self.xyz_coord()

    def xyz_coord(self):
        """
        Returns:
            Grid coordinate: [b, 3/4, row*cols]
        """
        xmin = self.x
        xmax = self.x + self.col * self.scale
        ymin = self.y
        ymax = self.y + self.row * self.scale
        return meshgrid(xmin, xmax, self.col,
                        ymin, ymax, self.row)

plane = Plane(0, -25, 0, 0, 0, 0, TARGET_H, TARGET_W, 0.1)

ピクセル値の補完

バイリニア補間を用いて、鳥瞰図上のピクセル値を元の画像から補間します。
これは、4 つの角の (距離による) 重み付けされた合計を取得することによって行われます。

def bilinear_sampler(imgs, pix_coords):
    pix_x, pix_y = np.split(pix_coords, [1], axis=-1)
    pix_x0 = np.floor(pix_x)
    pix_y0 = np.floor(pix_y)
    pix_x1, pix_y1 = pix_x0 + 1, pix_y0 + 1
    
    # 画像の境界を超えないようにクリッピング
    pix_x0 = np.clip(pix_x0, 0, img_w - 1)
    pix_y0 = np.clip(pix_y0, 0, img_h - 1)
    pix_x1 = np.clip(pix_x1, 0, img_w - 1)
    pix_y1 = np.clip(pix_y1, 0, img_h - 1)
    
    # 重み計算
    w00 = (pix_x1 - pix_x) * (pix_y1 - pix_y)
    w01 = (pix_x1 - pix_x) * (pix_y - pix_y0)
    w10 = (pix_x - pix_x0) * (pix_y1 - pix_y)
    w11 = (pix_x - pix_x0) * (pix_y - pix_y0)
    
    # 画像からピクセルを取得
    im00 = imgs[pix_y0.astype(int), pix_x0.astype(int)]
    im01 = imgs[pix_y1.astype(int), pix_x0.astype(int)]
    im10 = imgs[pix_y0.astype(int), pix_x1.astype(int)]
    im11 = imgs[pix_y1.astype(int), pix_x1.astype(int)]
    
    return w00 * im00 + w01 * im01 + w10 * im10 + w11 * im11

逆透視投影変換の実行

逆透視投影を行い、鳥瞰図を作成します。

def ipm_from_parameters(image, xyz, K, R):
    # Flip y points positive upwards
    xyz[1] = -xyz[1]

    P = K @ RT
    pixel_coords = perspective(xyz, P, TARGET_H, TARGET_W)
    image2 = bilinear_sampler(image, pixel_coords)
    return image2.astype(np.uint8)