< 07-1. Camera Calibration >
이번 장에서는
- 카메라상에서 왜곡, 카메라의 내재/외적 파라미터 등
- 이러한 파라미터들과 왜곡되지않은 이미지등을 찾아볼 것이다.
Basics
오늘날의 값싼 핀홀 카메라는 이미지에 많은 왜곡을 불러온다. 두 가지 주요한 왜곡은 방사형 왜곡(radial distortion)과 접선형 왜곡(tangential distortion)이다.
방사형 왜곡은 직선을 곡선처럼 보이게 한다. 이미지를 화면 중앙에서 멀어지게 한다면 이는 더 많은 영향을 미치게 된다. 예를 들어서, 아래의 이미지를 보면, 체스 보드의 양 끝 가장자리를 빨간 선으로 표시를 해두었다. 하지만 가장자리가 직선이 아니고, 빨간 선과도 일치하지 않는 것을 볼 수 있다. 예측된 직선들은 돌출되어있다. 왜곡(distortion) 에 자세한 설명이 더 있다.
방사형 왜곡은 다음과 같이 해결될 수 있다.
비슷하게, 다른 왜곡인 접선형 왜곡은 렌즈가 잡아낸 이미지가 이미지 평면과 정확하게 평행하게 정렬되지 않아서 발생하게 된다. 그래서 이미지의 어떤 부분은 예상보다 더 가깝게 보이게 된다. 이는 아래의 실으로 해결된다.
요약하자면, 우리는 왜곡 계쑤라고 알려진 이 5개의 파라미터를 찾아야한다.
추가적으로, 카메라의 내재 / 외적 파라미터들과 같은 정보를 더 찾아내야 한다. 내재 파라미터는 카메라마다 다르다. 이는 초점 길이
또한 이는 “카메라 매트릭스”라고도 불린다. 이는 카메라에만 의존하며, 그래서 한 번 계산되면, 추후 목적을 위해 저장될 수 있다. 이는 3X3 매트릭스로 표현된다.
외적 파라미터는 회전(rotation)과 변환(translation) 벡터에 해당하며, 이는 3D 점의 좌표를 좌표계로 변환한다.
스테레오에 적용시, 이 왜곡은 먼저 교정되어야 한다. 이 모든 파라미터들을 찾기 위해, 잘 정의된(일정한) 패턴(체스판 같은)을 가진 표본 이미지를 제공해야한다. 우리는 이미지에서 특정한 점들을 찾는다.(체스판의 사각 모서리) 우리는 실제 공간에서의 좌표를 알고, 이미지상에서의 좌표도 알고 있다. 이러한 데이터들로, 왜곡 계수를 얻기위해 몇 가지 수학적인 문제가 뒤에서 해결된다. 더 나은 결과를 위해, 최소 10개의 테스트 패턴 이미지가 필요하다. (체스판의 경우 다른 각도에서 찍은 여러 장이 필요하다는 것)
Code
위에서 말한 것 처럼, camera calibration을 하기 위해 최소 10개의 테스트 패턴을 가진 이미지를 필요로 한다. OpenCV github에서 데이터를 받을 수 있다. (left*.jpg) 이해를 위해서 체스보드 이미지 하나만을 생각해보자. camera calibration을 위해 필요한 중요한 입력 데이터는 3D 실제 포인트의 집합과 이와 일치하는 2D 이미지 포인트들이다. 2D이미지 포인트는 이미지에서 찾기 쉬워서 괜찮다. (이 이미지 포인트는 두 개의 검정 사각형이 체스판에서 서로 접혹하는 위치이다.)
실제 세상의 3D 점에 대해서는 어떨까? 이 이미지들은 정적인 카메라와 매번 다른 방향과 위치에 놓인 체스판으로부터 구성된다. 그래서 우리는
3D 포인트들은 “Object Point” 라고 불리고, 2D 이미지 포인트들은 “Image Points” 라고 불린다.
Setup
체스판의 패턴을 찾기 위해 cv2.findChessboardCorners() 라는 함수를 사용한다. 또한 어떤 종류의 패턴(8x8, 5x5 격자. 이 예에서는 7x6 격자를 사용한다.)을 찾으려고 하는지도 입력해야한다.(보통 체스판은 8x8 사각형과 7x7 내부 모서리로 이루어져 있다.) 이 함수는 코너 포인트들과 패턴을 보유하고 있으면 True값을 가지는 retval를 리턴한다. 이러한 코너들은 순서대로 위치하게 된다. (좌-우, 상-하)
- 체스판 대신에 원형의 격자를 사용할 수 있지만, 이 때는 cv2.findCirclesGrid()를 사용하여 패턴을 찾아야한다.
코너를 찾고, cv2.cornerSibPix()를 사용해서 정확도를 증가시킬 수 있다. 또한 cv2.drawChessboardCorners()를 사용함으로써 패턴을 그릴 수 있다. 이 모든 과정은 아래의 코드에 담겨있다!!
< termination criteria를 만드는 조건을 알아보고 가자! >
cv2.TERM_CRITERIA_EPS - 정해둔 정확도에 다다르면 알고리즘 반복을 멈춘다
cv2.TERM_CRITERIA_MAX_ITER - 지정한 반복 수만큼을 지나면 알고리즘을 멈춘다.
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER - 위의 조건 중 하나라도 만족하면 알고리즘을 멈춘다.
criteria = (a,b,c) 형태로 쓴다면, a가 위의 방식이고 b는 iterations 수, c는 요구되는 정확도(epsilon)이다!
import numpy as np
import glob, cv2
# 종료 기준(termination criteria)를 정한다.
criteria = (cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER, 30 ,0.001)
# Object Point(3D)를 준비한다. (0,0,0),(1,0,0),(2,0,0)... 처럼
objp = np.zeros((6*7,3),np.float32)
# np,mgrid[0:7,0:6]으로 (2,7,6) 배열 생성
# Transpose 해줘서 (6,7,2)로, reshpae(-1,2)로 flat 시켜서 (42,2)로 변환
objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)
# 이미지로 부터의 Object point와 Image points를 저장하기 위한 배열
objpoints = [] # 실제 세계의 3D 점들
imgpoints = [] # 2D 이미지의 점들
# 전체 path를 받기 위해 os말고 glob 사용
images = glob.glob('./images/Calib/*.jpg')
for name in images:
img = cv2.imread(name)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 체스판의 코너들 찾기
ret, corners = cv2.findChessboardCorners(gray,(7,6),None)
# 찾았으면, Object points, Image points 추가하기 (이후에 수정한다)
if ret == True:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
imgpoints.append(corners2)
# 코너를 그리고 봐보자
img = cv2.drawChessboardCorners(img,(7,6),corners2,ret)
cv2.imshow('img',img)
cv2.waitKey(2000)
cv2.destroyAllWindows()
Calibration
이제 object points, image points가 생겼으니 calibration을 하기 위한 준비가 됐다. 이제 cv2.calibrateCamera() 함수를 사용한다. 이는 camera matrix, 왜곡 계수, 회전/변환 벡터들을 리턴한다.
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
Undistortion
이제 왜곡을 없앨 수 있다. OpenCV는 두 가지 방법을 제공하는데 둘 다 볼 것이다. 하지만 이전에,cv2.getOptimalNewCameraMatrix()를 사용하여 free scaling 파라미터에 기반하여 카메라 매트릭스를 수정할 수 있다. 맨약 스케일링 파라미터인 alpha=0이라면, 최소한의 요구되지 않은 픽셀의 왜곡되지 않은 이미지를 리턴한다. 만약에 alpha=1이면, 모든 픽셀은 검정 이미지를 보유하게 된다. 또한 결과를 자르는데 사용될 수 있는 ROI도 리턴한다.
새로운 이미지 left12.jpg를 받아보자.
img = cv2.imread('./images/Calib/left12.jpg')
h,w = img.shape[:2]
newcameraMtx, roi = cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))
1. Using cv2.undistort()
쉬운 방법이다. 함수를 부르고 ROI를 사용해서 결과를 자른다.
dst = cv2.undistort(img,mtx,dist,None,newcameraMtx)
x,y,w,h = roi
dst = dst[y:y+h,x:x+w]
cv2.imwrite('calibRes.png',dst)
2. Using remapping
조금 돌아가는 방법. 먼저 왜곡된 이미지로부터 왜곡되지 않은 이미지까지의 매핑 함수를 찾는다. 그리고 remap 함수를 사용한다.
mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameraMtx,(w,h),5)
dst = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)
x,y,w,h = roi
dst = dst[y:y+h,x:x+w]
cv2.imwrite('calibRemap.jpg',dst)
가장자리가 직선이 된 모습을 볼 수 있다.
이제 카메라 매트릭스와 왜곡 계수를 저장해서 추후에 계속 사용할 수 있다.
np.savez('calib.npz',ret=ret,mtx=mtx,dist=dist,rvecs=rvecs,tvecs=tvecs)
np.savez나 np.savetxt로 저장 가능
Re-projection Error
Re-projection 에러는 발견된 파라미터가 얼마나 정확한지 잘 추정한다. 이는 가능한한 0에 가까워져야한다. 내재/왜곡/회전/변환 매트릭스가 있으면, 먼저 cv2.projectPoints()를 사용하여 Object points를 Image points로 변환해야한다. 그리고 우리가 얻은 변형과 코너를 찾는 알고리즘 간의 absolute norm을 계산한다. 평균 에러를 찾기 위해 모든 calibration 이미지에 대해 에러의 산술 평균을 계산한다.
mean_error = 0
for i in range(len(objpoints)):
imgpoints2,_ = cv2.projectPoints(objpoints[i],rvecs[i],tvecs[i],mtx,dist)
error = cv2.norm(imgpoints[i],imgpoints2,cv2.NORM_L2)/len(imgpoints2)
mean_error += error
print("Total error : {0}".format(mean_error/len(objpoints)))
Total error : 0.023686000375385673
이렇게 오차를 구할 수 있다.