케이스 스터디 — 사진밖에 없는 계측 데이터를 믿을 수 있게 만들기
README가 "무엇을 만들었나"라면, 이 문서는 "무엇을 결정했고 왜 그렇게 결정했나"입니다. 막힌 지점과 그때 고른 선택지를 순서대로 적었습니다.
0. 한 줄로
반도체 계측 장비가 측정 결과를 사진으로만 저장하고 데이터 파일을 남기지 않아서, 그 사진을 읽어 측정 데이터·공정능력 통계·보고서를 만드는 시스템을 만들었습니다.
기술적으로 어려웠던 건 "사진에서 숫자를 읽는 것"이 아니었습니다. 읽어낸 숫자를 믿어도 되는지 판단하는 것이었습니다.
계측 데이터에서 가장 위험한 실패는 프로그램이 멈추는 게 아니라, 틀린 값을 자신 있게 내놓는 것입니다. 아래 결정들은 전부 이 한 문장에서 나왔습니다.
1. 문제 — 장비가 데이터를 안 준다
wafer 위 여러 Point의 선폭(CD)과 정렬 오차(Overlay)를 계측 장비로 측정합니다. 장비는 결과를 화면에 띄우고, 그 화면을 사진(.bmp)으로 저장합니다. 그게 전부입니다. CSV도, 로그도 없습니다.
그래서 실제 업무는 이랬습니다.
- 사진을 한 장씩 연다
- 사진에 찍힌 숫자를 눈으로 읽는다
- 엑셀에 손으로 옮겨 적는다
- 그 값으로 Cp/Cpk를 계산한다
Point 하나에 사진 6장, wafer 하나에 Point 10개 이상. 한 번 측정할 때마다 수십~수백 장입니다.
시간도 문제였지만 진짜 문제는 따로 있었습니다. 눈으로 읽고 손으로 옮기는 과정에서 오타가 나도 아무도 모릅니다. 원본이 사진이라 나중에 대조할 수도 없습니다.
사진에 뭐가 찍혀 있나
측정값 사진에는 흰 글자로 이렇게 찍힙니다.
2=6.8,XY=(0.0,6.8)um
번호=값,XY=(X좌표,Y좌표) 형식입니다. 그리고 몇 가지 성가신 성질이 있습니다.
- 측정 전 사진과 측정값 사진이 섞여 있음 — 측정 전 사진에는 흰 글자가 없습니다
- 파일명에 종류 힌트가 없음 —
CaptImg20260619110318.bmp처럼 시간만 들어 있습니다 - 순서가 매번 다름 — Overlay가 먼저일 수도, Line/Space가 먼저일 수도 있습니다
2. 첫 번째 결정 — 범용 OCR을 버리다
처음에는 당연히 Tesseract를 썼습니다. 그리고 값이 널뛰었습니다.
6을8로,5를8로 오독- 배율을 키워도 해결되지 않음 — 폰트 자체를 헷갈리는 문제였습니다
- 게다가 2차 버그가 붙었습니다. 앞부분이 깨지면 코드가 대신 괄호 안 좌표를 집어왔습니다.
3=6.5,XY=(0.2,6.5)에서6.5대신0.2를 읽는 식으로요
관점을 바꾼 지점
Tesseract는 "어떤 폰트로 쓰였는지 모르는 글자"를 읽는 도구입니다. 그래서 확률적으로 추측합니다.
그런데 이 사진은 그런 상황이 아니었습니다. 계측기 화면이라 폰트도 글자 색도 항상 똑같습니다. 모르는 걸 추측할 이유가 없었습니다.
그래서 범용 OCR을 버리고 템플릿 매칭으로 갔습니다.
- 정답을 눈으로 확인한 사진에서
0~9와 기호(=,.()XY) 모양을 글자 단위로 잘라 저장 (build_templates.py→char_templates.pkl) - 새 사진에서는 글자를 하나씩 잘라 템플릿과 픽셀 일치율로 비교해서 가장 비슷한 것을 고름 (
ocr_core.py) - 일치율이 기준(
CONFIDENCE_THRESHOLD = 0.85)에 못 미치면 값을 내놓되확인필요로 표시
결과
Sample 폴더 사진 72장(측정값이 찍힌 39장) 전체를 다시 돌렸습니다.
| 항목 | Tesseract | 템플릿 매칭 |
|---|---|---|
| CD (Target 14.5) | 12.8 ~ 18.3 으로 널뜀 | 14.6 ~ 15.3 |
| L/S (Target 9.5 / 5.5) | 8.7 같은 값이 튀어나옴 | 9.2~9.6 / 5.2~5.7 |
| Overlay 좌표 오추출 | 발생 | 0건 |
| 신뢰도 미달 | — | 0건 (39장 전부 0.85 이상) |
배운 것: 범용 도구가 안 되면 더 좋은 범용 도구를 찾는 게 아니라, 내 문제에만 있는 제약(폰트가 고정이다)을 무기로 쓸 수 있는지 먼저 보는 게 빨랐습니다.
3. 두 번째 결정 — 순서를 믿지 않는다
사진은 시간순으로 저장되지만, 그 순서가 무엇을 뜻하는지는 보장되지 않습니다. Overlay가 먼저 찍힐 수도, Line/Space가 먼저 찍힐 수도 있고, 측정 전/측정값 순서도 항목마다 다릅니다.
"n번째 사진은 CD"처럼 순서를 가정하는 코드는 당장은 돌아가고 언젠가 반드시 조용히 틀립니다. 그리고 틀렸을 때 아무 티가 안 납니다.
그래서 순서를 아예 쓰지 않기로 했습니다.
- 측정 전/측정값 구분 — 흰 글자가 있으면 측정값, 없으면 버림 (18장 중 정확히 9/9로 갈림)
- 종류 판별 — 값이 4개면 Overlay, 2개면 Line/Space, 1개면 CD
- Target 매칭 — 읽어낸 값이 어느 Target에 가장 가까운지로 배정 (
measurement_plan.py) - Overlay 방향 — OCR이 읽은 번호 순서가 아니라, 글자 덩어리의 화면 픽셀 좌표를 4점의 중심과 비교해서 상/하/좌/우 판별 (
overlay_analysis.assign_directions)
마지막 항목이 특히 그랬습니다. 처음엔 "번호 0,1,2,3이 곧 방향이겠지" 했는데, 실제 사진에서는 번호 순서와 화면 위치가 사진마다 뒤바뀌었습니다. 번호를 믿었으면 상/하가 뒤집힌 데이터를 자신 있게 내놨을 겁니다.
4. 실제 사진이 가르쳐준 것들
여기서부터는 책상에서 나온 설계가 아니라, 실제 문제 사진을 폴더째 확보해서 재현하며 나온 수정들입니다.
4-1. 겹쳐 그려진 측정줄
계측기가 측정줄 두 개를 화면에 겹쳐 그리는 경우가 있습니다. 글자가 물리적으로 뭉개져서 그 줄은 못 읽습니다.

※ 실제 계측 사진은 공정 데이터라 공개할 수 없어, 같은 현상을 코드로 재현한
합성 이미지입니다(scripts/make_demo_images.py --overlap). 라벨 두 개가 겹쳐
2=1.3,XY=(1.3,0.0)과 3=1.0,XY=(1.0,0.0)이 한 줄로 섞였습니다.
아래는 같은 사진의 정상적인 줄입니다.

재현한 이미지를 실제 판독기에 넣어보면 이렇게 나옵니다 — 줄 4개가 3개로 잡히고, 값은 2개만 읽힙니다. 글로 쓴 현상이 실제로 그렇게 동작한다는 걸 이미지 자체가 증명합니다.
여기까지는 "한 줄 못 읽음"이면 끝날 문제인데, 실제로는 훨씬 나쁜 일이 벌어졌습니다. 종류를 "읽은 값 개수"로 판별하고 있었기 때문에, 4줄짜리 Overlay 사진이 2줄만 읽히면 Line/Space로 둔갑해서 엉뚱한 Target에 매칭됐습니다. 한 줄을 못 읽은 게 사진 한 장 전체를 오염시킨 겁니다.
→ 종류 판별의 근거를 "읽은 개수"에서 "글자 박스의 폭으로 역산한 실제 줄 개수"로 바꿨습니다 (count_lines_in_boxes). 값을 못 읽어도 "여기 줄이 몇 개였다"는 사실은 살아남습니다.
그리고 그런 사진에서 읽힌 나머지 값도 전부 확인필요로 표시합니다. 옆줄이 뭉개질 정도의 사진이면 읽힌 값도 의심스럽기 때문입니다.
곁가지로 두 가지를 더 고쳤습니다.
- 글자줄을 묶을 때 쓰는 가로 부풀림이 ±14였는데, 나란히 찍힌 다른 측정줄까지 한 덩어리로 붙여버려 그 줄 전체를 못 읽게 만들고 있었습니다 → ±5로 축소
- 밝은 사진은 배경까지 글자로 잡혀 12만 픽셀씩 잡혔습니다 → 흰 픽셀 비율이 1.5%를 넘으면 밝기 임계값을 단계적으로 올리도록 자동 조절
4-2. X와 Y가 붙어버리는 문제
X와 Y가 화면에서 맞닿게 찍히면 글자 하나로 뭉개져 엉뚱한 글자(주로 7)로 읽힙니다.
예전 코드는 이때 "형식이 안 맞는다"며 줄 전체를 버렸습니다. Sample3 폴더에서는 14장 중 12장을 통째로 놓치고 있었습니다. 값 자체는 멀쩡히 읽혔는데 구분자가 뭉개졌다는 이유로요.
→ 값과 좌표 사이의 ,XY= 부분은 글자를 그대로 요구하지 않고 "아무 글자 3개까지"로 완화했습니다.
LINE_RE = re.compile(r'(\d)=(\d+\.\d).{0,3}=\((\d+\.\d)[,.](\d+\.\d)\)')
# ^^^^^^ 여기가 ,XY= 자리
느슨하게 풀면 위험해 보이지만, 뒤에 나올 값-좌표 교차검증이 그 위험을 잡아줍니다. 4개 폴더 전수 비교로 안전한지 확인하고 풀었습니다.
같은 맥락에서 신뢰도 점수도 고쳤습니다. ,XY= 부분 글자는 점수 계산에서 빼고 실제로 값에 쓰는 글자(번호·값·X·Y)만 봅니다. 안 그러면 구분자가 뭉개졌다는 이유로 멀쩡한 줄이 무더기로 확인필요에 걸려서, 진짜 이상값이 그 속에 묻힙니다.
4-3. 밝은 패드를 글자로 착각
밝은 원형 패드 무늬가 글자 마스크로 잡히면서 세로 180px짜리 큰 덩어리가 생겨 글자줄로 오인됐습니다.
추측으로 임계값을 정하는 대신 실측했습니다. 4개 폴더 187줄을 전수 확인한 결과, 측정줄 한 줄의 높이는 항상 19px이었고 두 줄이 겹쳐도 28px을 넘지 않았습니다.
→ MAX_LINE_HEIGHT = 40 — 세로 40px을 넘는 덩어리는 글자줄에서 제외합니다. 실측 최대치 28px에 여유를 두되, 오인 사례인 180px과는 확실히 갈리는 값입니다.
배운 것: 임계값은 감으로 정하면 나중에 왜 그 숫자인지 아무도 모릅니다. 세어보고 정하면 근거가 코드에 남습니다.
5. "읽었다"와 "맞다"는 다르다
여기가 이 프로젝트에서 가장 마음에 드는 부분입니다.
측정줄 한 줄에는 숫자가 셋 있습니다 — 값, X좌표, Y좌표. 실제 사진 113줄을 놓고 이 셋의 관계를 살펴보다가 규칙을 찾았습니다.
값 = √(X² + Y²)
※ 실제 판독 데이터입니다. 왼쪽은 Sample 폴더의 통과 사례, 오른쪽은 LS Sample 폴더에서 유일하게 걸린 줄입니다.
값은 좌표 벡터의 길이였습니다. 그런데 중요한 건 이겁니다 — 값과 좌표는 줄 안에서 따로 찍혀 있고, OCR도 따로 읽습니다. 즉 서로 독립적으로 읽힌 숫자끼리 검산이 됩니다. 한쪽을 잘못 읽으면 이 등식이 깨집니다.
→ 이 관계가 허용 오차(REDUNDANCY_TOLERANCE = 0.05)보다 크게 어긋나면 확인필요로 표시합니다 (read_value_line).
그리고 예상 못한 결과
113줄 중 112줄은 오차 0.012 이하로 통과했고, 딱 1줄이 걸렸습니다. 오탐 0건입니다.
걸린 줄(8 (45).bmp의 번호 1)은 값이 1.8인데 좌표 길이는 1.71, 오차 0.088이었습니다. 사진 실물을 잘라서 확인했습니다.
OCR은 정상이었습니다. 사진에 1=1.8,XY=(0.2,1.7) 이라고 그대로 찍혀 있었습니다. 계측기가 값과 좌표를 서로 안 맞게 출력한 것이었습니다.
그래서 이 장치의 이름을 바꿨습니다. "OCR 오독 검출기"가 아니라 "값-좌표 정합성 검증기"입니다. OCR 오독과 원본 데이터 불일치를 둘 다 잡습니다.
배운 것: 데이터 안에 이미 들어 있는 중복(redundancy)을 찾으면, 추가 정보 없이 검산할 수 있습니다. 그리고 검증기를 만들면 내가 의심하던 것 말고 다른 것이 잡힐 수 있습니다 — 그때 검증기의 정의를 고쳐야지, 결과를 무시하면 안 됩니다.
6. 답을 모를 때는 지어내지 않는다
Line/Space 측정은 한 사진에 값이 두 개 찍히는데, 어느 쪽이 Line이고 어느 쪽이 Space인지 표시가 없습니다.
보통은 Target이 다르니 가까운 쪽으로 배정하면 됩니다. 그런데 Line Target과 Space Target이 같은 경우(둘 다 2um)가 있었습니다. 오차가 정확히 같아져서(0 = 0) 배정이 사실상 동전 던지기가 됩니다.
사진 속 번호 순서나 화면 위치는 앞서 봤듯 사진마다 뒤바뀌어서 못 씁니다.
물리에서 답을 찾다
이 사진들은 도금 전 사진입니다. Line은 PR(감광액)이 없어 바닥면이 보여 어둡게, Space는 PR이 덮여 있어 밝게 찍힙니다.
크로스헤어(측정 십자선) 주변 픽셀 밝기를 비교해봤더니, 테스트 폴더 6장 전부 "큰 값 = 어두움 = Line" 패턴이 일관됐습니다.

※ 코드로 그린 사진입니다. 마커 주변 밝기를 프로그램이 실제로 재보면 Line 69.2 / Space 98.0 — 차이 28.8로 확실히 갈립니다. 번호 순서가 뒤집힌 문제 사진에서도 밝기 패턴만은 안 뒤집혔습니다.
그런데 100%는 아니었다
전체 11장으로 넓히니 9장만 일치했습니다. 크로스헤어가 밝기 경계에 걸치는 사진이 있었습니다.

※ 같은 자리를 같은 축척으로 잘랐습니다. 마커만 경계로 옮겼을 뿐인데 Line 69.2 / Space 79.0 — 차이가 28.8에서 9.8로 줄어듭니다. 조금 더 어두운 띠 쪽으로 밀면 차이는 0.1까지 떨어집니다.
여기서 알게 된 것이 하나 더 있습니다. 이 검증은 틀린 답을 내놓는 방식으로 무너지지 않습니다. Line 마커도 같은 어두운 띠 위에 있어서 Space가 그보다 더 어두워질 수는 없기 때문에, 판정이 뒤집히는 게 아니라 두 밝기의 차이가 0에 수렴해 판정이 동전 던지기가 됩니다. 그래서 "밝기가 반대로 나왔다"가 아니라 "밝기로는 아무 말도 할 수 없다"가 정확한 표현입니다.
여기서 선택지가 둘이었습니다.
| 선택 | 결과 |
|---|---|
| 밝기 검증 결과로 배정을 뒤집는다 | 82% 확률로 맞고 18% 확률로 조용히 틀림 |
| 배정은 그대로 두고 불일치를 표시한다 | 사람이 그 2장을 확인하게 됨 |
두 번째를 골랐습니다. 배정은 1차 원칙(Line이 Space보다 물리적으로 큼)대로 두고, 밝기 검증이 어긋나면 로그를 남기고 해당 행을 확인필요로 표시합니다 (verify_ls_brightness, match_ls_targets).
82%짜리 근거로 값을 뒤집는 건, 정확도를 조금 올리는 대신 "이 값이 왜 이렇게 됐는지 아무도 모르는" 경우를 만드는 일입니다. 계측 데이터에서는 남는 장사가 아닙니다.
같은 철학이 프로그램 전체에 일관됩니다 — 값-좌표 검증도, 밝기 검증도, 신뢰도 점수도 값을 바꾸지 않습니다. 표시만 합니다.
디버깅 노트: 첫 구현에서 verify_ls_brightness가 NumPy의 np.False_를 반환해서 is False 비교가 통과하지 않았습니다. 파이썬 False와 값은 같지만 같은 객체가 아니라서요. bool()로 감싸 해결했습니다.
7. 조용히 틀리지 않게 — 회귀 테스트
측정 도구를 고칠 때 제일 무서운 건 판독 결과가 달라졌는데 모르고 지나가는 것입니다.
처음의 기준값은 이랬습니다.
5.5 Space Avg=5.3923 / 9.5 Line Avg=9.4154 / 14.5 Pad CD Avg=14.9538, 측정결과 92행
평균 3개와 행 수. 그리고 이게 새는 그물이라는 걸 실제로 겪었습니다. 사진 한 장(9-1_22.png)이 통째로 안 읽혔는데 평균은 멀쩡해서 안 걸렸습니다.
→ 기준을 사진 한 장 한 장의 판독 결과 전체로 내렸습니다 (scripts/regression_check.py).
- 데이터셋 6종을 JSON 기준값으로 고정 — 실측 5종 + 합성 1종(아래 10절)
- 달라지면 "어느 사진 어느 줄"까지 짚어줌
- 2층 구조 — ①사진별 판독(측정 계획과 무관) ②매칭·통계(계획 반영). 1층이 통과하고 2층만 깨지면 판독이 아니라 매칭 로직 문제라는 게 바로 갈립니다
기준 데이터셋도 그냥 모은 게 아닙니다. LS Sample은 Line Target = Space Target 상황을 재현하려고 직접 촬영해서 추가한 것이고, Sample2/Sample3은 겹친 줄·XY 붙음 버그를 재현하는 폴더입니다. 버그를 고칠 때마다 그 버그를 재현하는 사진이 기준값에 남았습니다.
한 가지가 더 붙었습니다. 실측 사진은 사내 데이터라 공개 저장소에 넣을 수 없는데,
기준값이 실측 사진에만 묶여 있으면 공개본에서는 회귀 검증을 아예 돌릴 수 없습니다.
그래서 합성 사진(10절)을 6번째 기준값으로 추가했습니다 — regression_check.py --demo는
사진이 없으면 그 자리에서 만들어(난수 seed 고정) 기준값과 대조합니다. 저장소를 clone 받은
사람도 같은 검증을 돌릴 수 있습니다.
8. 결과물 — 무엇이 나오는가

※ 예시 데이터로 생성한 화면입니다. 실제 공정 측정값이 아닙니다.
엑셀에는 측정값·Target·규격 판정이 행 단위로 남고, HTML 리포트에는 항목별 Cpk와
추세가 함께 나옵니다. 값 하나하나에 확인필요·입력방법이 붙어 있어서, 나중에
"이 숫자는 기계가 읽은 것인가 사람이 넣은 것인가"를 되짚을 수 있습니다.
9. 두 얼굴, 한 몸
데스크탑 프로그램(tkinter)으로 시작했는데, 사내 여러 명이 쓰게 되면서 웹 버전이 필요해졌습니다.
측정 로직을 복사하지 않았습니다. 웹은 기존 코어를 그대로 import 합니다. 판독 규칙이 두 벌 존재하면 언젠가 갈라지고, 그러면 "데스크탑 결과와 웹 결과가 다른" 최악의 상황이 옵니다.
다만 한 곳에서 구조가 갈렸습니다. 못 읽은 값을 사람이 입력하는 단계입니다.
- 데스크탑 — 같은 프로세스 안에서 tkinter 창이 답을 기다릴 수 있어서, 처리 도중에 끼워 넣으면 됩니다
- 웹 — 서버 스레드가 사람 입력을 기다릴 수 없습니다. 그래서 처리를 "판독" → "사람 입력" → "이어서 처리" 세 토막으로 쪼개고, 요청 두 번(처리 시작 / 폼 제출)에 나눠 부릅니다
손으로 넣은 값도 OCR 값과 완전히 같은 경로(Target 매칭·이상치 검사)를 지나갑니다. 그리고 엑셀의 입력방법 열에 자동/수동이 남습니다. 계측 데이터니까 사람이 넣은 값이 기계가 읽은 값과 안 섞이고 추적돼야 합니다.
운영은 Oracle Cloud ARM + Docker Compose + Caddy(Let's Encrypt 자동 갱신)이고, 배포는 스크립트 한 줄입니다.
10. 보여줄 수 있게 만들기 — 계측기 사진을 코드로 그리다
도구를 다 만들고 나서 막힌 곳은 엉뚱한 데였습니다. 보여줄 수가 없었습니다. 실제 계측 사진은 사내 데이터라 밖에 못 내놓고, 로그인 없이는 화면 하나 열리지 않으니 "이런 걸 만들었습니다"라고 말할 방법이 없었습니다.
그래서 계측기 사진을 코드로 그렸습니다(scripts/make_demo_images.py).
실제 사진을 픽셀 단위로 관찰해서 다시 만들었습니다. 처음엔 SEM으로 짐작하고 회색 고대비 배경을 그렸는데 실물과 전혀 달랐습니다 — 광학 현미경 사진이라 전체적으로 흐릿하고 대비가 아주 낮습니다(전 채널 80~150). 색 상수는 전부 실측값에서 가져왔습니다.
제일 중요한 발견은 측정 표시였습니다. 십자 마커가 아니라 선분 + 양 끝 X이고,
전부 초록으로 이어 그려야 합니다. ls_brightness가 초록 덩어리의 중심 주변 밝기를
재기 때문입니다 — X만 따로 찍으면 중심이 경계에 놓여 밝은 쪽과 어두운 쪽이 반반 섞이고
판정이 무너집니다.
제약도 하나 있었습니다. 배경은 어느 채널도 170을 넘으면 안 됩니다. 판독기가
R>170 & G>170 & B>170을 글자로 보기 때문에, 넘으면 배경 무늬가 글자 덩어리로 잡혀
줄 개수 판별이 틀어집니다. 다행히 실제 사진도 최대 150 언저리라 이 제약이 사실성을
해치지 않았습니다.

※ 위 사진은 전부 코드가 그린 것입니다. 4-1절의 겹친 라벨도 이 생성기로 재현했습니다 — 화면 가운데 왼쪽에 두 라벨이 겹쳐 있습니다.
검증은 자기참조로 했습니다. 만든 사진을 진짜 판독기로 다시 읽어 줄 수별 장수가
{0:9, 1:9, 2:9, 4:9}로 나오고 확인필요가 0건인지 확인합니다. 생성기만 통과하고 실제
프로그램에서 안 읽히면 아무 소용이 없으니까요.
결과만 보여주는 데모는 절반짜리였다
사진 36장으로 무로그인 데모를 열었더니 곧 문제가 드러났습니다. 방문자가 무슨 사진이 처리되는지 모른 채 숫자 표만 보게 된 것입니다. 이 도구의 값어치는 "사진 → 숫자"인데 정작 그 변환이 화면에 없었습니다.
그래서 판독 과정을 눈에 보이게 만들었습니다.
- 소개 화면에 사진 3종(CD / L&S / Overlay)을 놓고, 사진 위에 SVG 층을 겹쳐 화살표로
짚어줍니다. 주석을 픽셀에 굽지 않은 이유는 문구를 고칠 때마다 사진을 다시 만들어야
하기 때문입니다.
viewBox를 원본 크기(1280×1024)와 맞추면 원본 픽셀 좌표를 그대로 쓰면서도 문구는 텍스트로 남습니다. - 결과 화면에서는 측정 지점을 고르면 그 지점 사진에 프로그램이 실제로 읽은 영역이 초록 사각형으로 표시되고, 옆에 읽은 값과 규격 판정이 붙습니다.
여기서 한 가지를 의식적으로 지켰습니다. 사진이 고정이니 값도 고정이라 미리 계산해 넣고 싶어지지만, 그러면 판독이 깨져도 화면은 멀쩡해 보입니다. 데모가 거짓말을 하게 되는 것입니다. 그래서 그림(미리보기·좌표)은 한 번 만들어 공유하고, 값은 그 실행이 실제로 읽은 결과에서 가져옵니다.
11. 돌아보며
관통하는 원칙 하나를 꼽으면 이겁니다.
모르면 모른다고 말한다.
- 신뢰도가 낮으면 → 값은 내놓되
확인필요 - 값과 좌표가 안 맞으면 → 고치지 않고
확인필요 - 밝기 검증이 어긋나면 → 뒤집지 않고
확인필요 - 두 스펙에 모두 들어맞으면 → 판정하지 않고 사람에게
전부 "정확도를 조금 포기하고 설명 가능성을 얻는" 교환입니다. 일반적인 소프트웨어라면 과할 수 있지만, 계측 데이터는 틀린 값 하나가 공정 판단을 바꿉니다. 값이 틀렸다는 걸 알 수 있는 상태가, 틀렸는지 모르는 상태보다 낫습니다.
그리고 이 원칙은 도메인을 알아야 세울 수 있었습니다. Line이 Space보다 크다는 것, PR이 없으면 어둡게 찍힌다는 것, Cpk 계산에서 표본표준편차를 써야 한다는 것 — 코드가 아니라 공정 지식에서 나온 판단들입니다.
부록 — 숫자로 보는 프로젝트
| Python | 7,893줄 |
| 커밋 | 242개 |
| 회귀 테스트 데이터셋 | 실측 5종 + 합성 1종 |
| 외부 OCR 엔진 | 0개 (자체 템플릿 매칭) |
| 리포트 차트 라이브러리 | 0개 (SVG 직접 생성) |
| 배포 | Oracle ARM · Docker · Caddy · 스크립트 1줄 |
의존성을 얇게 유지한 건 취향이 아니라 제약이었습니다. 사내망에서 설치 승인 없이 돌아가야 했습니다.