안녕하세요, 기술 블로그 전문 에디터입니다. 스테레오 카메라 시스템을 구축하고 깊이(Depth) 정보를 얻는 과정에서, 보정(Rectification) 단계에서 예상치 못한 이미지 오정렬 문제와 불안정한 깊이 값 때문에 어려움을 겪는 개발자분들을 위해 이 글을 준비했습니다.
특히 OpenCV를 사용하여 스테레오 카메라의 보정을 수행했음에도 불구하고, 수평선 정렬(Epipolar Line Alignment)이 어긋나거나 깊이 맵이 무작위 값을 보이는 현상은 많은 개발자를 당황하게 만듭니다. 이번 글에서는 스택오버플로우에서 제기된 실제 사례를 바탕으로, 이러한 문제의 원인을 심층 분석하고 명확한 해결 방법을 제시해 드리겠습니다.
저희는 구글 검색 상위 노출을 위한 SEO 최적화와 사용자 경험(UX)을 고려하여, 개발자 친화적인 고품질 기술 문서를 제공합니다. 자, 그럼 문제의 원인과 해결책을 함께 살펴보시죠.
1. 에러 발생 상황
한 개발자분께서 IMX219 듀얼 카메라 보드를 사용한 스테레오 카메라 시스템에서 겪는 문제에 대해 문의하셨습니다. 시스템은 1620×1232 해상도의 이미지를 사용하며, 60개 이상의 이미지 쌍으로 단안 및 양안 캘리브레이션(Monocular and Binocular Calibration)을 수행했으며 RMS(Root Mean Square) 값도 낮게 나왔다고 합니다. 이는 캘리브레이션 자체는 성공적으로 이루어졌음을 시사합니다.
그러나 문제는 캘리브레이션 후 이미지 보정(Rectification) 단계에서 발생했습니다. 양쪽 카메라에서 캡처한 이미지를 보정하여 나란히 표시했을 때, 약 30픽셀 정도의 큰 오차가 발견되었습니다. 아래 이미지는 보정 결과가 제대로 정렬되지 않은 모습을 보여줍니다.
이러한 보정 오차는 곧바로 깊이 추정(Depth Estimation)에도 영향을 미쳤습니다. 개발자는 깊이 맵(Depth Map)에서 무작위적인 값들이 산재되어 나타나는 현상을 경험했다고 보고했습니다.
개발자는 문제 해결을 위해 다음 두 가지를 이미 확인했습니다:
- 카메라 좌우 매핑이 바뀌지 않았음.
- 캘리브레이션과 런타임 간 해상도가 일관됨.
아래는 개발자가 사용한 OpenCV를 활용한 파이썬 코드의 일부입니다.
BASE_DIR = Path(__file__).resolve().parent
CALIB_FILE = BASE_DIR / "stereo_calibration" / "stereo_calibration.npz"
data = np.load(CALIB_FILE)
NUM_LINES = 20
K1 = data["K1"]
D1 = data["D1"]
K2 = data["K2"]
D2 = data["D2"]
R = data["R"]
T = data["T"]
R1 = data["R1"]
R2 = data["R2"]
P1 = data["P1"]
P2 = data["P2"]
Q = data["Q"]
image_size = (1640, 1232) # <-- 이 부분에 주목해주세요!
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
K1,
D1,
K2,
D2,
image_size,
R,
T,
flags=cv2.CALIB_ZERO_DISPARITY,
alpha=0
)
map1x, map1y = cv2.initUndistortRectifyMap(
K1, D1, R1, P1,
image_size,
cv2.CV_32FC1
)
map2x, map2y = cv2.initUndistortRectifyMap(
K2, D2, R2, P2,
image_size,
cv2.CV_32FC1
)
FOCAL_LENGTH = P1[0, 0]
BASELINE = float(data["baseline_mm"])
width = int(data["image_width"])
height = int(data["image_height"])
print(width, height)
print(BASELINE)
print(FOCAL_LENGTH)
rms = float(data["rms"])
# ... (생략된 StereoSGBM 및 get_depth_map 함수)
2. 명확한 발생 원인
개발자는 “해상도가 캘리브레이션과 런타임 간 일관됨”을 확인했다고 했지만, 제공된 코드와 설명에서 핵심적인 불일치가 발견되었습니다. 이 불일치가 바로 30픽셀 오차와 깊이 값 불안정의 원인입니다.
문제는 `cv2.stereoRectify` 함수에 전달되는 image_size 매개변수에 있습니다. 코드의 이 부분을 다시 살펴보겠습니다:
image_size = (1640, 1232) # <-- 문제의 원인
# ...
width = int(data["image_width"])
height = int(data["image_height"])
print(width, height) # 캘리브레이션 파일의 실제 이미지 크기 (예: 1620, 1232)
여기서 개발자는 실제 이미지 해상도가 1620×1232라고 언급했지만, `cv2.stereoRectify` 함수가 사용하고 있는 `image_size`는 (1640, 1232)로 너비(width) 값이 다릅니다. `cv2.stereoRectify` 함수 호출 이후에 캘리브레이션 파일에서 `width`와 `height`를 로드하고 출력하고 있는데, 이 `print(width, height)` 결과는 아마 (1620, 1232)였을 것입니다. 즉, `cv2.stereoRectify`는 잘못된 너비(1640)로 보정 매핑을 계산하고 있었던 것입니다.
왜 이것이 문제인가?
cv2.stereoRectify 함수는 스테레오 카메라의 내부(Intrinsic) 및 외부(Extrinsic) 파라미터를 사용하여 두 이미지 평면이 완벽하게 평행하고, 에피폴라 선(Epipolar Line)이 정확히 수평선이 되도록 이미지 평면을 회전하고 투영하는 매트릭스(R1, R2, P1, P2)를 계산합니다. 이 과정에서 image_size 매개변수는 매우 중요한 역할을 합니다. 이는 새로 생성될 보정된 이미지의 크기와 새로운 카메라 매트릭스(P1, P2)를 계산하는 데 사용됩니다.
만약 `cv2.stereoRectify`에 잘못된 `image_size`가 전달되면, 계산된 R1, R2, P1, P2 매트릭스는 실제 이미지 크기에 맞지 않게 됩니다. 그 결과, `cv2.initUndistortRectifyMap`과 `cv2.remap`을 통해 이미지를 변환할 때, 에피폴라 선이 완벽하게 정렬되지 않고 약 30픽셀의 오차가 발생하게 되는 것입니다.
에피폴라 선의 오정렬은 깊이 추정 단계에서 치명적입니다. `cv2.StereoSGBM_create`와 같은 알고리즘은 좌우 이미지에서 동일한 물체를 찾아 시차(Disparity)를 계산하는데, 이 시차 계산은 에피폴라 선을 따라 이루어집니다. 선이 어긋나면 올바른 대응점을 찾기 어렵거나 아예 찾지 못하게 되어, 깊이 맵이 무작위 값으로 나타나는 현상으로 이어집니다.
3. 해결 방법 및 코드 예시
이 문제의 해결책은 의외로 간단합니다. `cv2.stereoRectify` 함수에 전달되는 image_size 매개변수가 실제 카메라로 캡처되는 이미지의 너비와 높이 또는 캘리브레이션 시 사용된 이미지의 너비와 높이와 정확히 일치하도록 수정하는 것입니다.
가장 좋은 방법은 캘리브레이션 파일(`stereo_calibration.npz`)에 저장된 실제 이미지 크기(`image_width`, `image_height`)를 사용하여 `image_size` 변수를 정의하는 것입니다.
수정된 코드 예시
아래와 같이 `image_size` 변수를 수정하여 문제를 해결할 수 있습니다.
BASE_DIR = Path(__file__).resolve().parent
CALIB_FILE = BASE_DIR / "stereo_calibration" / "stereo_calibration.npz"
data = np.load(CALIB_FILE)
# ... (K1, D1, K2, D2, R, T, R1, R2, P1, P2, Q 로딩 부분은 동일)
# 캘리브레이션 데이터에서 실제 이미지 너비와 높이를 가져옵니다.
# 'image_size = (1640, 1232)'와 같은 하드코딩된 값 대신 실제 값 사용
width = int(data["image_width"])
height = int(data["image_height"])
# stereoRectify에 전달할 image_size를 캘리브레이션 데이터의 실제 크기로 설정합니다.
image_size = (width, height) # 예: (1620, 1232)
print(f"실제 이미지 크기: {image_size}") # 수정된 이미지 크기를 출력하여 확인
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
K1,
D1,
K2,
D2,
image_size, # 실제 이미지 크기로 변경된 image_size 사용
R,
T,
flags=cv2.CALIB_ZERO_DISPARITY,
alpha=0
)
map1x, map1y = cv2.initUndistortRectifyMap(
K1, D1, R1, P1,
image_size, # 동일하게 수정된 image_size 사용
cv2.CV_32FC1
)
map2x, map2y = cv2.initUndistortRectifyMap(
K2, D2, R2, P2,
image_size, # 동일하게 수정된 image_size 사용
cv2.CV_32FC1
)
FOCAL_LENGTH = P1[0, 0]
BASELINE = float(data["baseline_mm"])
# ... (나머지 StereoSGBM 및 get_depth_map 함수는 동일)
이 수정으로 인해 `cv2.stereoRectify`는 올바른 이미지 크기 정보를 기반으로 보정 매트릭스를 계산하게 되며, `cv2.initUndistortRectifyMap` 또한 동일한 일관된 크기를 사용하여 정확한 매핑을 생성할 것입니다. 이렇게 되면 이미지 보정 시 발생하는 30픽셀 오차는 사라지고, 에피폴라 선이 완벽하게 정렬되어 안정적인 깊이 맵을 얻을 수 있습니다.
4. 향후 예방을 위한 팁
이러한 종류의 문제를 예방하고 스테레오 비전 시스템의 안정성을 높이기 위해 몇 가지 팁을 드립니다.
-
파라미터 일관성 유지
캘리브레이션, 보정, 그리고 실제 이미지 처리 과정에서 사용되는 모든 이미지 관련 파라미터(특히 해상도)는 항상 일관되어야 합니다. 하드코딩된 값보다는 캘리브레이션 파일에서 직접 로드하거나, 런타임에 캡처된 이미지의 실제 속성을 사용하는 것이 안전합니다.
-
데이터의 단일 소스
캘리브레이션 데이터를 저장할 때, 카메라 파라미터(K, D, R, T)뿐만 아니라 캘리브레이션에 사용된 이미지의 너비와 높이 등 모든 관련 정보를 함께 저장하는 것이 좋습니다. 그리고 이 파일이 모든 후속 처리의 단일 진실 공급원(Single Source of Truth)이 되도록 합니다.
-
시각적 검증 습관화
보정(Rectification) 후에는 항상 보정된 이미지에 에피폴라 선을 그려서 시각적으로 정렬 상태를 확인하는 습관을 들여야 합니다. 이 단계에서 오정렬이 발견된다면, 깊이 추정으로 넘어가기 전에 문제를 해결해야 합니다. 문제가 발생했을 때 빨리 발견하고 고칠 수 있는 가장 확실한 방법입니다.
-
디버깅을 위한 출력문 활용
코드 중간중간에 `print()` 함수를 사용하여 주요 변수들의 값(예: `image_size`, `width`, `height` 등)을 출력하여 예상과 일치하는지 확인하는 것은 작은 실수를 찾아내는 데 큰 도움이 됩니다.
스테레오 카메라 시스템은 정확한 캘리브레이션과 일관된 파라미터 관리가 매우 중요합니다. 위 해결책과 예방 팁을 통해 안정적인 스테레오 비전 시스템을 구축하시길 바랍니다. 궁금한 점이 있다면 언제든지 댓글로 문의해주세요!
![[에러 해결] Python 재귀 함수 수동 캐싱 오류: 잘못된 메모이제이션 원인과 올바른 구현 방법 [에러 해결] Python 재귀 함수 수동 캐싱 오류: 잘못된 메모이제이션 원인과 올바른 구현 방법](https://dev-error.com/wp-content/plugins/contextual-related-posts/default.png)