
시리즈 5/7. 같은 체커보드 이미지에서 렌즈의 광학적 품질까지 뽑아냅니다. 그런데 첫 번째 질문은 이겁니다. "텔레센트릭 렌즈는 왜곡이 0 이라고 하는데, 굳이 측정해야 할까요?"
1. 기하 지표와 광학 지표의 차이
앞 편에서 다룬 기하학적 지표 (재투영 오차, 직교성) 는 "격자가 반듯한가" 를 봅니다. 그런데 이 지표들로는 답하지 못하는 질문이 여전히 있습니다.
- 이 렌즈의 왜곡 특성이 스펙 범위 안에 들어오는가?
- 이미지 전체에 조명이 균일하게 들어왔는가?
- 포커스가 영역별로 균일하게 맞았는가?
이건 격자의 기하학적 반듯함이 아니라, 렌즈 자체의 물리적/광학적 특성입니다. 이번 편에서 다룰 세 지표가 정확히 이 영역을 담당합니다.
- 방사 왜곡 계수 (Radial Distortion k1, k2)
- 영역별 콘트라스트 (Michelson Contrast)
- 에지 샤프니스 (Sobel-based Sharpness)
2. 방사 왜곡 계수 — "0 인 걸 알면서 왜 재나요?"
2.1. 텔레센트릭 렌즈에 왜곡 측정이 왜 필요한가
우리 장비는 텔레센트릭 렌즈를 사용합니다. 이론적으로 텔레센트릭 렌즈의 왜곡 계수는 ≈ 0 이어야 합니다. 그러면 "어차피 0 인 걸 아는데, 왜 굳이 측정하나요?" 라는 질문이 자연스럽게 나옵니다.
저도 이 질문을 스스로에게 던져 봤고, 결론은 이랬습니다.
"이론상 0" 이라는 전제 자체를 실측으로 검증하는 것이 캘리브레이션의 본질이다.
텔레센트릭 렌즈도 제조 편차가 있고, 오래 쓰면 미세하게 틀어집니다. 충격을 받거나 온도 변화가 큰 환경에서는 광학 요소의 정렬이 바뀔 수 있습니다. k1 이 기준값을 벗어나는 순간 이 그 렌즈를 의심해야 할 시점입니다.
즉 이 지표는 "값이 얼마인가" 가 아니라 "임계값을 넘었는가" 를 판정하기 위해 존재합니다. 대부분의 경우 측정값은 1e-9 수준이고 사람이 직접 볼 필요도 없습니다. 그러다 어느 날 1e-5 가 찍히면, 그때 이 지표가 밥값을 합니다. 캘리브레이션 도구의 많은 부분이 "안 쓰이다가 결정적인 순간에 한 번 쓰이는" 식으로 동작합니다. 평소에 값이 0 이라는 것 자체가 도구가 잘 만들어졌다는 증거입니다.
2.2. 계산 과정
1) 이미지 중심 (cx, cy) 기준 각 코너의 반경
r_detected[i] = sqrt((x[i] - cx)² + (y[i] - cy)²)
2) 재투영 오차에서 구한 projected 점들의 반경
r_ideal[i] = sqrt((px[i] - cx)² + (py[i] - cy)²)
3) 반경 방향 편차
dr[i] = r_detected[i] - r_ideal[i]
4) 최소자승법으로 모델 피팅
dr = k1 · r³ + k2 · r⁵
→ cv::solve(A, b, x, DECOMP_SVD) 로 [k1, k2] 추정
5) 최대 왜곡량
dMaxRadialDistortion = max(|dr[i]|) (pixels)
2.3. 왜 3차, 5차 항만 쓰는가
Brown–Conrady 왜곡 모델의 방사 성분은 이렇게 생겼습니다.
dr = k1·r³ + k2·r⁵ + k3·r⁷ + ...
일반 렌즈 캘리브레이션에서는 k3 까지 쓰기도 합니다. 그런데 텔레센트릭 렌즈에서는 k1, k2 만으로 충분합니다.
기대값이 거의 0 인 영역에서 고차항을 넣으면 오히려 노이즈에 피팅되어 해석이 어려워집니다. 모델의 자유도를 필요 이상으로 늘리면 "아무것도 없는 곳에서 패턴을 찾아내는" 현상이 벌어집니다. 모델은 목적에 맞게 가볍게 유지하는 편이 좋습니다.
2.4. 판정 기준
|k1| < 1e-7 : 왜곡 없음 (정상 텔레센트릭)
|k1| > 1e-5 : 비텔레센트릭 의심 — 렌즈 점검 필요
2.5. 구조체
// STEP 9: 왜곡
double dDistortionK1;
double dDistortionK2;
double dMaxRadialDistortion; // pixels
3. 영역별 콘트라스트 — Michelson
3.1. 무엇을 보는가
조명과 노출이 9 영역에 균일하게 들어왔는지를 봅니다. 체커보드의 흑/백 타일이 각 영역에서 얼마나 명확하게 구분되는가로 정량화합니다.
3.2. Michelson 콘트라스트
contrast = (V_max - V_min) / (V_max + V_min)
두 밝기 값의 차이를 합으로 정규화한 값입니다. 0 ~ 1 사이로 나오고, 1 에 가까울수록 흑과 백이 명확하게 구분된다 는 뜻입니다. 체커보드는 이 계산에 딱 어울리는 패턴입니다. 각 코너 주변에는 흑/백 타일이 대각선 배치로 놓여 있어서, 작은 패치만 샘플링해도 V_min / V_max 를 뽑아낼 수 있습니다.
3.3. 계산 방식
각 코너 주변 NxN 패치 (예: 20×20 px):
코너를 중심으로 4분면으로 나눔
┌─────┬─────┐
│ A │ B │ 체커보드 패턴상
├─────┼─────┤ A, D = black, B, C = white
│ C │ D │ (또는 그 반대)
└─────┴─────┘
V_white = (B + C) 평균
V_black = (A + D) 평균
contrast = (V_white - V_black) / (V_white + V_black)
영역 인덱스 (r, c) 로 9 영역 소속 판정 → dRegionContrast[9] 평균
3.4. 왜 Michelson 인가
콘트라스트 정의는 여러 가지가 있습니다.
- Weber:
(V_max - V_min) / V_min— 배경이 균일할 때 - Michelson:
(V_max - V_min) / (V_max + V_min)— 주기 패턴에 적합 - RMS: 픽셀 강도의 표준편차 — 전역 이미지 통계용
체커보드는 주기적인 흑/백 패턴 이므로 Michelson 이 표준입니다. 또한 평균 밝기에 의존하지 않고 정규화되어 있어서, 노출 조건이 다른 이미지끼리도 비교 할 수 있다는 장점이 있습니다. 이 점이 산업 현장에서 특히 중요합니다. 조명 세팅이 미묘하게 바뀌더라도 같은 기준으로 비교할 수 있어야 하거든요.
3.5. 구조체
// STEP 7: 콘트라스트
double dRegionContrast[9]; // 영역별 0 ~ 1
double dContrastMin;
double dContrastMax;
4. 에지 샤프니스 — "포커스는 잘 맞았는가"
4.1. 계산
각 코너 주변의 작은 crop 에서 Sobel 그래디언트의 평균 크기 를 계산합니다.
// 각 코너 위치에서 31 × 31 crop
cv::Sobel(crop, sobelX, CV_64F, 1, 0);
cv::Sobel(crop, sobelY, CV_64F, 0, 1);
cv::magnitude(sobelX, sobelY, gradient);
double sharpness = cv::mean(gradient)[0];
// 영역별 평균 → dRegionSharpness[9]
4.2. 절대값이 아니라 상대값
샤프니스는 mean(|gradient|) 이기 때문에 절대 수치 자체는 조명/노출에 따라 달라집니다. 그래서 우리가 실제로 보는 건 영역 간 상대 편차입니다.
double dSharpnessMin;
double dSharpnessMax;
// 사용 예: (max - min) / avg 가 크면 영역별 포커스 불균일
4.3. 왜 유용한가 — "가장 흔한 보이지 않는 불량"
포커스 문제는 렌즈 캘리브레이션에서 가장 흔한 "보이지 않는 불량" 입니다. 현장 운용자는 이미지가 살짝 흐릿해도 검출이 성공하면 그냥 진행해 버리는 경우가 많습니다. 검출률만 체크하는 시스템이라면 이 상태로 계속 측정이 돌고, 어느 순간 측정값이 누적으로 틀어지기 시작합니다.
샤프니스 지표가 있으면 "검출은 성공했지만 포커스가 일부 영역에서 나쁘다" 는 상황을 잡을 수 있습니다. 유용한 조합 예시는 이런 것들입니다.
- 중심 샤프니스 대비 모서리가 50% 이하 → 필드 커버리지 문제 (곡률 수차 의심)
- 전체 샤프니스가 기준값 이하 → 포커스 재조정 필요
- 특정 변만 샤프니스 급락 → 렌즈 한쪽 오염 또는 기울어진 설치
4.4. 구조체
// STEP 8: 샤프니스
double dRegionSharpness[9];
double dSharpnessMin;
double dSharpnessMax;
5. 전체 12 개 지표 총정리
이 편까지 소개한 지표를 한 표로 정리하면 이렇습니다.
| 분류 | 지표 | 필드 |
|---|---|---|
| 기본 | 해상도 H / V | dScaleH, dScaleV |
| 격자 회전 각도 | dGridAngle |
|
| 검출 코너 수 | iDetectedCount |
|
| 균일도 | 9 영역 스케일 | dRegionScaleAvg[9] |
| 등방성 | dIsotropyRatio |
|
| 중심-주변 편차 | dCenterEdgeDevMax, dCenterEdgeDevAvg |
|
| 기하 정밀도 | 재투영 오차 | dReprojErrorRMS, dReprojErrorMax |
| 직교성 | dOrthogonalityMean, dOrthogonalityMax |
|
| 광학 특성 | 방사 왜곡 k1, k2 | dDistortionK1, dDistortionK2 |
| 최대 왜곡량 | dMaxRadialDistortion |
|
| 영역별 콘트라스트 | dRegionContrast[9] |
|
| 영역별 샤프니스 | dRegionSharpness[9] |
체커보드 한 장에서 이 정도를 뽑아낼 수 있다는 건, 결국 코너 좌표에 들어있는 정보 밀도가 그만큼 높다 는 뜻입니다. 우리가 지금까지 그 대부분을 버리고 있었던 거죠.
6. 한 줄 교훈
측정의 목적은 "값을 알기 위해서" 가 아니라 "임계값을 넘었는지 판정하기 위해서" 다.
이 관점이 잡히고 나니, 앞으로 추가할 지표들의 우선순위가 훨씬 명확해졌습니다. "값을 보여주기 위한" 지표가 아니라 "판정을 내리기 위한" 지표 를 먼저 쌓는 거죠.
그런데 이렇게 열심히 지표를 뽑아내도, "어떻게 보여주느냐" 에서 실패하면 현장에서 안 쓰이게 됩니다. 다음 편은 이 12 개 지표를 운용자가 1 초 안에 해석할 수 있도록 만든 대시보드 설계 이야기 입니다.
'Vision & Inspection' 카테고리의 다른 글
| [LensCal] 아직 끝나지 않은 회고 — 렌즈 캘리브레이션 작업 중간 정리 (0) | 2026.04.13 |
|---|---|
| [LensCal] 숫자보다 히트맵 — 현장에서 실제로 쓰이는 대시보드 만들기 (0) | 2026.04.13 |
| [LensCal] 재투영 오차와 직교성 — 격자는 얼마나 반듯한가 (0) | 2026.04.13 |
| [LensCal] 체커보드에서 뽑아내는 기본 지표 5가지 (0) | 2026.04.13 |
| [LensCal] 검출기를 갈아끼울 수 있게 만든 설계 (0) | 2026.04.13 |