【OpenCV】ステレオ画像の奥行きを推定してみた(その2)

2020.08.05

カフェチームの山本です。

現在、カフェではコンピュータビジョン用のカメラとして、Intel製のRealSense Depth Camera D435Iというデバイスを利用し、RGB画像とDepth画像を取得しています。RGB画像から骨格を検出し、関節の位置のDepthを取得することで、関節の3次元位置を計測しています。

RealSenseを通常のWebカメラに置き換えることで、コストを抑えられる可能性があるため、Depthを取得する処理を、通常のWebカメラで実現する方法を調べています。

前回は、ステレオ画像から物体の写っている位置の視差を計算するために、OpenCVのStereoBMという機能を利用してみました。

【OpenCV】ステレオ画像の奥行きを推定してみた

今回は、StereoBMで得られた結果から、実際の距離に変換する処理を実装します。また、UnrealEnginで作成したステレオ画像(CG)で正しく距離を計算できることを確認します。

お詫び

本ページの数式が正しく表示されていません。現在、修正中です。数式まで見たい方はこちらのページをご覧ください。

【OpenCV】ステレオ画像の奥行きを推定してみた(その2)

状況の整理

前回の出力

前回のOpenCVのStereoBMを実行したコードと、実行結果は以下のようにでした。上2つが入力した画像で、出力3つ目が出力です。出力の値を見てみると、値が大きい箇所で1400以上あり、なんの値だか良くわかりません。

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=15)

StereoBMの仕様

StereoBMの仕様について調べると、こちらのページで以下のように書かれています。

disparity: Output disparity map. It has the same size as the input images. Some algorithms, like StereoBM or StereoSGBM compute 16-bit fixed-point disparity map (where each disparity value has 4 fractional bits), whereas other algorithms output 32-bit floating-point disparity map.

要約すると、StereoBMの出力は視差(ピクセルのズレ幅)を表し、値は16倍されて(小数点以下の数値が4bit分追加されて)出力されるようです。たしかに、前項の出力を1/16すると最大値がStereoBMのパラメータであるnumDisparitiesの値(96)近くであり、ピクセル数を表していることがわかります。

(例えば、画面右端の車の黄色い箇所が、左右の画像で90(=1400/16)程度ピクセルずれている、ということを表します)

計算・実装

視差→距離 の計算方法

前回の記事に書いたように、視差とdepthには以下のような関係があります。

\[{\rm disparity} = x - x'= \frac{Bf}{Z}\]

よって、depth(Z)を求めるには、以下の計算をすれば良いことになります。

\[Z = \frac{Bf}{{\rm disparity}}\]

このdisparityに対応する値が、StereoBMで得られます。ただし、上の数式の各変数は距離を表しているのに対し、StereoBMの出力はピクセル数を表し、かつ、(前節で述べた通り)16倍されているため、補正する必要があります。

StereoBMの出力をdisparity_px'とし、スクリーン上で距離1に対応するピクセル数をratioとすると、以下の用に計算することになります。

\[{\rm disparity\_px}=\frac{\rm disparity\_px'}{16}\]

\[{\rm disparity}=\frac{\rm disparity\_px}{\rm ratio}\]

通常の解説では、焦点距離とセンササイズが利用されていますが、今回は、それらの代わりに視野角(FOV)を利用します。カメラの水平視野角をfov[rad]とし、撮影画像の横幅解像度をwとすると、ratioは以下のように計算できます。

\[{\rm ratio} = \frac{\frac{w}{2}}{f\tan {\frac{\rm fov}{2}}}\]

最終的なdepthの計算は、上の値を使って以下のようになります。

\[\begin{matrix} Z &=& \frac{Bf}{{\rm disparity}} \\ &=& \frac{Bf}{{\rm {\rm diparity\_px}/{\rm ratio}}} \\ &=& \frac{Bf{\rm ratio}}{{\rm {\rm diparity\_px'/16}}} \\ &=& \frac{16Bf{\rm ratio}}{{\rm {\rm diparity\_px'}}}\end{matrix}\]

(もっと書くと以下のとおりです。fには依存しません。)

\[\begin{matrix} Z &=& \frac{16Bf \frac{\frac{w}{2}}{f\tan {\frac{\rm fov}{2}}}}{{\rm {\rm diparity\_px'}}} \\ &=& \frac{16B w}{{2\tan {\frac{\rm fov}{2}}\rm {\rm diparity\_px'}}} \end{matrix}\]

実装コード

実装したコードは以下のようです。環境はJupyterを使用しました。

画像を読み込む

import numpy as np
import cv2
from matplotlib import pyplot as plt
%matplotlib inline

# read images
imgL = cv2.imread('unrealcv_desk_l.png', 0)
imgR = cv2.imread('unrealcv_desk_r.png', 0)

# left camera image
plt.figure(figsize=(13,3))
plt.imshow(imgL)
plt.show()

# right camera image
plt.figure(figsize=(13,3))
plt.imshow(imgR)
plt.show()

視差を計算する

# estimate depth
stereo = cv2.StereoBM_create(numDisparities=64, blockSize=15)
disparity_px_16 = stereo.compute(imgL, imgR)

# raw output
plt.figure(figsize=(13,3))
plt.imshow(disparity_px_16)
plt.colorbar()
plt.show()

# output value in pixel
plt.figure(figsize=(13,3))
plt.imshow(disparity_px_16/16)
plt.colorbar()
plt.show()

距離に変換する

以下のコードでは、StereoBMの出力は、距離が計算できない箇所はマイナスになったり、視差がない箇所は0になる場合があるため、対象でない値域のピクセルに0を代入し直して表示しています。

# camera parameters
fov = 90  # [deg]
B = 20  # [cm]
width = disparity_px_16.shape[1]  # [px]
# f = 1.0 # [cm]

# ratio: px per cm
fov_ = fov * np.pi / 180  # [rad]
ratio = (width/2) / np.tan(fov_/2)

# convert disparity to depth
depth = B * ratio * 16.0 / disparity_px_16
# this line above is equivalent to the line below
# depth = B * f / (disparity_px_16 / 16.0 / ratio)

# set unexpected values to zero 
depth[np.where(depth < 0)] = 0
depth[np.where(depth > 500)] = 0

# show result
plt.imshow(depth)
plt.colorbar()
plt.show()

動作確認(3DCG)

今回はUnrealEngineで作成したステレオ画像を利用しました。予めdepthがわかっているステレオ画像がある場合はそちらを使っても構いません。

Unreal Engine・UnrealCVのインストール

UnrealEngine・UnrealCVのインストール方法や動かし方については、同じチームの宮島が執筆した記事をご覧ください。

Unreal Engine 4で作ったバーチャル空間上で3Dモデルがぬるぬるアニメーションしているところを複数視点でcubemosの骨格検知にかけてみた | Developers.IO

  • 留意点

インストールするUnreal Engineのバージョンは、最新のもの(v4.26)ではなく、上記の記事で書かれているバージョン(v4.20)を選択した方が良いと思われます。自分は間違えて、Unreal Engineのv4.26を選択してしまい、「UnrealCVプラグインのビルド」でエラーとなり進めませんでした。

正しいバージョンを選択すればビルドできると思いますが、プラグインのビルドをする代わりに、ビルド済みのプラグインを利用することもできます。Githubのリポジトリからダウンロードして、Unreal EngineのPluginのフォルダに入れれば、インストールできます。この場合、インストールするUnreal Engineのバージョンは、(リンク先に書かれている通り)v4.16を選択する必要があるので、ご注意ください。ただ、バージョンが古いと動かないアセットがあるようです(今回の記事の範囲では問題ありませんでした)。

データ作成(UnrealCVによるシーン撮影)

実行したコードは以下のとおりです。カメラのパラメータは、視野角 FoV=90°、横ピクセル数=640[px]、カメラ間距離 B=20[cm]です。

import numpy as np
import cv2
import PIL.Image as Image
from io import BytesIO
from unrealcv import client

def get_frame(client, camera_id=0, location=None, rotation=None, fov=None, img_ext="png"):
    if not location is None:
        location_str = " ".join(map(str, location))
        client.request(f'vset /camera/{camera_id}/location {location_str}') # x y z
    if not rotation is None:
        rotation_str = " ".join(map(str, rotation))
        client.request(f'vset /camera/{camera_id}/rotation {rotation_str}') # pitch yaw roll
    if not fov is None:
        client.request(f'vset /camera/{camera_id}/horizontal_fieldofview {fov}') # pitch yaw roll

    res = client.request(f'vget /camera/{camera_id}/lit {img_ext}')

    img = Image.open(BytesIO(res))
    npy = np.asarray(img)[:,:,:3]

    return cv2.cvtColor(npy, cv2.COLOR_RGB2BGR)

if __name__ == "__main__":
    try:
        res = client.connect() # connect to UE4 via UnrealCV
        print(res)

        img_l = get_frame(client, location=[125, 20, 130], rotation=[0, 180, 0], fov=90)
        img_r = get_frame(client, location=[125, 0, 130], rotation=[0, 180, 0], fov=90)

        cv2.imwrite('unrealcv_desk_l.png', img_l)
        cv2.imwrite('unrealcv_desk_r.png', img_r)

    finally:
        client.disconnect()
        cv2.destroyAllWindows()

2シーン撮影し、以下のような画像2枚*2セットを出力しました。左カメラ画像・右カメラ画像の順に並べています。

  • シーン1:カメラから300cm離したところに、家具を設置したシーン

  • シーン2:床から400cmにカメラを設置し、人物を配置したシーン

実行結果

撮影したステレオ画像から、距離に変換した結果は以下のとおりです。

  • シーン1

床の一部は計算できていませんが、机や椅子の奥行きは正しく計算できているように見えます。

机の根本付近の値を取ると以下のようになっていました。カメラと机の距離は300cmであるため、正しく距離が計算できていると言えそうです。

depth[340, 330:350]

出力

array([285.2367688 , 285.2367688 , 289.26553672, 295.10086455,
       295.95375723, 296.8115942 , 297.6744186 , 296.8115942 ,
       296.8115942 , 296.8115942 , 296.8115942 , 295.95375723,
       295.95375723, 296.8115942 , 296.8115942 , 296.8115942 ,
       295.95375723, 295.10086455, 294.25287356, 294.25287356,
       294.25287356, 295.10086455, 295.10086455, 295.10086455,
       295.10086455, 295.95375723, 295.95375723, 296.8115942 ,
       297.6744186 , 297.6744186 ])
  • シーン2

人体が映っている領域で一部計算できていない箇所がありますが、概ね正しく計算できているように見えます。

以下のように計算された距離を見ると、頭と手の推定結果に少し誤差がありましたが、正しく計算できていることが確認できました。

  • 床の距離(400cm)
depth[100][280:320]

出力

array([400.        , 400.        , 400.        , 400.        ,
       400.        , 400.        , 398.44357977, 400.        ,
       400.        , 400.        , 400.        , 400.        ,
       400.        , 400.        , 400.        , 398.44357977,
       400.        , 400.        , 400.        , 400.        ])
  • 頭までの距離(約220cm)
depth[240][300:320]

出力

array([  0.        ,   0.        ,   0.        ,   0.        ,
         0.        ,   0.        ,   0.        , 231.15124153,
       230.63063063, 230.63063063, 229.59641256, 229.08277405,
       228.57142857, 227.55555556, 227.55555556, 225.05494505,
       224.56140351, 223.09368192, 222.12581345, 221.64502165])
  • 手までの距離(約250cm)
depth[230][400:410]

出力

array([250.98039216, 251.5970516 , 252.21674877, 252.83950617,
       252.83950617, 253.46534653, 253.46534653, 253.46534653,
       252.83950617, 252.83950617])

まとめ

今回は、StereoBMの出力からdepth(距離)を計算する方法について、コードを交えながらまとめました。また、Unreal Engine(UnrealCV)を利用して正解データ(ステレオ画像)を作成し、動作を確認しました。

StereoBM自体の解説ページはあるものの、出力をどう扱うかについては、解説されているページがなかったので、調べるのにすこし苦労しました。みなさんのご参考になれば幸いです。

参考にさせていただいたページ

ステレオカメラ関連の関数(StereoSGBM)について|teratail

OpenCV: cv::StereoMatcher Class Reference

おまけ

ステレオ画像を実際のカメラで撮影しようと思うと、2つのカメラをきっちり合わせて配置しないと計算結果が正しくならず、動作確認が難しくなりますが、今回のようにCGでやると簡単に正しいデータを得られるので楽で良いなと思いました。今回はUnreal Engineを利用してRGB画像のみを取得してしましたが、depth画像も取得できるようです。

処理時間がそこそこかかるので、高フレームレートで処理しようとすると、それなりの性能のボードが必要になりそうです。もともとの動機は、RealSenseからWebカメラにしてコストを抑えたいということだったのですが、そこまで安くならないかもしれません。

追記

(8/6 この部分の記載内容は削除いたしました。)