
시리즈 3/7.
2편까지 해서 정렬된 그리드 (m_vGridMap) 가 준비됐습니다. 이제 여기 위에 분석 함수들을 얹기 시작할 텐데, 가장 먼저 올리는 건 화려한 지표가 아니라 기본 중의 기본입니다. 해상도, 등방성, 9영역 균일도, 중심-주변 편차. 이름만 들으면 심심해 보이지만, 뒤에 올라갈 모든 고급 지표는 결국 이 바닥 위에 서 있습니다.
기본이 흔들리면 고급 지표가 아무리 정교해도 숫자에 의미가 없어져요. 재투영 오차 0.1 px 라는 값도, 애초에 스케일 계산이 편향되어 있으면 그 0.1 px 는 그냥 위장된 거짓말입니다. 그래서 이번 편은 "당연해 보이지만 당연하지 않은" 것들에 대한 얘기입니다.
해상도 — 정의는 단순한데
가장 핵심이 되는 값이고, 수식은 한 줄이면 끝납니다.
해상도 = 타일 실제 크기 (mm) / 코너 간 픽셀 거리 (px)
구현도 어렵지 않아요. 수평과 수직을 분리해서 계산합니다.
dScaleH = dKnownPitch_mm / dMeanDistH_px;
dScaleV = dKnownPitch_mm / dMeanDistV_px;
그런데 "코너 간 픽셀 거리" 를 어떻게 뽑느냐가 실전에서는 생각보다 신경 쓸 게 많습니다. m_vGridMap[r][c] 을 이용해서 수평/수직 이웃 쌍의 거리를 전부 수집한 다음 평균을 내는데, 이때 빈 셀이 껴 있는 경우를 가드해야 해요.
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols - 1; ++c) {
int i0 = m_vGridMap[r][c];
int i1 = m_vGridMap[r][c+1];
if (i0 < 0 || i1 < 0) continue;
double d = cv::norm(m_vDetectedPoints[i0] - m_vDetectedPoints[i1]);
m_vDistH.push_back(d);
}
}
수직도 똑같이 돌립니다. 평균은 dMeanDistH_px, 표준편차는 dStdDistH_px 에 저장하고요.
H 와 V 를 굳이 분리하는 이유
처음에는 저도 "그냥 평균 하나 쓰면 되는 거 아닌가?" 생각했습니다. 근데 실장비 이미지를 몇 장 돌려보니 H 와 V 가 미묘하게 다릅니다. 같은 렌즈에서 말이에요.
원인은 여러 가지입니다. 센서의 픽셀이 완전한 정사각형이 아닐 수도 있고, 렌즈가 미세한 타원 왜곡을 가질 수도 있고, 광축이 센서에 완전 수직이 아닐 수도 있어요. 이 차이가 다음에 나올 등방성 지표의 근거가 됩니다. 만약 H 와 V 를 평균으로 합쳐버리면, 이 미세한 편차가 평균 속에 녹아 없어집니다. 문제가 있어도 안 보이는 상태가 되는 거죠.
표준편차를 함께 저장하는 이유
dStdDistH_px 는 처음에 "그냥 통계 넣어두면 좋으니까" 정도로 추가했는데, 나중에 이 값이 의외로 결정적인 역할을 하더군요. 이상적이라면 코너 간 거리는 거의 일정해야 해요. 표준편차가 지나치게 크다면 셋 중 하나입니다. 일부 코너가 오검출됐거나, 격자 정렬이 어긋났거나, 왜곡이 예상보다 크거나.
이게 왜 중요하냐면, 뒤에서 나올 재투영 오차와 이 값이 서로 다른 방향에서 같은 문제를 가리키는 경우가 있거든요. 두 지표가 동시에 경고를 내면 "아 이건 진짜 검출 문제다" 라고 확신 있게 말할 수 있습니다. 확신은 단일 지표에서 나오지 않고, 서로 독립적인 두 지표의 일치에서 나옵니다.
등방성 — 0.1% 라는 기준
X축과 Y축 해상도가 얼마나 일치하는가. 수식은 다시 한 줄입니다.
dIsotropyRatio = |dScaleH - dScaleV| / avg(dScaleH, dScaleV) × 100 (%)
판정 기준은 이렇게 잡았습니다.
enum IsotropyStatus {
ISOTROPY_OK = 0, // < 0.1%
ISOTROPY_WARNING = 1, // 0.1% ~ 0.5%
ISOTROPY_ERROR = 2 // > 0.5%
};
0.1% 라는 숫자가 어디서 나왔냐면, 저희가 쓰는 텔레센트릭 렌즈의 공칭 정밀도가 대략 이 수준이거든요. 이 값 위로 올라가면 렌즈 자체의 문제든, 설치 각도 문제든 뭔가 있는 겁니다. 참고로 이건 현장 데이터로 다시 튜닝해야 할 값이긴 해요. 교과서에서 온 값이라서요.
사실 이 값은 1차 게이트입니다
등방성의 진짜 역할은 "H 와 V 가 얼마나 일치하는지" 를 보여주는 게 아니라, "이 측정을 믿어도 되는가" 의 1차 필터입니다. 등방성이 ERROR 로 뜨면 그 뒤의 재투영 오차, 직교성 값은 거의 100% 같이 나빠집니다. 원인이 렌즈나 설치 쪽에 있을 가능성이 높거든요.
그래서 대시보드에서도 등방성 배지를 가장 눈에 띄는 위치에 놓았습니다. 초록(OK) / 노랑(WARNING) / 빨강(ERROR). 운용자는 이 배지 색 하나만 봐도 "오늘 이 장비는 믿을 수 있다/없다" 를 3초 안에 판단할 수 있어요. 숫자가 0.08% 인지 0.12% 인지 구별하는 것보다, "색이 바뀌었다" 는 사실 자체가 운용자에게는 훨씬 강한 신호입니다.
9영역 균일도 — 3×3 으로 나눈 이유
이미지를 3×3 으로 나눠서 각 영역에서 독립적으로 스케일을 냅니다.
[0] TL [1] T [2] TR
[3] ML [4] C [5] MR
[6] BL [7] B [8] BR
영역 인덱스 결정은 정수 나눗셈 두 번이면 됩니다.
int GetRegion9(int r, int c, int totalRows, int totalCols) {
int rBand = (r * 3) / totalRows;
int cBand = (c * 3) / totalCols;
return rBand * 3 + cBand;
}
그리고 각 영역에 속한 수평/수직 이웃 쌍의 거리로 국소 스케일을 냅니다. 영역 경계에 걸친 코너를 어디에 넣을지, 코너 수가 적은 영역을 어떻게 다룰지 같은 디테일이 의외로 결과에 영향을 줘요. 경험적으로는 "경계 코너는 상위 영역에 포함, 코너 수 3개 미만 영역은 평균에서 제외" 가 가장 안정적이었습니다. 이걸 정하느라 반나절쯤 씨름했어요.
왜 하필 3×3 이냐
이 질문에 답하려고 5×5, 4×4, 2×2 를 다 돌려봤습니다. 결론은 이래요.
5×5 는 영역이 작아져서 영역당 코너 수가 부족해집니다. 특히 작은 체커보드 (예: 7×5 패턴) 에서는 한 영역에 코너가 1~2개밖에 없어서 통계가 안 잡혀요. 2×2 는 너무 거칠어서 "중심이 좋은가 주변이 나쁜가" 를 구분할 수 없고요.
3×3 이 딱 "중심 / 변 / 모서리" 라는 세 그룹을 구분할 수 있는 최소 단위입니다. 이 세 그룹은 렌즈 설계에서 실제로 물리적 의미가 다릅니다. 대부분의 렌즈는 모서리가 가장 나쁘고, 변이 그 다음, 중심이 제일 깨끗해요. 이 경향을 검증할 수 있느냐가 판정 능력을 좌우합니다.
예를 들어 모서리 4칸만 이상하게 나오면 "광학적 필드 커버리지 문제" 를 의심해야 합니다. 반면 한쪽 변 3칸만 이상하면 "조명 비대칭" 일 가능성이 높아요. 이 두 진단은 전혀 다른 방향이고, 대응 방법도 다릅니다. 3×3 이 있어야 이 구분이 가능해요.
중심-주변 편차 — 9영역을 한 숫자로
9영역 균일도를 한 단계 더 압축한 지표입니다. 중앙 (C) 을 기준으로 주변 8영역이 얼마나 벗어나는지만 봅니다.
double center = m_tStepData.dRegionScaleAvg[4];
double maxDev = 0.0;
double sumDev = 0.0;
for (int i = 0; i < 9; ++i) {
if (i == 4) continue;
double dev = fabs(m_tStepData.dRegionScaleAvg[i] - center) / center * 100.0;
maxDev = std::max(maxDev, dev);
sumDev += dev;
}
m_tStepData.dCenterEdgeDevMax = maxDev;
m_tStepData.dCenterEdgeDevAvg = sumDev / 8.0;
이 지표가 특히 잘 잡는 케이스가 세 가지 있습니다.
텔레센트릭 렌즈 품질 검증 — 이론상 중심과 주변 스케일이 0.1% 이내여야 정상입니다. 그보다 크면 렌즈 불량입니다. 위에서 얘기한 "이론상 0 을 실측으로 검증" 의 한 케이스죠.
설치 각도 점검 — 광축이 기울어지면 특정 방향으로 편차가 커집니다. 예를 들어 오른쪽 영역 3개만 편차가 크다면 렌즈가 오른쪽으로 기울어져 있을 가능성이 있어요. 이걸 max 하나로 보면 "편차 크다" 만 알고 방향은 모릅니다. 그래서 대시보드에서는 이 값을 9영역 히트맵의 색상 강도로도 함께 표시합니다. 숫자 압축 + 방향성 표시 두 개를 같이 보여주는 거죠.
수차 — 방사 왜곡과는 다른 종류의 "스케일 편차" 를 잡아냅니다. 방사 왜곡은 중심에서 방사 방향으로 생기지만, 중심-주변 편차는 단순히 "영역별 스케일 차이" 라서 원인이 왜곡이 아니어도 잡힙니다.
기본 지표만으로 답할 수 없는 것
이번 편에서 소개한 지표들만으로 "이 렌즈의 스케일이 정상인가" 는 충분히 판단할 수 있습니다. 대부분의 운용 상황에서는 사실 이 기본 지표들만 봐도 답이 나와요.
그런데 한 가지 질문에는 여전히 답을 못 합니다. "격자 자체가 얼마나 반듯한가?" 평균 스케일은 정상인데 격자가 미묘하게 휘어져 있는 경우가 있거든요. 이건 기본 지표로는 안 잡힙니다. 평균이 가려버려요.
이 질문에 답하려면 개별 코너의 잔차를 봐야 하고, 그게 재투영 오차 입니다. 다음 편에서 다루겠습니다. 그리고 재투영 오차 혼자로는 또 잡지 못하는 게 있어서 직교성 도 같이 들어갑니다. 두 지표를 교차해서 봐야 진단이 결정적으로 달라지는 순간이 있는데, 그 부분이 다음 편의 하이라이트가 될 것 같네요.
'Vision & Inspection' 카테고리의 다른 글
| [LensCal] 텔레센트릭 렌즈에도 왜곡 측정이 필요한 이유 — 광학 품질 3종 세트 (0) | 2026.04.13 |
|---|---|
| [LensCal] 재투영 오차와 직교성 — 격자는 얼마나 반듯한가 (0) | 2026.04.13 |
| [LensCal] 검출기를 갈아끼울 수 있게 만든 설계 (0) | 2026.04.13 |
| [LensCal] Cognex 정식 검출기 vs 자체 OpenCV 검출기, 아직 고민 중입니다 (0) | 2026.04.13 |
| [LensCal] 렌즈 캘리브레이션 엔진을 다시 짜면서 — 시리즈 소개 (0) | 2026.04.13 |