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

2020.07.31

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

現在、カフェではコンピュータビジョン用のカメラとして、Intel製のRealSense Depth Camera D435Iというデバイスを利用し、RGB画像とDepth画像を取得しています。RealSenseは精度や性能が高く、コストパフォーマンスが高いとても良い製品なのですが、値段としては数万円程度するものです。通常のカメラで同様のことができれば、数千円程度で済ませられ、大幅にコストを削減できるかもしれません。

今回は、通常のカメラでDepth画像を取得できないか検討するために、ステレオ画像からDepthを推定する方法(Stereo Depth Estimation)を試します。利用したのは、OpenCVのStereoBMという機能です。

お詫び

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

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

Stereo Depth Estimationとは

ステレオ画像とは、2台のカメラをすこし左右にずらして同時に撮影した、2枚の画像のセットのことです。Stereo Depth Estimationは、ステレオ画像から、画像中の物体のDepthを推定する方法です。近い物体ほど、2つの画像上では大きくズレて映るため、対応する点がわかれば、その奥行きがわかるという仕組みです。数式などの詳しい内容は以下のリンクで解説されています。

OpenCV: Depth Map from Stereo Images

数式の部分が少しわかりにくいので、補足として説明します。

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

まず、各変数の定義ですが、x・x'は、それぞれのスクリーン端からの距離を表し、画面上のピクセル数を表しているわけではありません。B・f・Zも同様に距離を表していますです。

数式は図のまま計算していけばわかるはわかるのですが、以下の図のように、左カメラの図を右にBだけずらして、右カメラに重ねるとわかりやすいです。d = x-x' = B*f/Z になっているのが、すぐに分かると思います。

上のリンク先では、ステレオ画像を入力すると下の画像のような結果が得られる、と書かれています。

実装・動作確認

Jupyter(Python)で実装しました。コードをまとめると、以下のようです。ステレオ画像(000000_10_L.png、000000_10_R.png)は、KITTI2012というデータセットに含まれている画像(元のファイル名は000000_10.pngという画像ファイルで、2つのディレクトリに分かれて入っています)です。

8/3修正:KITTI2012ではなく、KITTI Stereo Evaluation 2015というデータセットのものでした。ご迷惑をおかけしました。dandelionさんからご指摘いただきました。

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

# read images
imgL = cv2.imread('000000_10_L.png',0)
imgR = cv2.imread('000000_10_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()

# (confirm difference between two images)
plt.figure(figsize=(13,3))
plt.imshow((imgR+imgL)/2)
plt.show()

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

# show depth estimation result
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.show()

# (show with colorbar)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

実行すると、以下の様な画像が出力されます。

画像内の奥側のDepthは正しく推定できていそうですが、手前側は推定ができなかったようです。また、特徴的な点がある領域のDepthを推定はできていますが、一色だったり変化がない領域は推定できないようです。

パラメータ調整

先程のOpenCVのページを見てみると、StereoBMのパラメータを調整する必要がある、と書かれています。コードでいうと、以下の箇所です。

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

numDisparities

先程のOpenCVのページには、mnumDisparitiesは、Depthを推定する際に画像をスライドさせるピクセル数を表す、と書かれています。StereoBMでは、numDisparitiesによってスライドさせて、その近辺で特徴点同士のマッチング処理をかけるようです。そのため、物体が手間に写っていて、numDisparitiesよりも画面上のピクセルのズレ幅が大きいと、特徴点同士マッチングがされなくなり、Depth推定ができないようです。

numDisparitiesを大きくしながら実行すると、結果は下図のようでした。だんだんと手前の部分も奥行きを推定するようになっていることがわかります。

図中右のカラーバーが示すように、色が表す値のレンジが変わっていることにご注意ください。画面内奥側の色が変わっていますが、値としては変わっていないようです。

stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=32, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=64, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=128, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=192, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

numDisparitiesが大きいほど、手前側の物体までDepth推定ができるという利点がありますが、計算量が多くなり処理速度が下がる、画面左のDepth推定できない面積が大きくなるという欠点があります。なので、予め推定したいDepthから、画面上のピクセル数のズレ幅を計算して設定するのが良いようです。

blockSize

こちらの解説ページを見ると、blockSizeはアルゴリズムによって比較されるブロックのサイズを表す、と書かれていますが、何を基準に設定したら良いかあまり書かれていません。

とりあえず処理の様子を見てみるために、コードを動かしてみます。

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=5)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=9)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=21)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

stereo = cv2.StereoBM_create(numDisparities=96, blockSize=31)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(13,3))
plt.imshow(disparity)
plt.colorbar()
plt.show()

blockSizeが小さいと、細かい領域ごとにDepthを推定されます。

値を大きくしていくと、まとまった領域でDepthが推定されます。しかし、道路の真ん中など小さくDepthを推定できていた箇所が潰れてしまいます。9、15辺りが良さそうな値ですが、予め設定するための計算方法などはわからず、目で見て調べるのがよさそうでした。

まとめ

OpenCVのStereoBMという機能を利用して、ステレオ画像から奥行きを推定するコードを動かしてみました。

予想と違って、全ての画素のDepthを推定できるわけではなく、パラメータ調整も必要であることがわかりました。利用するには、パラメータの設定や後処理にすこし工夫が必要な印象を受けました。

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

OpenCV: Depth Map from Stereo Images

class cv::StereoBM

補足

正直なところ、かなり前の手法を使っています。本当は、下のような新しい論文のコードを動かして結果を書きたいと思っていたのですが、動かして処理結果を取り出すまでになかなか至らず、OpenCVを動かしてみることにしました。もし、このようなコードを動かし方をご存知でしたら、コメントなどでアドバイスをいただけると嬉しいです。

mileyan/AnyNet

zhixuanli/StereoNet

JiaRenChang/PSMNet

NVIDIA-AI-IOT/redtail