カニカニクラブライフ

Python系技術メモ、書評とか

python+opencvでカメラキャリブレーション→ステレオマッチング→奥行き計測をする方法

今回でほぼパーペキにわかったのでまとめ

環境:python 2.7.6 OpenCV2.4.11

 
キャリブレーションをせずともステレオマッチングと、視差マップをもとにした奥行き計測はできる↓

 

russeng.hatenablog.jp

 

 


①事前
・左右カメラで同一のチェスボードを撮影した画像を数10枚程度(最低10枚くらい、確か)用意する(Zhangの方法)
・↑と同様のカメラの位置関係で撮影したステレオマッチング用の画像(奥行計測したい画像)を用意する


②cv2.findChessboardCorners(image, patternSize[, corners[, flags]]) → retval, corners
「チェスボードの交点を見つける」
・cornersには交点の画像内ピクセル座標が戻り値として与えられる
・撮影したキャリブレーション用画像すべてに対して行う
・cornersは、左右カメラ独立に取り出せるよう処理しておくのがよい


③cv2.calibrateCamera(objectPoints, imagePoints, imageSize[, cameraMatrix[, distCoeffs[, rvecs[, tvecs[,
flags[, criteria]]]]]]) → retval, cameraMatrix, distCoeffs, rvecs, tvecs
「カメラのキャリブレーション(カメラ行列と歪み係数を求める)」
・objectPointsには、キャリブレーションパターン内の座標値(パターン内の平面座標したがってz=0)
・imagePointsには、パターンを撮影した画像内の交点座標(先のcorners)を与える
・チェスボードはカメラに向かって水平にしなくてもよい
・1個のカメラのみにおける外部パラメータ(回転行列・並進ベクトル)も算出できるが、とりあえず使わない
・この関数を左右のカメラそれぞれにおいて独立に適用する


④cv2.stereoCalibrate(objectPoints, imagePoints1, imagePoints2, imageSize[, cameraMatrix1[, distCoeffs1[,
cameraMatrix2[, distCoeffs2[, R[, T[, E[, F[, criteria[, flags]]]]]]]]]]) → retval, cameraMatrix1,
distCoeffs1, cameraMatrix2, distCoeffs2, R, T, E, F
「撮影システムのキャリブレーション(左右のカメラを含めた外部パラメータを求める)」
・cameraMatrix1(,2)とdistCoeffs1(,2)には、先で求めたカメラ行列と歪み係数を入れる
・同様にcv2.findChessboardCornersで求めたピクセル座標と、パターン内の交点座標を入力する
・RとTは「左右のカメラ間の」回転行列と並進ベクトルであり、1で入力した方のカメラのカメラ座標系をそのままワールド座標系と考えて計算している
・カメラ行列、歪み係数も再度最適化(誤差を最小化するよう補正)される。
 つまり入力する必要は必ずしもないが、安定した解を素早く求めるために、先のcv2.calibrateCameraで求めておくことが推奨される。
    この"二度手間"の最適化を行わない条件で関数を動かすこともできる(flagsで制御)


⑤cv2.stereoRectify(cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize, R, T[, R1[, R2[, P1[,
P2[, Q[, flags[, alpha[, newImageSize]]]]]]]]) → R1, R2, P1, P2, Q, validPixROI1, validPixRPI2
「それぞれのカメラの平行化のための回転行列・射影行列およびQを求める」
・前述のRとTを入力
・Qは視差-奥行のマッピング行列(ステレオマッチング後の3D座標の取得に必要)
・上記の3つの関数はこのドキュメント*1にリファレンスが載っている。以下に紹介する関数も特記しない限り載っている。この関数はなぜかpython2系の情報について書かれてないが、存在はする。


そもそも平行化とは?
ステレオマッチングとは、2つの画像間の対応点を探索し、対応点どうしのピクセル距離(視差)を求める作業である。その目的は、次式により、画像の取得過程(3次元→2次元)で失われた奥行情報を計算することにある。
    z=bf/(u-u')
ところでこの式は、以下のワールド座標-画像内座標の変換式を、左右のカメラが画像のx軸に平行であり、y方向・Z方向の位置は同一である(=平行ステレオ)と単純に仮定した場合に得られる変換式である。この仮定は、回転行列と並進ベクトルを用いて以下のように定義する。

しかし実際には、カメラのわずかな回転などにより、完全に平行にカメラを配置できるわけではない。
したがって平行化の操作とは、まるで"上記の仮定が成立する場合に得られた画像である"かのように、実際の画像を修正する作業である。
平行化の作業は、エピポーラ線を求めやすくもする、つまりステレオマッチングにおける対応点探索において解を求めやすくなる(書いてて思ったが、そっちの方の目的がメインじゃないのか?)


⑥cv2.initUndistortRectifyMap(cameraMatrix, distCoeffs, R, newCameraMatrix, size, m1type[, map1[, map2]]) →
map1, map2
「平行化と歪み補正を同時に行う"マップ"を求める」
・マップとは、OpenCVにおける画像の幾何変換のための位置指定情報を乗せた行列(関数Remapで使用する)
・左右のカメラ画像についてそれぞれ行う
・関数Rectifyで求めたRをRに、P1(P2)の左側の3×3行列をnewCameraMatrixに入力する
・sizeには歪み補正後の画像サイズを入力する
・cameraMatrixとdistCoeffsにはstereoCalibrate関数で求めたものを入力する


⑦cv2.remap(src, map1, map2, interpolation[, dst[, borderMode[, borderValue]]]) → dst
「マップを用いて画像を変換」
・initUndistort関数で求めたmap1,map2を入力する
・interpolationは補間方法の指定
・srcには変換したい元画像を入力し、distには変換後の画像が出力される
 (ここではステレオマッチングを行う用のペア画像を変換する)


マッチング関数(StereoBM関数、StereoSGBM関数等)を用い、remapで変換した画像に対しステレオマッチング
 →視差画像の取得


⑨cv2.reprojectImageTo3D(disparity, Q[, _3dImage[, handleMissingValues[, ddepth]]]) → _3dImage
「画像から3次元座標を得る」
・disparityにはマッチング関数で得た視差画像を、QにはstereoRectify関数で求めたものを入力する
・_3dImageは、dispartyと同じサイズでかつ3チャンネル(3次元座標)の構造体が入力されている


 
おわり
・CやC++でもほぼ同様の名前の関数を適用する(たぶん)
・コードはいずれまた
 

python+OpenCVでステレオマッチングするんだけど、キャリブレーション結果どうやって反映させるねん

python+OpenCVでステレオマッチング(左右カメラ画像の対応点探索)して、対象の3次元座標を計測する、というのを一応ゴールにやってきたんだけど

本家pythonチュートリアルDepth Map from Stereo Images — OpenCV-Python Tutorials 1 documentationは、今までの話(キャリブレーション、姿勢推定…云々)をすっとばして、いきなり「奥行マップをつくろう(唐突)」となるわけです。


視差disparity = B f / Z
となるわけだから、
奥行きZは = B f / disparityとなり、求められるのはわかる。

じゃあ基線長Bってどんな値なのか?単純にカメラ間の距離を定規ではかるだけでいいのだろうか?
そしてこの式が使えるのは平行ステレオの場合のみのはず(教科書((Amazon.co.jp: ディジタル画像処理: 本)))



今のところ僕の理解ではこんな流れのはず。
・内部パラメータをそれぞれのカメラごとに求める
・外部パラメータを左右のカメラ同時に求める
 ・solvePnP関数なるものによって、それぞれのカメラに対しての外部パラメータを求める方法は記載してある。これをどう合わせるのか目下謎。
 ・同時に求める(2枚の画像を読み込ませ)関数も存在するはず・・・。どこかに。
・わかった内部パラメータ、外部パラメータから、三次元座標への変換パラメータを導出する。
(あるいは、内部パラメーター、外部パラメーターを利用して歪み補正や平行化を行い、平行ステレオと同じ条件で得られた理想画像(っぽいの)に補正する)


よくわかんねーなーと思って調べてたら奥行きマップから3次元モデルを生成という記事があった。python-gazo.blog.jp


最終的にやりたいゴールをこれなので、とりあえずコードをパクリ、色々動かして見ながら調べてみることにした。コードはこれ、例によってmain関数は外す。視差画像を求めるまで。

# -*- coding: utf-8 -*-

import cv2

# 画像取得
gray_l = cv2.imread("left.png",0)
gray_r = cv2.imread("right.png",0)
# 画像のヒストグラム平坦化・平滑化
gray_l = cv2.GaussianBlur( cv2.equalizeHist(gray_l),(5,5), 0)
gray_r = cv2.GaussianBlur( cv2.equalizeHist(gray_r),(5,5), 0)
# BM法でステレオ対応点探索
stereo = cv2.StereoBM(cv2.STEREO_BM_BASIC_PRESET,ndisparities=32, SADWindowSize=21)
disp = stereo.compute(gray_l,gray_r)    # 視差を計算
# 視差データを8bitデータに変換(imshowで表示させるため)
disp = cv2.normalize(disp, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
cv2.imshow("disp",disp)                 # 視差画像の表示
cv2.waitKey(0)
cv2.destroyAllWindows()


パクリ元python-gazo.blog.jp

・とりあえず動く。
ヒストグラム平坦化をなぜやるのかは不明。

Zhangの方法でチェスボード内の正方形のサイズを指定してどうなるの?

※注※ この記事での疑問の答えは出ていませんので読むのは時間の無駄です



russeng.hatenablog.jp


で、チェスボードの正方形のサイズを設定できる(square_sizeという変数がそれ)。


これに関して、tutorial*1では、

But if we know the square size, (say 30 mm), and we can pass the values as (0,0),(30,0),(60,0),..., we get the results in mm. (In this case, we don’t know square size since we didn’t take those images, so we pass in terms of square size).

とある。正方形サイズがわかっているとき、これをある単位で与えてやれば、その単位で結果を受け取ることができるよ、と書いてあるのだと思う。

実際に入力してみた。

  1. square_size = 1 のとき

f:id:russENG:20150616004511p:plain

  1. square_size = 23.0 のとき (実際に私がつかっているチェスボードの正方形サイズ(mm))

f:id:russENG:20150616234016p:plain


同じキャプチャじゃないよ!



とりあえず内部パラメータに違いは出ない(考えてみれば当たり前か)
外部パラメータや、奥行距離等に出てくるのかもれない。

Zhangのカメラキャリブレーションで、きちんとチェスボードの交点を検出できているか確かめるコード

russeng.hatenablog.jp


でパクったキャリブレーションのコードですが、


OpenCV を利用した非接触 3 次元座標測定技術に関する研究

この報告によれば、チェスボードの角度がカメラの視線方向に対して斜め15度を超えると、

"検出した交点座標が画像上の交点位置と異なる"

"全ての交点座標が検出できない"             などの問題が発生するそう。


そこで、

  • 目視により検出したポイントを表示
  • ボタンにより画像を使用するか否かを人の目で判別する   という機能をつけます。
上記コードにつけたし
# -*- coding: utf-8 -*-

import numpy
import cv2
from glob import glob
import Tkinter
import tkMessageBox


square_size = 23.0      # 正方形のサイズ
pattern_size = (10, 7)  # 模様のサイズ
pattern_points = numpy.zeros( (numpy.prod(pattern_size), 3), numpy.float32 ) #チェスボード(X,Y,Z)座標の指定 (Z=0)
pattern_points[:,:2] = numpy.indices(pattern_size).T.reshape(-1, 2)
pattern_points *= square_size
obj_points = []
img_points = []
 
for fn in glob("*.jpg"):
    # 画像の取得
    im = cv2.imread(fn, 0)
    print "loading..." + fn
    # チェスボードのコーナーを検出
    found, corner = cv2.findChessboardCorners(im, pattern_size)
    # コーナーがあれば
    if found:
        term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1)
        cv2.cornerSubPix(im, corner, (5,5), (-1,-1), term)    #よくわからないがサブピクセル処理(小数点以下のピクセル単位まで精度を求める)
        cv2.drawChessboardCorners(im, pattern_size, corner,found)
        cv2.imshow('found corners in' + fn,im)
    # コーナーがない場合のエラー処理
    if not found:
        print 'chessboard not found'
        continue
    # 選択ボタンを表示
    root = Tkinter.Tk()
    root.withdraw()
    if tkMessageBox.askyesno('askyesno','この画像の値を採用しますか?'):
         img_points.append(corner.reshape(-1, 2))   #appendメソッド:リストの最後に因数のオブジェクトを追加 #corner.reshape(-1, 2) : 検出したコーナーの画像内座標値(x, y)
         obj_points.append(pattern_points)
         print 'found corners in ' + fn + ' is adopted'
    else:
         print 'found corners in ' + fn + ' is not adopted'        
    cv2.destroyAllWindows()
    
    
# 内部パラメータを計算
rms, K, d, r, t = cv2.calibrateCamera(obj_points,img_points,(im.shape[1],im.shape[0]))
# 計算結果を表示
print "RMS = ", rms
print "K = \n", K
print "d = ", d.ravel()
# 計算結果を保存
numpy.savetxt("K.csv", K, delimiter =',',fmt="%0.14f") #カメラ行列の保存
numpy.savetxt("d.csv", d, delimiter =',',fmt="%0.14f") #歪み係数の保存

コーナーの描画には、opencv出来合いの関数を使用*1

めんどくさいので、main関数にする表記も外しました*2



結果はこんな。

f:id:russENG:20150620120548p:plain

*1:tutorialにあるように代入表記すると僕の環境(opencv2系)だと戻り値はNoneなのでエラ―を吐きます。

*2:main関数にするメリット、いまだによくわからない

pythonとOpenCVでカメラキャリブレーション(1個のカメラの内部パラメータと歪み係数を求める)するコード(パクリ)

パクりました。

# -*- coding: utf-8 -*-

import numpy
import cv2
from glob import glob
 
def main():
 
    square_size = 1.0      # 正方形のサイズ
    pattern_size = (10, 7)  # 模様のサイズ
    pattern_points = numpy.zeros( (numpy.prod(pattern_size), 3), numpy.float32 ) #チェスボード(X,Y,Z)座標の指定 (Z=0)
    pattern_points[:,:2] = numpy.indices(pattern_size).T.reshape(-1, 2)
    pattern_points *= square_size
    obj_points = []
    img_points = []
 
    for fn in glob("*.jpg"):
        # 画像の取得
        im = cv2.imread(fn, 0)
        print "loading..." + fn
        # チェスボードのコーナーを検出
        found, corner = cv2.findChessboardCorners(im, pattern_size)
        # コーナーがあれば
        if found:
            term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1)
            cv2.cornerSubPix(im, corner, (5,5), (-1,-1), term)
        # コーナーがない場合のエラー処理
        if not found:
            print 'chessboard not found'
            continue
        img_points.append(corner.reshape(-1, 2))   #appendメソッド:リストの最後に因数のオブジェクトを追加
        obj_points.append(pattern_points)
        #corner.reshape(-1, 2) : 検出したコーナーの画像内座標値(x, y)
 
    # 内部パラメータを計算
    rms, K, d, r, t = cv2.calibrateCamera(obj_points,img_points,(im.shape[1],im.shape[0]))
    # 計算結果を表示
    print "RMS = ", rms
    print "K = \n", K
    print "d = ", d.ravel()
    # 計算結果を保存
    numpy.savetxt("rms.csv", rms, delimiter =',',fmt="%0.14f")
    numpy.savetxt("K.csv", K, delimiter =',',fmt="%0.14f")
 
if __name__ == '__main__':
    main()


ついでにコードに要らないコメントを書き込んで見にくくしてやりました*1

画像取得は、中で何やってるかよくわからんですが、ソースがあるフォルダ内の画像をfor文で
順番に呼んでくれる感じです。
f:id:russENG:20150616004218p:plain

結果はこうなりました

f:id:russENG:20150616004511p:plain



うん、ちゃんと出来てるのかすらよくわからんな。

これを使って歪み補正します ⇒







以下からソースを拝借しました。python-gazo.blog.jp

*1:ゲヘー

カメラの内部パラメータと歪み係数を求めるキャリブレーションって、チェスボードを視線に水平にしなくちゃダメなんじゃないの?

 

実験して確かめる。

 

何の話?

ステレオ視の話。

・カメラキャリブレーションにより、内部パラメータ・歪み係数と、外部パラメータを求める必要がある。 (過去ブログ記事参照)

・これにはよくZhangの方法と呼ばれる、チェスボードを撮影し、マスの交点を自動探索させる方法が用いられる。

・内部パラメータ・歪み係数は、左右のカメラに対し、それぞれ個別に求める必要がある。

・カメラ位置と対象物の関係、つまり奥行きに関する直接的な情報を決めるのは、2つのステレオカメラの位置についての外部パラメータである。 

・tutorial*1でも、内部パラメータ・歪み係数を求める際には、zは一定で入力する、という記述があるし、実際のコードも、z=0を入力している。

What about the 3D points from real world space? Those images are taken from a static camera and chess boards are placed at different locations and orientations. So we need to know (X,Y,Z) values. But for simplicity, we can say chess board was kept stationary at XY plane, (so Z=0 always) and camera was moved accordingly. This consideration helps us to find only X,Y values.

・実際、内部パラメータと歪み係数を得てできる画像への操作は、レンズ方向の歪みの補正である。

 

疑問 

では、奥行き情報はひとつのカメラではわからないのだから、チェスボード内のすべての点で奥行距離を一致させなくてはいけないのでは?

→つまり、カメラに対して水平にチェスボードを設置させなくてはいけないのじゃないの?

 

・しかしながら既存のいろんな資料*2で、チェスボードを「いろんな角度で」撮影することとしている。

・そして元のZhangの論文は英語なので読む気にならない。

・そうは言っても、チェスボードのマスの大きさは与えているわけだし、マスが小さく映っているか、大きく映っているかで、カメラからの距離と、チェスボードの傾きはわかっているのじゃないの?

 

よくわからないので実験します。

方法:「チェスボードをわざと傾ける」グループと、「チェスボードまあまあ水平にした」グループで、各パラメータや補正結果に違いが出るのか。

 

●準備作業

 ・ステレオカメラ、つまり2台のカメラを用意する*3

 ・2台のカメラをなんらかの方法で固定した計測システムをつくる

 ・カメラの自動明るさ調整などのスイッチを切る*4

 ・カメラを2台接続し、同時に映像を記録するためのコードを用意する*5

 

●手順1 : 内部パラメータと歪み係数を求めて比較する

 ・キャリブレーションするコードはこんな感じ

russeng.hatenablog.jp

 

・「斜めにチェスボードを撮影した場合、交点が正確に検出できない場合がある*6」現象が起こるらしいので、目視で確認するコードを作ったりする。

 

●手順2 : 外部パラメータを求めて比較する

 ・まだやってません。

 

結果