US-01 · T1-T5
✅ 충족
사진 입력 + 전처리
이미지 업로드, RGB/resize, 포맷·크기 검증, 흐림/어두움 거부.
원래 요구 (AC)
이미지 업로드, RGB/resize 정규화, 포맷·크기·EXIF 등 입력 검증, 너무 어둡거나 흐릿한 이미지 거부.
구현
- UI: Streamlit
st.file_uploader (jpg/jpeg/png/webp). PIL.Image.open으로 RGB 변환.
- 입력 검증:
validate_image_file()가 확장자 + 헤더(MIME sniffing) + 최대 24 MB.
- 품질 평가:
assess_image_quality()가 Laplacian variance 기반 blur score, 평균 luminance brightness score, 224×224 해상도 하한으로 quality_flags(blurry/dark/too_small) 생성.
- 전처리: 224×224 center-crop + ImageNet mean/std 정규화. PyTorch/ONNX 모두 같은 numpy tensor.
검증
pytest tests/test_quality.py 정상/blur/dark fixture 통과. 실측 시연 카메라 캡처 정상 처리.
US-02 + US-06 · T6-T10
✅ 충족
Top-3 후보 + 유사 구분
cosine 유사도 기반 Top-3, 점수 표시, 로그, 상세 페이지 링크, 유사 외관 분리.
원래 요구 (AC)
cosine 유사도 기반 Top-3, 점수 표시, 검색 로그, 상세 페이지 링크, 유사한 외관(궁궐/문 등) 명확 구분.
구현
search_by_image: image embedding × 13 클래스 prototype index, 같은 클래스 max-pool로 1점수 응축.
search_by_text: text embedding × landmark_text_index.json + keyword/alias fusion (text 0.6, keyword 0.4).
ConfidencePolicy 4-way: matched / ambiguous(마진 부족) / out_of_scope(임계값 미만) / low_quality(품질 플래그 우선).
- UI
render_top3: 카드 3개 + "자세히 보기" 버튼. cosine은 0~100 스케일.
- 로그:
logs/demo.jsonl에 top1, top2, margin, reason_codes, thresholds, model_version, quality_flags, mode, runtime.
검증
demo_regression_v1.json 양성 13 + OOS 2 fixture로 image 18/18, text 10/10 통과. FP32↔INT8 회귀 cos 0.999, top-1 100%, decision 100% 일치.
US-03 · T11-T14
✅ 충족
상세 정보 조회
landmark 정보 데이터, 상세 페이지 UI, 한·영 이름·설명·좌표.
원래 요구 (AC)
landmark 정보 데이터베이스, 상세 페이지 UI, 한국어/영어 이름·설명·좌표 노출.
구현
- 데이터:
assets/landmark_info.json에 13 클래스 × {id, name_ko, name_en, description_ko, description_en, lat, lng, address, hero_image}.
- 화면 전환:
st.session_state["page"]로 home ↔ landmark. "자세히 보기" → page="landmark" + selected_landmark_id 세팅 + st.rerun().
render_landmark_page: hero 이미지, 이름 ko/en, 설명, Google Maps 임베딩, "← 검색으로 돌아가기" 버튼.
검증
13 클래스 모두 직접 클릭해 페이지 진입 확인. 텍스트 검색 ↔ 상세 이동도 이슈 #8 수정 후 정상.
US-04 · T15-T17
⚠️ 부분 충족
저신뢰 입력 예외 처리
흐림/어두움 거부, 저신뢰 안내, 지원 범위 밖 입력 처리.
원래 요구 (AC)
흐릿/어두운 이미지 거부, 신뢰도 낮은 결과 안내, 지원 범위 밖 입력 처리.
구현
- 품질 게이트:
quality_flags를 ConfidencePolicy가 가장 먼저 보고 low_quality로 분기. "후보 참고용으로 보기" 토글로 강제 열람.
- OOS 처리: 화면 캡처/uniform noise/관계없는 이미지는 cosine 미만으로
out_of_scope. 회귀 fixture에 OOS 2건 포함.
- 애매한 입력 처리: Top-1 점수와 Top-2 마진이 충분하지 않으면
ambiguous로 안내하고 후보를 참고용으로 보여준다.
검증
화면 캡처와 uniform noise는 out_of_scope, 심한 blur/dark 입력은 low_quality로 처리됨을 확인했다. 결정과 reason_codes는 jsonl 로그에 기록된다.
아직 부족한 점
강한 노이즈, 압축 깨짐, 저해상도 재확대가 섞인 복합 손상 이미지는 사람이 보기 어려워도 현재 품질 휴리스틱이 항상
low_quality로 분리하지는 못한다. 예를 들어 실제 광화문 이미지를 심하게 망가뜨린 경우 낮은 점수의
ambiguous 후보가 나올 수 있다.
Sprint 2 구현 방향
- 실제 랜드마크 이미지에 blur, dark, noise, low-resolution, JPEG artifact를 입힌
quality regression set을 만든다.
assess_image_quality()에 noise_score, artifact 감지 지표, 복합 손상 기준을 추가한다.
- 복합 손상 지표가 높으면 모델 후보 점수보다
low_quality를 우선 적용해 재촬영 안내로 분기한다.
US-07 · T18-T22
✅ 초과 충족
경량 모델 + 추론
ONNX export, 로딩, embedding 생성, 추론 상태. Sprint 1에서 NPU 실측·text encoder ONNX까지 확장.
원래 요구 (AC)
경량(ONNX) 모델 준비, 로딩, embedding 생성, 추론 상태 반환.
구현
- 학습 가중치: MobileCLIP2-S4 fine-tuned
best.pt (1.7 GB).
- Image encoder ONNX:
scripts/export_mobile_onnx.py (opset 18, dynamo). FP32 1.23 GB external data + INT8 weight-only 약 320 MB.
- Text encoder ONNX 추가:
scripts/export_text_encoder_onnx.py 신규. FP32 473 MB / INT8 229 MB. OnnxTextEncoder로 PyTorch와 인터페이스 통일 (변경 7).
- 실행 모드:
run.py / --onnx / --int8. 모드별 InferenceBundle이 image+text encoder 쌍 로드.
- NPU 검증: AI Hub Snapdragon S23/S24/S25에 ONNX 직접 컴파일. 1014/1014 layer 100% Hexagon NPU 탑재, fallback 0. warm latency 518 / 390 / 374 ms.
검증
PyTorch ↔ ONNX top-3 exact match 4/4. INT8 회귀 cos 0.999. text encoder PyTorch ↔ ONNX cosine 1.00000 (5/5).