[240726] Camera Calibration

FSA·2024년 7월 26일
0

camera

목록 보기
8/11

1. 카메라의 원리

  • 쉽게 이해하고 싶으면: https://www.youtube.com/watch?v=3fjLRebuxBg
  • 위 그림은 피사체에 초점을 잘 맞춰서, 피사체의 한 점이, 이미지센서(필름)의 정확히 한 점에 모이는 경우이다.
    • 초점을 잘 맞춘다?: (카메라 렌즈와 이미지 센서간 거리를 잘 맞춰야 한다.)
    • 카메라 앞부분을 돌리는 행위, 혹은 휴대폰 카메라의 0.5x, 1x, 1.5x 를 조정하는 것은
      • 필름(이미지센서)의 위치를 조정하는 것이라고 이해하면 된다.
  • 볼록렌즈
    • 평행하게 빛이 들어가면, 초점으로 모이는 성질
    • 주점으로 빛이 들어가면, 굴절되지 않는 성질
  • 현대의 카메라는 디지털 방식으로, 아래와 같다.
  • n만 화소: 이미지 센서에 배열된 화소의 개수가 n개이다.

2. intrinsic parameter 이란?

  • 여기서 말하는 초점거리는, 주점이미지센서간 거리를 의미하는 듯하다.
  • 위 그림의 빨간색 (카메라 주점을 원점으로 하는 3D 절대좌표계의 좌표값) (X,Y,Z)를
    • Z로 나누면, (X/Z, Y/Z, 1)이 되고,
    • 이 값은 카메라 unit image plane에 매칭됨
    • 카메라 unit image plane이란, 카메라 주점과의 거리가 1인 image 평면을 의미
  • (X/Z, Y/Z, 1) (카메라 unit image plane)과, 카메라 image plane간의 관계를 수학적으로 나타낸 것이 intrinsic matrix
  • 다른 말로 하면, 공간의 한 점을 (빨간색) 카메라 주점을 원점으로 하는 3D 절대좌표계의 좌표값 표현에서 -> image plane 좌표계로 변환하기 위한 matrix!

3. Camera Distortion?

  • intrinsic parameter만 알면, (빨간색) 카메라 주점을 원점으로 하는 3D 절대좌표계의 좌표값 표현에서 -> image plane 좌표계로 변환이 될까?
    • 정답: 부정확하다.
  • 이유:
    • 카메라의 원리 1에서 말한 것처럼, 볼록렌즈의 문제로 인해,
    • 피사체의 특정한 한 점에서 나온 빛들이, 이미지 센서의 한 점으로 정확하게 모이지 않는다.
      • 정확하게 모이려면, 초점을 잘 맞춰야 한다.(카메라 렌즈와 이미지 센서간 거리를 잘 맞춰야 한다.)
  • 즉, 피사체의 각 부분(점)에서 방출된 빛이, 이미지 센서의 한 픽셀에 모이지 않는다. (여러 픽셀들에 투사된다.)
    • 이것이 Distortion의 기본 원리이다. (비약 + 생략이 많음)
  • (비약 + 생략이 많음)이러한 이유로, 단순히 intrinsic matrix(삼각비 공식)을 이용해서, 행렬 곱을 수행한다고,
    • (빨간색) 카메라 주점을 원점으로 하는 3D 절대좌표계의 좌표값 표현에서 -> image plane 좌표계로 변환이 정확히 되지 않는다.
  • distortion의 종료는 크게 2가지가 있다.
  • https://velog.io/@hsbc/카메라의-왜곡

4. Extrinsic이란?


5. Camera Calibration?

5.1. 개요

  • 아래 2개 값을 최소화하도록, 파라미터를 최적화하는 것.
    • 촬영해서 얻은 이미지 plane (H,W,3)
    • 3D XYZ 좌표계로부터, extrinsic / distortion model / intrinsic을 이용하여 구한 이미지 plane (H,W,3)
  • 최적화할 파라미터? (TODO: 이 부분 공부가 제대로 안됨)
    • extrinsic / distortion model / intrinsic의 모든 Parameter
  • 최적화 알고리즘?
    • Levenberg-Marquardt 알고리즘 같은 비선형 최소 제곱법

5.2. 준비물

  • 체스보드 패턴 판 (하얀 벽에 부착하라)

  • OpenCV 라이브러리 (python)

  • 조명이 일정하고 움직임이 없는 환경


  • 캘리브레이션 보드를 다양한 각도와 거리에서 촬영

5.3. opencv-python 사용

5.3.1. 체스보드의 각 특징점의 3D world 좌표계(객체 지점) 설정하기

  • 각 이미지에서 보드의 전체 패턴이 포함되도록 촬영해야 함
  • 이들 이미지는 여러 카메라로부터 촬영되었고,
    • 체스 보드는 정적으로 고정된 위치에 있었지만,
    • 각 카메라 촬영 관점에서, 체스 보드는 다른 위치와 방향에 놓여 있습니다.
  • 체스판의 3D XYZ 좌표를 단순화시키기 위해,
    • 체스판이 XY 평면을 유지하고 있는 절대좌표계를 도입(즉, 체스판 위의 모든 점의 Z=0이 되도록 좌표계를 도입)
    • 그리고 카메라는 이에 따라 움직이고 있습니다.
    • 이러한 중요한 가정이 오직 X, Y값만을 계산하는 것으로 단순화시켜 줍니다.
    • 이제 X,Y 값에 대해, 지점의 위치를 나타내는 (0,0), (1,0), (2,0), … 형식으로 단순하게 전달할 수 있습니다.
    • 이 경우, 결과는 체스판에서의 사각형의 크기 축척으로 구해집니다.
    • 그러나 만약 이 사각형의 크기를 알고 있다면(30mm라고 말할 수 있다면), mm 단위 결과로써 (0,0), (30,0), (60,0), …처럼 전달할 수 있습니다.
      • TODO: 코드에 넣을 때는, 그냥 (0,0), (1,0), (2,0), … 형식으로 단순하게 전달하는 것 같음
# 체스보드 패턴 생성
# 체스보드 크기
chessboard_size = (9, 6) # column 수, row 수
# objp: (9 * 6, 3)
objp = np.zeros((np.prod(chessboard_size), 3), np.float32)
"""
- np.mgrid: 
  - (2, column 수, row 수): (column 수, row 수)행렬이 2개인데, 
  - 각각 height 좌표, width 좌표를 의미
- (2,  column 수, row 수).T = (row 수, column 수, 2)
- reshape: (column 수 * row 수, 2)
- 이 뜻은, 
  - ( column 수, row 수) array를, 
  - height 순으로 읽어내려가겟다는 뜻이다. (세로 줄 하나씩 위에서 아래로 읽어가겠다는 뜻)
- objp[:, 0]: height index
- objp[:, 1]: width index
"""
objp[:, :2] = np.mgrid[0:chessboard_size[0],
                       0:chessboard_size[1]].T.reshape(-1, 2)

5.3.2. 2D 이미지 지점에서의 checkerboard 패턴 찾기

  • cv2.findChessboardCorners() 이용
  • 이 함수는 코너 지점과 패턴이 발견되었는지의 여부를 반환
  • 이들 코너 지점들은 위치상 왼쪽에서 오른쪽으로, 위에서 아래로 정렬되어 있습니다.

  • 일단 코너를 발견하면, cv2.cornerSubPix() 함수를 사용하여 정확도를 높일 수 있습니다.
  • 또한 cv2.drawChessboardCorners() 함수를 사용해 코너 결과를 그릴 수 있습니다.
    • 각 코너를 원이나 점으로 표시

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
imgpoints = []

# 이미지 로드
images = ["IMG_3323.jpg"]  # 캘리브레이션 이미지 파일 리스트

for image in images:
    img = cv2.imread(image)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # corners: np.array (N, 1, 2)
    ret, corners = cv2.findChessboardCorners(image=gray,
                                             patternSize=chessboard_size,
                                             corners=None)
    if ret:
        objpoints.append(objp)
        # corners2: cv2.typing.MatLike # cv2.mat_wrapper.Mat, NumPyArrayNumeric
        # corners2: np.array (N, 1, 2)
        corners2 = cv2.cornerSubPix(image=gray,
                                    corners=corners,
                                    winSize=(11, 11),
                                    zeroZone=(-1, -1),
                                    criteria=criteria)
        imgpoints.append(corners2)

        # 코너를 이미지에 그립니다.
        cv2.drawChessboardCorners(img,
                                  patternSize=chessboard_size,
                                  corners=corners,
                                  patternWasFound=ret)

        # 결과 이미지를 저장합니다.
        output_image_path = f"output_{image}"
        cv2.imwrite(output_image_path, img)

5.3.2.1. cv2.drawChessboardCorners()

  • 이 함수는 다양한 환경에서 감지 강건성을 높이기 위해, 보드 주변에 넓은 흰색 공간(테두리가 두꺼울수록 좋음)이 필요
  • 그렇지 않으면 테두리가 없고 배경이 어두운 경우 외부 검은 정사각형을 제대로 분할할 수 없어서 정사각형 그룹화 및 순서 지정 알고리즘이 실패합니다.
  • gen_pattern.py(캘리브레이션 패턴 생성)를 사용하여 체커보드를 생성하세요.
  • https://github.com/opencv/opencv/blob/4.x/doc/pattern.png

5.3.2.2. cv2.cornerSubPix

  • 이 함수는 코너 주변의 이미지 그레이디언트를 계산하여 코너 위치를 미세하게 조정
  • 설정된 반복 횟수 또는 정확도 기준에 도달할 때까지 정제 과정을 반복
  • winSize: 코너 주변을 고려할 윈도우 크기
  • zeroZone:
    • 중앙 영역의 크기를 설정하여 노이즈를 제거하는 데 사용.
    • 일반적으로 (-1, -1)로 설정하여 모든 영역을 고려

5.3.3. intrinsic / extrinsic / distortion parameter 구하기

  • 위 과정을 통해, 우리는 체커보드 특징점의 3D 좌표계(objpoints)를 구했고,
  • 각 체커보드 특징점에 매칭되는 (N개 view)의 이미지 plane에서의 각 이미지 좌표들(imgpoints)을 구했습니다.
  • 이제 우리는 cv2.calibrateCamera 메서드를 이용해, intrinsic / extrinsic / distortion parameter을 구할 수 있습니다.
  • 이론적 이해? https://velog.io/@hsbc/camera-calibration-이론
(ret, mtx, dist, rvecs, tvecs) = cv2.calibrateCamera(objpoints,
                                                     imgpoints,
                                                     imageSize=gray.shape[::-1],
                                                     cameraMatrix=None,
                                                     distCoeffs=None)
"""
ret: float
    - 전체 Root Mean Square re-projection error
    - 이 값은 캘리브레이션 과정의 품질을 나타내며, 낮을수록 더 정확한 캘리브레이션 결과를 의미
    - 0.4043353
mtx: np.ndarray (3, 3)
    - intrinsic matrix
    - [[2.82941040e+03 0.00000000e+00 1.97697417e+03]
 [0.00000000e+00 2.82939480e+03 1.46900315e+03]
 [0.00000000e+00 0.00000000e+00 1.00000000e+00]]

dist: np.ndarray (1, 5)
    - `[k1, k2, p1, p2, k3]`을 포함하며, 
    - 이는 각각 반경 왜곡 계수(k1, k2, k3)와 접선 왜곡 계수(p1, p2)
    [[ 0.09491546 -0.22681414  0.00043416  0.00034345  0.18057507]]

rvecs: Tuple[np.ndarray]
    - (np.ndarray (3, 1), ...)
    - 각 패턴 뷰에 대한 회전 벡터의 튜플.
    - 각 뷰에 대한 3x1 회전 벡터로, 월드 좌표계를 카메라 좌표계로 변환하는 회전을 나타냅니다.
    - 이 벡터는 Rodrigues 변환을 통해 회전 행렬로 변환할 수 있습니다.
    -  (array([[ 0.01413114],
       [ 0.01593625],
       [-0.00571477]]),)

tvecs: Tuple[np.ndarray]
    - 설명: 각 패턴 뷰에 대한 변환 벡터의 튜플.
    - 값의 의미:
      - 각 뷰에 대한 3x1 변환 벡터로, 월드 좌표계를 카메라 좌표계로 변환하는 평행 이동을 나타냄
      - `(array([[-3.89025738],
        [-2.26881858],
        [ 7.43518093]]),)`
    
"""
  • TODO: 아래 부분 공부 필요
  • cv2.calibrateCamera 알고리즘은 다음 단계를 수행합니다:

5.3.4. intrinsic matrix 개선하기

  • cv2.getOptimalNewCameraMatrix() 함수를 사용하여 카메라 메트릭스(intrinsic)를 개선할 수 있음
    • intrinsic에 distortion을 반영!
  • 새롭게 구해진 intrinsic matrix는, 촬영된 이미지 plane <-> undistored된 normalized image plane 간 변환을 가능하게함
  • 13개의 샘플 이미지 중 왜곡 현상을 제거할 하나를 사용해 이미지의 크기를 얻고, 카메라 메트릭스(intrinsic)를 얻는 코드는 다음과 같습니다.
# img = cv2.imread('./data/chess/left12.jpg')
h, w = img.shape[:2]
(newcameramtx, roi) = cv2.getOptimalNewCameraMatrix(cameraMatrix=mtx,
                                                    distCoeffs=dist,
                                                    imageSize=(w, h),
                                                    alpha=1,
                                                    newImgSize=(w, h))
"""
newcameramtx: np.ndarray (3, 3)
    - `출력되는 새로운 카메라 intrinsic 행렬.`
    - [[2.86704074e+03 0.00000000e+00 1.97858396e+03]
 [0.00000000e+00 2.86554634e+03 1.47026838e+03]
 [0.00000000e+00 0.00000000e+00 1.00000000e+00]]

roi: Tuple[int]
    - 선택적 출력 사각형으로, `왜곡 보정된 이미지에서 모든 유효 픽셀 영역을 둘러싼 사각형`
    - (x, y, w, h)
    - ROI 영역
    - (14, 12, 3997, 2995)
"""

5.3.4.1. cv::getOptimalNewCameraMatrix 함수

  • 자유 스케일링 파라미터(alpha)를 기반으로, 최적의 새로운 카메라 내부 행렬(intrinsic)을 반환
  • alpha:
    • 자유 스케일링 파라미터로, 0~1 사이
    • 0
      • 원본 이미지의 유효한 픽셀만, 보정된 이미지로 가져오기
      • 원본 이미지의 원치않는 픽셀을 최소로 갖는 보정된 이미지가 얻어지는데,
        • 원본 이미지의 코너 지점의 픽셀들이 제거될 수도 있음
      • 왜곡 보정된 이미지의 모든 픽셀이 유효한 pixel임
    • 1
      • 원본 이미지의 모든 픽셀은, 보정된 이미지에 유지됩니다.
      • (원본 이미지의 모든 픽셀왜곡 보정된 이미지유지)
  • 알파가 0보다 클 때, 왜곡 보정된 결과는,
    • 캡처된 왜곡 이미지 밖의 "가상" 픽셀에 해당하는 일부 검은 픽셀을 가질 가능성이 있습니다.
  • TODO: 공부 필요
  • 반환 값
    • new_camera_matrix:
      • 출력되는 새로운 카메라 intrinsic 행렬.
      • np.ndarray (3, 3)
    • validPixROI:
      • 선택적 출력 사각형으로, 왜곡 보정된 이미지에서 모든 유효 픽셀 영역을 둘러싼 사각형
      • Tuple[int]
      • (x, y, w, h)
      • (14, 12, 3997, 2995)

5.3.5. 왜곡 제거하기

  • 이제
    • extrinsic matrix도 구했고,
    • distortion이 반영된, 제대로 된 intrinsic matrix도 구했으니,
  • 왜곡된 이미지를 펴보자!
# dst: 보정된 이미지
dst = cv2.undistort(src=img,
                    cameraMatrix=mtx,
                    distCoeffs=dist,
                    dst=None,
                    newCameraMatrix=newcameramtx)
x, y, w, h = roi
dst = dst[y:y + h, x:x + w]
cv2.imwrite('calibresult.png', dst)
  • 이미지의 해상도가 캘리브레이션 단계에서 사용된 해상도와 다른 경우,
    • fxfy, cxcy는 각각 스케일링되어야 하며, 왜곡 계수는 동일하게 유지됨

5.3.6. calibration 과정의 오차 정량적 계산하기

  • 왜곡 제거: 이미지의 프로젝션
  • 이 왜곡 제거 시 수행된 프로젝션에 발생하는 오차가 얼마인지를 알기 위해 cv2.projectPoints() 함수가 사용
  • 결과적으로 얻어지는 값이 0에 가까울수록 정확한 것

# 에러 계산
tot_error = 0
for camera_idx in range(len(objpoints)):
    # objpoint: 실제 세계의 3D 점 (H*W, 3)
    # rvec: 각 패턴 뷰에 대한 회전 벡터의 튜플.
    # tvec: 각 패턴 뷰에 대한 변환 벡터의 튜플.
    # mtx: intrinsic matrix (3, 3) # optimal 은 아님
    # dist: distortion coefficients (1, 5)
    # object point를 이미지 point로 변환
    imgpoints2, _ = cv2.projectPoints(objpoints[camera_idx], rvecs[camera_idx],
                                      tvecs[camera_idx], mtx, dist)
    # cv2.findChessboardCorners + cv2.cornerSubPix로 얻은 이밎 point와,
    # 변환된 이미지 point와 거리 계산
    error = cv2.norm(imgpoints[camera_idx], imgpoints2,
                     cv2.NORM_L2) / len(imgpoints2)
    tot_error += error
print("total error: ", tot_error / len(objpoints))

cv2.destroyAllWindows()
profile
모든 의사 결정 과정을 지나칠 정도로 모두 기록하고, 나중에 스스로 피드백 하는 것

1개의 댓글

comment-user-thumbnail
2024년 7월 29일
  • 예전에 intrinsic matrix를 구하려고 시도했었던 여러 방법을 공유드렸었지만 리마인드차 남겨봅니다.

    1. 아이폰 공식문서의 스펙을 이용해 구하기: 정확한 정보를 구하기 힘들 뿐더러 (표기된 fov가 hfov/vfov/dfov중 뭔지, 몇배줌을 기준으로 작성된 것인지 등등 확실하지 않았음), 구한 결과를 적용했을 때 정성적으로 정확하지 않았음.
    2. opencv를 이용하기: 난이도로나 성능으로나 가장 쉽고 정확
    3. vanishing point의 특징을 이용하기: 안정적인 vanishing point가 맺히도록 사진을 찍는 것이 생각보다 노력을 요할 뿐더러, 적은 (10개 이하) 수의 사진으로는 원하는 정확도를 얻지 못했음.
  • opencv를 이용했던 방법중 아쉬웠던 부분을 적자면

    • cv2.cornerSubPix 함수는 체커보드의 꼭짓점을 pixel 단위보다 fine한 subpixel 단위로 보정해주는 함수인데요, 시각화해보니 오히려 꼭짓점의 위치를 왜곡시켜버리는 효과가 있었습니다. 사용 후 제대로 적용되었는지 확인이 필요합니다.
    • 체커보드가 image plane의 다양한 위치에 투사되게 찍는게 좋습니다. 멋모르고 다양한 각도에서만 찍었었는데요 (체커보드는 항상 이미지의 중심에 오게 찍어버림), 체커보드가 이미지의 중심 뿐만아니라 구석구석에 등장하게 찍어줘야 합니다.
답글 달기