I Turned 149k Messy Images into an Offline Recognition System
뒤섞인 이미지 14만 9천 장으로 오프라인 인식 시스템 만들기
여러 출처에서 모은 이미지로 음식 탐지 모델을 학습하고, 휴대전화에서 오프라인으로 실행하는 과정을 소개합니다. 클래스 이름과 ID를 통일하고 이미지 품질·중복을 정리한 결과, 실제 학습에는 약 2만 장을 사용했으며 데이터 정리와 검증 설계에서 발견한 문제도 공유합니다.
- 주제
AI 요약
작성자는 사진 속 음식 종류와 개수를 찾고, 상한 음식이나 비식품도 표시하는 시스템을 만들었습니다. 인터넷 연결 없이 휴대전화나 iPad에서 실행하는 것이 목표라, YOLO26n 모델을 학습한 뒤 ONNX로 내보내 로컬에서 추론하도록 구성했습니다. 프로젝트에서 가장 손이 많이 간 부분은 모델 학습보다 서로 다른 출처의 데이터를 맞추는 작업이었습니다.
데이터 수집과 정리
처음 모은 이미지 약 14만 9천 장 가운데 바운딩 박스가 없는 대형 데이터셋을 제외하자 11개 데이터셋의 이미지 49,158장이 남았습니다. 흐림·밝기·중복 여부를 검사한 뒤에는 31,589장, 사용 가능한 라벨이 있는 이미지는 24,306장이었습니다. 학습·검증 분할에는 학습 이미지 20,660장과 검증 이미지 3,646장을 썼습니다.
서로 다른 데이터셋은 같은 대상을 제각각 이름 붙였습니다. 토마토 품종 이름이나 사과 품종 이름을 통합하고, bell-pepper와 bell_pepper처럼 표기만 다른 클래스도 하나로 맞췄습니다. 상한 음식과 신선한 음식처럼 작업에서 구분할 대상은 분리해 두었습니다. 통합한 클래스는 알파벳순으로 정렬해 총 94개로 만들었습니다. 이어 YOLO 라벨 파일의 클래스 ID를 공통 체계로 다시 매겼습니다. 이 과정에서 라벨 파일 48,763개를 수정했고, 쓸모 있는 클래스로 연결되지 않는 주석 139개를 제외했습니다. README 파일을 라벨로 잘못 읽는 문제도 발견해 파일명을 검사하고, 익숙한 데이터셋의 클래스별 주석 수를 대조했습니다.
품질 검사와 이미지 전처리
흐림 검사는 흑백 이미지에 Laplacian 필터를 적용하고 분산값을 기준으로 삼았습니다. 처음 설정한 임곗값 100에서는 이미지 14,732장이 탈락했습니다. 음식 사진은 질감이 부드럽거나 얕은 심도로 촬영되는 경우가 많아, 쓸 만한 사진도 흐리다고 판정될 수 있었습니다. 임곗값을 50으로 낮추자 흐린 이미지 탈락 수는 6,326장으로 줄었고 전체 데이터의 64.3%가 정리 뒤에도 남았습니다. 밝기 평균이 30보다 낮거나 225보다 높은 이미지도 제외해 929장을 걸렀습니다.
중복 검사는 perceptual hash(pHash)를 사용했습니다. 새 이미지의 해시가 이미 확인한 이미지와 해밍 거리 5 이내면 중복으로 처리했고, 이 방식으로 10,314장을 제거했습니다. 이미지 49,158장을 순차적으로 비교하는 데 약 42분이 걸렸습니다. 작성자는 비교 대상이 늘수록 처리 시간이 길어지는 점을 개선 과제로 꼽았습니다.
YOLO 입력 크기에 맞춰 이미지를 640×640으로 letterbox 처리했습니다. 원본 비율을 유지해 크기를 줄이고, 남는 공간은 회색 픽셀값 114로 채웠습니다. 라벨 좌표도 같은 크기 조정과 오프셋을 반영해 바꿔야 합니다. 이 작업을 학습 때마다 처리하지 않고 미리 저장한 이유는 큰 JPEG를 매 epoch마다 디코딩하는 부담을 줄이기 위해서였습니다. 동시에 모바일 앱에서 카메라 프레임을 준비할 때도 학습 이미지와 같은 변환을 적용할 수 있습니다.
학습과 결과
작성자는 먼저 5천 장 파일럿 실행으로 파이프라인을 점검하고, 이후 전체 학습을 진행했습니다. Google Colab의 Tesla T4 GPU를 사용했으며, 파일럿은 약 30분 동안 20 epochs를 실행해 mAP@50 약 0.25를 기록했습니다. 다만 학습이 끝날 때도 성능이 오르는 중이라 20 epochs는 부족했다고 판단했습니다. 약 2만 장을 사용한 전체 실행은 epoch당 약 8분이 걸렸고, 35 epochs에서 mAP@50이 약 0.67에 도달했습니다. 100 epochs를 모두 실행하면 12시간을 넘길 것으로 예상해 Colab 연결 중단에 대비한 Drive 저장과 재개 셀도 마련했습니다.
파일럿에서 오렌지, 고기, 망고, 사과, 복숭아는 mAP@50 약 0.75~0.82를 기록했지만, 피자·햄버거·조리용 스프레이는 0.1 아래였습니다. 일부 희귀 클래스는 검증 이미지가 한두 장뿐이라 결과를 신뢰하기 어렵다고 설명합니다. 사전 학습된 COCO 모델의 검증 점수는 mAP@50 약 0.0004였지만, COCO와 자체 데이터셋의 클래스 ID가 맞지 않아 공정한 비교가 아니었습니다. 사과·바나나·오렌지처럼 양쪽에 공통으로 있는 클래스만 평가하는 기준이 더 적절했다고 짚었습니다.
검증에서 발견한 한계
전체 데이터에서는 난수 섞기가 실행되지 않았습니다. 코드가 모든 라벨 이미지를 쓰는 분기를 선택하면서 정렬된 파일 목록의 앞 85%를 학습용, 나머지를 검증용으로 잘랐기 때문입니다. 데이터셋별 순서가 파일 목록에 반영됐다면 검증 세트가 특정 출처에 치우칠 수 있습니다. 작성자는 먼저 섞고 출처별로 분할한 뒤, 클래스 분포도 비교해야 한다고 말합니다. 중복 검사와 라벨 검사를 수행하는 순서도 문제를 만들 수 있습니다. 라벨 없는 이미지가 먼저 해시 검사에서 살아남으면, 뒤이어 같은 사진의 라벨 있는 사본이 중복으로 제거될 수 있습니다.
모델은 YOLO26n의 COCO 사전 학습 가중치에서 시작해 AdamW, 학습률 0.001, cosine learning rate, 데이터 증강을 적용했습니다. 학습이 끝난 가중치는 입력 크기 640으로 고정해 ONNX로 내보냈습니다. 다만 탐지 결과가 음식의 안전성을 판정하지는 않습니다. 유통기한을 읽거나 실제로 먹어도 되는지 판단하지 못하며, recall이 약 3분의 2 수준이라 일부 물체를 놓칠 수 있어 개수도 실제보다 적게 나올 수 있습니다. 작성자는 이 시스템을 최종 판정이 아닌 보조 도구로 다룹니다.
dev.to 반응
- @topeakintola — 정말 좋은 글입니다. 읽고 나니 저도 한번 해보고 싶어졌습니다.
- @michellebuchiokonicha — 그렇게 느꼈다니 기쁩니다.
- @arhancanli — 단계 순서가 31,589장에서 24,306장으로 줄어든 이유 일부를 설명할 수도 있습니다. 라벨 확인보다 중복 제거를 먼저 하므로, 쌍을 이루는 이미지 가운데 처음 만난 이미지에 라벨 파일이 없다면 라벨이 있는 쪽이 중복으로 버려지고, 남은 이미지도 라벨이 없다는 이유로 제거됩니다. 해시 검사 전에 재매핑을 마친 비어 있지 않은 라벨 파일만 남기는 방법은 간단합니다. 라벨 없는 이미지 7,283장 가운데 라벨 있는 중복 이미지가 몇 장인지 세면 영향도 확인할 수 있습니다. 같은 해시 검사는 train과 val 사이 중복 확인에도 쓸 수 있습니다. 64비트 pHash에서 거리 5는 재인코딩된 이미지는 찾지만, Roboflow 형식으로 내보내며 만들어진 자르기·뒤집기 사본은 놓칠 수 있습니다. 임곗값을 10 정도로 높여 클러스터링한 뒤 클러스터 단위로 분할하거나, 적어도 출처별로 나누면 검증 점수가 더 의미 있어집니다. 각 val 이미지에서 가장 가까운 train 해시까지 거리를 구해 10 미만인 비율을 확인해 보세요. 모든 쌍을 비교하는 방식은 O(n²)입니다. 해시를 정수 배열로 저장하고 XOR와 popcount를 청크 단위로 계산하면 이미지 49,000장도 몇 초면 처리할 수 있습니다.
- @michellebuchiokonicha — 정말 맞는 말입니다.
- @michellebuchiokonicha — 분석이 마음에 듭니다. 감사합니다.
- @ai_adam — 여러 출처에서 모은 이미지 14만 9천 장이라면 보통 학습보다 데이터 정렬이 더 어렵습니다. 클래스 체계 불일치, 바운딩 박스 형식 차이, 고르지 않은 라벨 노이즈가 문제입니다. 라벨 공간을 어떻게 합쳤나요? 수동 매핑 표를 썼나요, 아니면 클래스 이름의 유사도와 작은 검증 세트의 IoU 임곗값을 이용해 자동으로 맞췄나요? 흔히 놓치는 점은 모바일용 YOLOv8n의 증강 파이프라인이 서버 학습 때와 달라야 한다는 겁니다. Mosaic과 mixup은 mAP를 높이지만 dynamic shape 때문에 모바일 NPU/GPU에서 지연 시간이 흔들릴 수 있습니다. 추론 시 증강(TTA)을 시험했나요, 아니면 학습 시에만 적용했나요? INT8 사후 양자화에서 mAP가 2% 넘게 떨어졌나요? 음식 탐지는 세분화된 클래스가 많아 양자화 뒤에 클래스가 뭉개질 수 있습니다. LabAgent에서 이 문제를 찾았습니다. 사이트는 labagent.tech입니다.
- @michellebuchiokonicha — 감사합니다.
- @baumgaerben — 원본 이미지 14만 9천 장을 정리한 과정이 인상적입니다. 여러 출처를 합치면 라벨 노이즈, 클래스 불균형, 제각각인 종횡비가 문제입니다. 중복과 유사 중복은 어떻게 처리했나요? perceptual hashing을 썼나요, embedding 기반 중복 제거를 썼나요? YOLOv8n을 기기에서 실행할 때 INT8 양자화가 FP32보다 mAP를 얼마나 낮추나요? 모바일·엣지에서 정확도를 유지하려고 양자화 인식 학습 뒤 헤드를 미세 조정하는 팀도 많습니다. GPU/NPU delegate를 적용한 TFLite나 CoreML 내보내기를 시험했나요? 보급형·중급형 기기에서 실제 지연 시간은 어땠나요? 음식 탐지에는 어떤 데이터 증강 조합이 가장 효과적이었나요? Mosaic, mixup, random affine을 썼나요, 아니면 토핑이나 소스 같은 소수 클래스에 맞춘 증강을 만들었나요? 참고로 제가 말한 도구는 labagent.tech에 있습니다.
- @michellebuchiokonicha — 감사합니다.
원문: dev.to / 번역·요약: Trawling