概要
カメラキャリブレーションについて調べていると、ソースコード有りのサイトは結構あるのですが、とっ散らかったコードが多く、何がどう効くかわかりづらかったので、まとめました。
私は元々画像処理系ではないので大きなことは言えませんが、とにかくキャリブレーションしたい人向けにまとめたいと思います。
主に以下の二つについて述べます。
- カメラキャリブレーション
- カメラパラメータおよび歪みパラメータの利用
フォルダ構成
\root
│ Calib.py // 実行ファイル
│
└─Image //チェスボード画像ファイルを格納するフォルダ
│ image1.jpg //画像ファイル
│ ...
└─dist // コーナー検出状態確認用フォルダ
カメラキャリブレーション
以下のソースコードを示します。Imageフォルダに画像ファイルを格納し、実行すると、Calib.pyと同一フォルダにキャリブレーション結果(mtx.csvおよびdist.csv)が出力されます。途中、評価値が表示されます(total errorというところ)が、0.05以下の値であればひとまず良いです。
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import os
from pathlib import Path
import logging
logging.basicConfig(level=logging.DEBUG)
class Calbration():
def __init__(self, imagePath="", cols=2, rows=2, squareSize=1.0):
# log
self.log = logging.getLogger("Calbration")
# self.log.addHandler(logging.StreamHandler())
# self.log.setLevel(logging.DEBUG)
self.formats = ["*.jpg", "*.png"] #画像フォーマット
self.image = [] # 画像バッファ
self.imageSize = None # 画像サイズ
self.imagePath = Path(imagePath)# 画像ファイルパス
self.setConfig(cols, rows, squareSize)# 列×行
self.log.debug("initial Calb..")
def setConfig(self, cols, rows, squareSize):
self.log.debug("setConfig")
self.patternSize = (cols, rows)# 画像サイズ
self.patternPoints = np.zeros( (np.prod(self.patternSize), 3), np.float32 ) #チェスボード(X,Y,Z)座標の指定 (Z=0)
self.patternPoints[:,:2] = np.indices(self.patternSize).T.reshape(-1, 2)
self.patternPoints *= squareSize # 正方形の1辺のサイズ[cm]を設定
self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 50, 0.0001)# 高精度化する際の閾値。cornerSubPixする際に何回やるか、目標精度を設定
# image read from iamge folder
def read(self):
if( os.path.exists(self.imagePath) ):
for fmt in self.formats:
for path in self.imagePath.glob(fmt):
img = cv2.imread(str(path)) # 画像読込み
self.image.append([ path, img])
if( len(self.image) > 0 ):
self.log.debug("find image..." + str(len(self.image)))
return True
else:
# error
self.log.debug("Don't exist image file.")
return False
else:
# error
self.log.debug("Don't exist folder.")
return False
def calbration(self):
if( not self.read()):# read image
# error
return False
self.log.debug("corner finding start")
imgPoints = []# corner buff
objPoints = []# obj buff
count = 0
for img in self.image:
self.log.debug(str(img[0]) + " find...")
gray = cv2.cvtColor(img[1], cv2.COLOR_BGR2GRAY)
if(self.imageSize is None):
self.imageSize = gray.shape[::-1]
ret, corners = cv2.findChessboardCorners(gray, self.patternSize, None)
if(ret):
self.log.debug("detected corner")
corners = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1), self.criteria) #コーナー位置精度補正。(11,11)のところは高精度化する際の探索窓の大きさ?
imgPoints.append(corners)
objPoints.append(self.patternPoints)
# debug draw
distImg = cv2.drawChessboardCorners(img[1], self.patternSize, corners, ret)
cv2.imwrite( str(self.imagePath) + "/dist/dist_" + str(img[0]).replace( str(self.imagePath) + "\\", ""), distImg)
count += 1
else:
os.remove(str(img[0]))
self.log.debug("don't detected corner")
self.log.debug("detected image len is..." + str(count))
if(len(imgPoints) > 0):
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objPoints, imgPoints, self.imageSize, None, None)
np.savetxt("mtx.csv", mtx, delimiter=",", fmt="%0.14f") # カメラ行列
np.savetxt("dist.csv", dist, delimiter=",", fmt="%0.14f") # 歪みパラメータ
# 計算結果を表示
print("RMS:", ret)
print("mtx:", mtx)
print("dist:", dist)
# 再投影誤差による評価
mean_error = 0
for i in range(len(objPoints)):
image_points2, _ = cv2.projectPoints(objPoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgPoints[i], image_points2, cv2.NORM_L2) / len(image_points2)
mean_error += error
print ("total error: ", mean_error/len(objPoints)) # 0に近いほど良い
return True
else:
self.log.debug("all image don't exist corner")
return False
if __name__ == "__main__":
path = "Image" # チェスボード画像ファイルが格納されているフォルダ
# チェスボードの交点
rows = 10
cols = 7
size = 2.4 # チェスボードの辺の長さ
calb = Calbration(path, cols=cols, rows=rows, squareSize=size)
calb.calbration()
カメラパラメータおよび歪みパラメータの利用
以下が出力されたパラメータの利用例です。上のプログラムを改造してnumpy配列を出力してもいいのですが、今回は出力したcsvファイルを読み込む形にしました。
import cv2
import numpy as np
class Correction():
def __init__(self, mtx, dist, formatType=None):
if ( formatType == "csv" ):
self.mtx = np.loadtxt(mtx, delimiter=",")
self.dist= np.loadtxt(dist, delimiter=",")
else:
self.mtx = mtx
self.dist= dist
def __call__(self, image):
cv2.CALIB_FIX_PRINCIPAL_POINT
# print(image.shape)
h, w = image.shape[:2]
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(self.mtx, self.dist, (w, h), 1, (w, h))
# print(roi)
# undistort
dst = cv2.undistort(image, self.mtx, self.dist, None, newcameramtx)
# crop the image
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
return dst
if __name__ == "__main__":
corr = Correction("mtx.csv", "dist.csv", formatType="csv")
img = corr(cv2.imread("入力する画像ファイル名"))
cv2.imwrite( "出力ファイル名", img)
トラブルシューティング
roiが(0,0,0,0)となる
これの日本語の資料が少ないんですよね。理屈が分かればその通りなんかもしれませんが、あくまで理屈を最小限に説明します。
海外のQ&Aでは、「チェスボードをもっと上手く撮れ」的なことを言っています。簡単に言うと「チェスボードをもっと近くで大きく、重複しないパターンで撮れ」ということだと私は解釈しています。重複しないというところもとても重要で、重複しても(0,0,0,0)が出ることがあります。