어안 카메라 왜곡 보정

박재완·2025년 1월 30일

1. 렌즈계 왜곡의 종류

렌즈 왜곡에는 크게 방사왜곡(radial distortion)과 접선왜곡(tangential distortion)이 있습니다.

방사왜곡은 볼록렌즈의 굴절률에 의한 것으로서 아래 그림과 같이 영상의 왜곡 정도가 중심에서의 거리에 의해 결정되는 왜곡입니다.

2. 렌즈계 왜곡의 수학적 모델

일반적으로 렌즈 왜곡의 수학적 모델은 카메라 내부 파라미터의 영향이 제거된 normalized image plane에서 정의됩니다.

렌즈계의 왜곡이 없다고 할 경우, 3차원 공간상의 한 점 (Xc, Yc, Zc)는 central projection (pinhole projection)에 의해 normalized image plane상의 한 점 (xn_u, yn_u)로 투영됩니다 (첨자 n: normalized, u: undistorted):

그러나 실제로는 (xn_u, yn_u)는 렌즈계의 비선형성에 의해 왜곡(주로 radial distortion)이 됩니다. (xn_d, yn_d)를 렌즈계의 왜곡이 반영된 normalized 좌표라면, 렌즈계 왜곡 모델은 다음과 같습니다 (첨자 d: distorted):


위 수식에서, 우변의 첫번째 항은 radial distortion, 두번째 항은 tangential distortion을 나타냅니다 (k1, k2, k3는 radial distortion coefficient, p1, p2는 tangential distortion coefficient). ru는 왜곡이 없을 때의 중심(principal point)까지의 거리(반지름)입니다.

이 때, (xn_d, yn_d)는 normalized image plane에서의 좌표이며 실제 영상 픽셀 좌표 (xp_d, yp_d)는 카메라 내부 파라미터를 반영하여 다음과 같이 구해집니다.




여기서, fx, fy는 초점거리, cx, cy는 렌즈 중심 영상좌표(principal point), skew_c는 비대칭계수를 나타내는 카메라 내부 파라미터들입니다.

3. 영상 왜곡 보정

왜곡된 영상으로부터 보정된 영상을 생성하는 것은 비교적 간단하게 구현할 수 있습니다 (참고로, 왜곡된 영상을 보정하기 위해서는 먼저 카메라 캘리브레이션을 통해 카메라 내부 파라미터(intrinsic parameter)를 구해야 합니다.

왜곡된 영상을 Id, 보정된 Iu라 하겠습니다. 기본 아이디어는 Iu의 각 픽셀값을 해당 픽셀 좌표를 왜곡시켰을 때의 Id의 대응되는 픽셀값으로 채우는 것입니다.

영상 Iu의 한 점을 (xp_u, yp_u)라 하면, 일단 이것을 카메라 파라미터를 역으로 적용하여 normalized 좌표 (xn_u, yn_u)로 변환합니다

다음으로, 중심까지의 거리 ru (ru2 = xn_u2 + yn_u2)를 구하고 식(2) 왜곡모델을 적용하여 왜곡된 좌표 (xn_d, yn_d)를 구합니다.

마지막으로 (xn_d, yn_d)를 다시 픽셀 좌표계로 변환하면 (xp_u, yp_u)의 왜곡된 영상에서의 좌표 (xp_d, yp_d)를 구할 수 있습니다.

4.run_calibartion.py

import argparse
import os
import numpy as np
import cv2
from surround_view import CaptureThread, MultiBufferManager
import surround_view.utils as utils

카메라 보정 데이터를 저장할 디렉토리
TARGET_DIR = os.path.join(os.getcwd(), "yaml")
DEFAULT_PARAM_FILE = os.path.join(TARGET_DIR, "camera_params.yaml")

def main():
parser = argparse.ArgumentParser()

# 입력 카메라 장치 선택
parser.add_argument("-i", "--input", type=int, default=0,
                    help="input camera device")

# 체커보드 패턴 크기
parser.add_argument("-grid", "--grid", default="9x6",
                    help="size of the calibrate grid pattern")

parser.add_argument("-r", "--resolution", default="640x480",
                    help="resolution of the camera image")

parser.add_argument("-framestep", type=int, default=20,
                    help="use every nth frame in the video")

parser.add_argument("-o", "--output", default=DEFAULT_PARAM_FILE,
                    help="path to output yaml file")

parser.add_argument("-fisheye", "--fisheye", action="store_true",
                    help="set true if this is a fisheye camera")

parser.add_argument("-flip", "--flip", default=0, type=int,
                    help="flip method of the camera")

parser.add_argument("--no_gst", action="store_true",
                    help="set true if not use gstreamer for the camera capture")

args = parser.parse_args()

if not os.path.exists(TARGET_DIR):
    os.mkdir(TARGET_DIR)

text1 = "press c to calibrate"
text2 = "press q to quit"
text3 = "device: {}".format(args.input)
font = cv2.FONT_HERSHEY_SIMPLEX
fontscale = 0.6

resolution_str = args.resolution.split("x")
W, H = int(resolution_str[0]), int(resolution_str[1])
grid_size = tuple(int(x) for x in args.grid.split("x"))
grid_points = np.zeros((1, np.prod(grid_size), 3), np.float32)
grid_points[0, :, :2] = np.indices(grid_size).T.reshape(-1, 2)

objpoints, imgpoints = [], []  # 3D 및 2D 좌표 저장

# 카메라 연결
device = args.input
cap_thread = CaptureThread(device_id=device,
                           flip_method=args.flip,
                           resolution=(W, H),
                           use_gst=not args.no_gst)
buffer_manager = MultiBufferManager()
buffer_manager.bind_thread(cap_thread, buffer_size=8)

if cap_thread.connect_camera():
    cap_thread.start()
else:
    print("cannot open device")
    return

quit = False
do_calib = False
i = -1

while True:
    i += 1
    img = buffer_manager.get_device(device).get().image
    if i % args.framestep != 0:
        continue

    print(f"searching for chessboard corners in frame {i}...")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    found, corners = cv2.findChessboardCorners(
        gray, grid_size,
        cv2.CALIB_CB_ADAPTIVE_THRESH +
        cv2.CALIB_CB_NORMALIZE_IMAGE +
        cv2.CALIB_CB_FILTER_QUADS
    )
    if found:
        term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01)
        cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), term)
        print("OK")
        imgpoints.append(corners)
        objpoints.append(grid_points)
        cv2.drawChessboardCorners(img, grid_size, corners, found)

    cv2.putText(img, text1, (20, 70), font, fontscale, (255, 200, 0), 2)
    cv2.putText(img, text2, (20, 110), font, fontscale, (255, 200, 0), 2)
    cv2.putText(img, text3, (20, 30), font, fontscale, (255, 200, 0), 2)
    cv2.imshow("corners", img)

    key = cv2.waitKey(1) & 0xFF
    if key == ord("c"):
        print("\nPerforming calibration...\n")
        N_OK = len(objpoints)
        if N_OK < 12:
            print(f"Less than 12 corners ({N_OK}) detected, calibration failed")
            continue
        else:
            do_calib = True
            break
    elif key == ord("q"):
        quit = True
        break

if quit:
    cap_thread.stop()
    cap_thread.disconnect_camera()
    cv2.destroyAllWindows()

if do_calib:
    N_OK = len(objpoints)
    K = np.zeros((3, 3))
    D = np.zeros((4, 1))
    rvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
    tvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
    calibration_flags = (cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC +
                         cv2.fisheye.CALIB_CHECK_COND +
                         cv2.fisheye.CALIB_FIX_SKEW)

    if args.fisheye:
        ret, mtx, dist, rvecs, tvecs = cv2.fisheye.calibrate(
            objpoints, imgpoints, (W, H), K, D, rvecs, tvecs,
            calibration_flags, (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
        )
    else:
        ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
            objpoints, imgpoints, (W, H), None, None)

    if ret:
        fs = cv2.FileStorage(args.output, cv2.FILE_STORAGE_WRITE)
        fs.write("resolution", np.int32([W, H]))
        fs.write("camera_matrix", K)
        fs.write("dist_coeffs", D)
        fs.release()
        print("successfully saved camera data")
        cv2.putText(img, "Success!", (220, 240), font, 2, (0, 0, 255), 2)
    else:
        cv2.putText(img, "Failed!", (220, 240), font, 2, (0, 0, 255), 2)

    cv2.imshow("corners", img)
    cv2.waitKey(0)

if name == "main":
main()

6.결과

profile
안녕하세요!

0개의 댓글