Introduction
앞선 게시물에서 핀홀(pinhole) 카메라 모델을 통해 3차원 좌표가 어떻게 픽셀 좌표로 변환되는지 알아봤습니다. 그러면 이제 문제없이 카메라의 포즈를 구할 수 있는 걸까요? 아쉽게도 그렇지 않습니다. 핸드폰이 아닌 로봇이나 자동차의 카메라에서 얻은 영상을 보면 실물과 많이 다름을 알 수 있습니다. 핸드폰의 경우도 광각으로 찍은 사진은 묘하게 실제 풍경과 비율이 다르다는 느낌을 받는 경우가 많습니다.
이는 우리가 영상을 찍을 때 렌즈(lens)를 사용하기 때문입니다. 앞서 설명한 투영 과정은 렌즈가 평면인 경우를 가정합니다. 그러나 더 넓은 FOV(field of view)를 얻기 위해서 곡률이 큰 렌즈를 사용하기 마련인데요. 위 사진은 그 예시로 어안렌즈(fisheye camera)로 바라본 세상입니다. 아래 덱들의 경우 왼쪽으로 직진으로 설치되어 있겠지만, 현재 이미지에서는 렌즈 모양을 따라 곡선으로 이쁘게 설치되어 있습니다.
이번 포스트에서는 이러한 렌즈에 의한 왜곡이 생기는 이유와 이를 상쇄하기 위해 해주는 작업들에 대해 이야기할 예정입니다. 수식적으로 너무 복잡하거나 불필요한 내용은 생략하였으니, 이러한 개념이 있구나 정도로 이해하시면 될 것 같습니다.
Distortion
렌즈 왜곡은 크게 두 가지 원인에 의해 발생합니다. 첫 번째는 렌즈의 굴절률에 의해 형성되는 방사 왜곡(radial distortion)이며, 두 번째는 카메라 제조 과정에서 생기는 접선 왜곡(tangential distortion)입니다. Radial distortion부터 살펴보도록 하겠습니다.
Radial distortion은 렌즈의 굴절률에 의해 생기는 왜곡입니다. 그렇기에 아래 그림과 같이 왜곡 정도가 중심에서의 거리에 의해 결정되게 되죠. 카메라의 중점(principal point)을 중심으로 대칭적으로 형성(자연스럽게 방사형)되는 게 그 특징입니다.
술통 왜곡이라고 불리는 barrel distortion은 이미지의 가장자리로 갈수록 바깥으로 휘어지는 형태를 띕니다.띱니다. 이는 오목렌즈(concave(minus) spherical lens)에서 주로 나타나며, 넓은 시야각을 위해 사용하는 광각 렌즈나 초점거리 차이가 큰 줌 렌즈에서 나타나는 현상입니다. 반대로 쿠션 왜곡이라고 불리는 cushion(pincushion) distortion은 barrel distortion과 선이 반대 방향으로 휘게 됩니다. 이는 볼록렌즈(convex(plus) spherical lens)에서 주로 나타납니다. Mustache distortion은 두 왜곡 현상을 합친 것과 같으며, 중심부는 barrel, 가장자리는 cushion 왜곡의 형태를 띱니다.
Radial distortion에 의해 왜곡된 좌표는 아래의 수식을 따르며, $k_n$은 radial distortion coefficient라고 합니다.
$$\left\{\begin{matrix} x_{distorted}=x(1+k_1r^2+k_2r^4+k_3r_6) \\ y_{distorted}=y(1+k_1r^2+k_2r^4+k_3r_6) \end{matrix}\right. \quad ,r=\sqrt{x^2+y^2}$$
Tangential distortion은 렌즈와 image plane이 완벽하게 평행을 이루지 못하기 때문에 발생합니다. 즉, 공정 상 오차나 설계적 한계로 인해 발생하는 왜곡입니다. Tangential distortion에 의해 왜곡된 좌표는 아래의 수식을 따르며, $p_n$은 tangential distortion coefficient라고 합니다.
$$\left\{\begin{matrix} x_{distorted}=x+[2p_1xy+p_2(r^2+2x^2)]\\ y_{distorted}=y+[p_1(r^2+2y^2)+2p_2xy] \end{matrix}\right. \quad ,r=\sqrt{x^2+y^2}$$
최종적으로 두 왜곡이 모두 적용된 좌표는 아래와 같으며, radial distortion coefficient($k_n$)와 tangential distortion coefficient($p_n$)을 합쳐 distortion coefficient라고 합니다.
$$\left\{\begin{matrix} x_{distorted}=\underbrace{x(1+k_1r^2+k_2r^4+k_3r^6)}_{radial\, distortion}+\underbrace{2p_1xy+p_2(r^2+2x^2)}_{tangential\, distortion}\\ y_{distorted}=\underbrace{y(1+k_1r^2+k_2r^4+k_3r^6)}_{radial\, distortion}+\underbrace{p_1(r^2+2y^2)+2p_2xy }_{tangential\, distortion}\end{matrix}\right. \quad ,r=\sqrt{x^2+y^2}$$
Undistortion
물체가 어떻게 왜곡되어 보이는지 알고 있다면, 복원 과정은 크게 어렵지 않습니다. 물론, 여기서 어렵지 않다는 것은 식을 세우는 단계까지이며, 계산은 꽤나 복잡합니다. 그 과정을 글로 옮기는 것은 꽤나 고통스러운 작업이기에 이 포스트에서는 다루지 않을 것입니다. 다만, 제가 참고한 글들(다크 프로그래머님의 글, 위키피디아)을 첨부하니 참고 바랍니다.
대신 이번 포스트에서는 실제로 카메라 왜곡 보정이 코드 상에서 어떻게 활용되는지 전반적인 과정을 살펴보도록 하겠습니다. 우선 카메라의 intrinsic parameter와 distortion coefficient를 하드웨어 스펙이나 주어진 configuration 파일에서 찾도록 합니다. 아래는 제가 현재 사용 중인 데이터셋의 예시입니다.
# General sensor definitions.
sensor_type: camera
comment: VI-Sensor cam0 (MT9M034)
# Sensor extrinsics wrt. the body-frame.
T_BS:
cols: 4
rows: 4
data: [0.0148655429818, -0.999880929698, 0.00414029679422, -0.0216401454975,
0.999557249008, 0.0149672133247, 0.025715529948, -0.064676986768,
-0.0257744366974, 0.00375618835797, 0.999660727178, 0.00981073058949,
0.0, 0.0, 0.0, 1.0]
# Camera specific definitions.
rate_hz: 20
resolution: [752, 480]
camera_model: pinhole
intrinsics: [458.654, 457.296, 367.215, 248.375] #fu, fv, cu, cv
distortion_model: radial-tangential
distortion_coefficients: [-0.28340811, 0.07395907, 0.00019359, 1.76187114e-05]
크게 세 부분으로 나뉘는데요. 첫 번째로는 이 센서가 카메라임을 알려주고 있습니다. 다음으로는 카메라의 extrinsic parameter를 제공하는데, 여기서의 값은 카메라가 달려있는 본체로부터의 $R, t$를 의미합니다. 마지막으로 제일 중요한 카메라의 스펙입니다. 카메라의 주사율, 해상도(사이즈), 카메라 모델(pinhole) 등 다양한 정보를 제공합니다.
그리고 intrinsic parameter(K)와 distortion coefficient(D) 역시 제공하는데요. 여기서 왜곡 모델은 radial-tangential 모델로 앞서 언급한 두 가지 왜곡 형태가 동시에 발생한 상황임을 알 수 있죠. 여기서 왜곡 계수의 개수는 radial 왜곡을 얼마나 정밀하게 묘사했느냐에 따라 값이 달라집니다.
이제 이 값들을 활용해 코드를 작성해 보도록 하겠습니다. 편의상 Python에서 OpenCV를 활용해 작성했으며, C++을 이용하는 방법은 추후에 stereo calibration 전반적인 내용을 다룰 때 다루도록 하겠습니다.
Initialize camera parameters for undistortion
해당 데이터셋은 stereo vision을 사용했기에 left와 right에 대해 각각의 파라미터들이 존재합니다. 이를 초기화해 준 코드입니다.
import cv2
import numpy as np
# Intrinsic matrix
K_left = np.float32(
[
[457.587, 0, 379.999],
[0, 456.134, 255.238],
[0, 0, 1],
]
)
K_right = np.float32(
[
[458.654, 0, 369.215],
[0, 457.296, 248.375],
[0, 0, 1],
]
)
# Distortion coefficients
D_left = np.float32([-0.28368365, 0.07451284, -0.00010473, -3.55590700e-05])
D_right = np.float32([-0.28340811, 0.07395907, 0.00019359, 1.76187114e-05])
왜곡 보정을 실전에서 활용하는 방법은 두 가지가 있습니다. 첫 번째는 전체 이미지에 대한 보정을 수행한 후 다른 작업을 이어나가는 것이고, 두 번째는 후에 사용될 특징점들을 미리 feature detection and matching 과정을 거쳐 선별한 후 해당 점들에 대해 보정을 수행하는 방법입니다.
전자의 경우 코드 복잡도가 낮은 대신 특징점만을 이용해 후속 동작들을 진행할 경우 불필요한 연산이 왜곡 보정 과정에서 들어가게 됩니다. 반면에 후자는 코드 복잡도가 조금 증가하는 대신 성능 측면에서 약간의 이득을 얻을 수 있습니다. 성능 측면의 비교는 후에 stereo calibration에서 전체 과정을 거친 성능을 비교하는 것으로 대체하겠습니다.
Undistort entire image
전체 이미지에 대한 왜곡 보정을 다루는 코드입니다. OpenCV를 활용해 간단하게 표현할 수 있습니다.
left_image = cv2.imread("original_images/left.png", cv2.COLOR_BGR2GRAY)
right_image = cv2.imread("original_images/right.png", cv2.COLOR_BGR2GRAY)
undistort_left = cv2.undistort(image_left, K_left, D_left)
undistort_right = cv2.undistort(image_right, K_right, D_right)
Undistort key points
이번에는 전체 이미지가 아닌 특징점들에 대한 보정을 다루는 코드입니다. Feature matching 하는 함수의 경우 이미 구현되어 있다는 가정으로 작성했습니다.
left_image = cv2.imread("original_images/left.png", cv2.COLOR_BGR2GRAY)
right_image = cv2.imread("original_images/right.png", cv2.COLOR_BGR2GRAY)
matched_left, matched_right = obtainCorrespondingPoints(
image_left.astype(np.uint8), image_right.astype(np.uint8), 50, show=False
)
undistorted_left = cv2.undistortImagePoints(matched_left, K_left, D_left)
undistorted_right = cv2.undistortImagePoints(matched_right, K_right, D_right)
위 이미지는 왜곡 보정 전과 후에 대한 비교입니다. 좌측이 원본 이미지이고, 우측이 왜곡 보정을 끝낸 영상입니다. 좌측 이미지의 경우 중심에서 멀어질수록 영상이 울렁거리거나 휘어지는 현상을 발견할 수 있는데요. 이를 우측 이미지에서는 반듯하게 펴준 것을 볼 수 있습니다. 이렇게 간단하게 왜곡 보정을 할 수 있습니다. 코드 전문은 아래 repository의 stereo_image.py
와 stereo_keypoint.py
파일을 보시면 됩니다.
Reference
이미지들에 대한 출처는 클릭 시 확인하실 수 있습니다.