Skip to content

Repository files navigation

2026 동양미래 EXPO — 재활용품 AI 분류 서버

라즈베리파이 5에서 동작하는 FastAPI 기반 AI 분류 서버.
하드웨어(카메라 + 무게센서)로부터 이미지와 무게를 받아 9-class YOLO + 멀티헤드 상태분류기로 판정 후 Spring 서버로 결과를 전송한다.


수신 형식 (하드웨어 → AI 서버)

POST /api/v1/detect
Content-Type: multipart/form-data

필드 타입 필수 설명
image File (jpg/png) 카메라 촬영 이미지
client_id string 사용자/피드백 구분 ID. AI 응답과 Spring 콜백에 그대로 반환
weight_g float 무게센서 값 (g). 미입력 시 무게 이상감지 생략

헤더

헤더 설명
X-API-Key 서버 인증 키 (.envAPI_KEY와 일치해야 함)

요청 예시

curl -X POST http://localhost:8000/api/v1/detect \
  -H "X-API-Key: 인증키" \
  -F "image=@sample.jpg" \
  -F "client_id=hardware-user-001" \
  -F "weight_g=28.0"

응답 형식 (AI 서버 → 하드웨어 / Spring)

{
  "client_id": "hardware-user-001",
  "status": "ALLOWED",
  "classification": {"class_id": 3, "class_name": "plastic", "confidence": 0.94},
  "conditions": {"has_label": false, "is_dented": true},
  "weight": {"value_g": 28.0, "anomaly": false},
  "guidance": [],
  "bbox": [120.0, 80.0, 410.0, 560.0]
}

status 판별자

의미 채워지는 필드
ALLOWED 재활용 허용 classification, conditions, weight, guidance(빈 배열)
REJECTED 거부 조건불충족: guidance / 완전거부(유리 등): rejection
GENERAL_WASTE 일반쓰레기 general
NOT_DETECTED 감지 실패 (없음)

guidance 코드 (REJECTED 재처리 안내)

code 의미
EMPTY_CONTENTS 플라스틱(PET 포함)·캔의 무게 이상 또는 내용물 존재
WEIGHT_ANOMALY 종이·비닐의 무게 이상
FOREIGN_MATERIAL 외부 이물질 제거
REMOVE_LABEL 라벨 제거 (페트·플라스틱)
COMPRESS 압착 (페트·캔)

conditions.has_foreign_material은 외부 JSON에 보내지 않는다. 향후 foreign_material 헤드가 포함된 모델을 탑재하면 FOREIGN_MATERIAL guidance 코드로만 전달한다.

rejection 코드 (완전 거부)

code 의미
GLASS 유리
BATTERY 건전지
FLUORESCENT 형광등
STYROFOAM 스티로폼

general 코드 (일반쓰레기)

code 의미
VINYL 비닐
LOW_CONFIDENCE 신뢰도 미달
UNCLASSIFIED 미분류

메인 모델은 PET와 플라스틱을 별도로 감지하지만 API 응답과 Spring 콜백에서는 모두 class_id=3, class_name=plastic으로 통합한다. 비닐은 정확히 판정되고 상태 조건을 충족한 경우에만 class_id=5, class_name=vinyl, status=ALLOWED로 비닐함 투입을 허용한다. 저신뢰·미분류는 계속 GENERAL_WASTE다.


Spring 콜백

하드웨어 응답 직후 백그라운드로 Spring 서버에 client_id를 포함한 동일한 JSON을 POST한다. .envSPRING_CALLBACK_URL 미설정 시 전송하지 않는다. 타임아웃·연결 오류·HTTP 408/425/429/5xx는 지수 백오프로 최대 3회 재시도하며, HTTP 4xx 계약 오류는 중복 요청을 피하기 위해 재시도하지 않는다.

콜백 URL: https://oneexpo.kro.kr/api/v1/feedback-detail/result


환경 설정 (.env)

API_KEY=인증키
SPRING_CALLBACK_URL=https://oneexpo.kro.kr/api/v1/feedback-detail/result
SPRING_TIMEOUT_SEC=3.0
SPRING_MAX_ATTEMPTS=3
SPRING_RETRY_BACKOFF_SEC=0.5
# 선택
MAIN_MODEL_PATH=weights/yolo26m_best_ncnn_model
STATE_MODEL_PATH=weights/multihead.onnx
VERIFIER_MODEL_PATH=weights/verifier_qwen35_mnv3_v1.onnx
VERIFIER_SHADOW_ENABLED=true
VERIFIER_SHADOW_LOG_PATH=logs/verifier_shadow.jsonl
VINYL_CORRECTION_ENABLED=true
VINYL_CANDIDATE_CONF=0.10
VINYL_CANDIDATE_IOU=0.70
VINYL_CANDIDATE_RATIO=0.40
VINYL_VERIFIER_CONF=0.65
VINYL_VERIFIER_MARGIN=0.25
DETECT_CONF=0.25
TRUST_CONF=0.55
WEIGHT_ANOMALY_ENABLED=true
# 결과 로깅 (Spring 없이도 logs/results.jsonl 에 저장)
LOG_RESULTS=true
LOG_DIR=logs
# 재학습/오인식 검수용 원본 이미지 + 판정 JSON 저장
CAPTURE_REQUESTS=true
CAPTURE_DIR=logs/captures
CAPTURE_RETENTION_DAYS=90
CAPTURE_MAX_STORAGE_MB=10240

저신뢰 PET/플라스틱 결과는 같은 bbox에 비닐 보조 후보가 있고 crop 검증기도 비닐을 충분한 신뢰도와 차이로 지지할 때만 vinyl(class_id=5)로 교정한다. 고신뢰 YOLO 결과나 비닐 보조 후보가 없는 투명 플라스틱 용기는 검증기 단독 판단으로 덮어쓰지 않는다.

결과 로그 (logs/results.jsonl)

LOG_RESULTS=true(기본값)이면 Spring 콜백 여부와 무관하게 판정 결과를 JSONL 형식으로 기록한다.
SPRING_CALLBACK_URL 미설정 시 로그만 저장하므로 Spring 서버 없이도 결과를 확인할 수 있다.

{"timestamp":"2026-07-03T10:00:00+00:00","client_id":"hardware-user-001","status":"ALLOWED","classification":{"class_id":3,"class_name":"plastic",...},...}

Spring 전송 결과는 logs/callbacks.jsonlclient_id, 시도 횟수, HTTP 상태와 함께 delivered/retry/failed로 기록한다. 따라서 AI 판정 성공과 Spring 수신 성공을 별도로 확인할 수 있다.

요청 이미지와 판정 캡처 (logs/captures/)

CAPTURE_REQUESTS=true이면 정상적으로 판정된 요청마다 원본 이미지와 판정 JSON을 같은 capture_id로 저장한다. Docker Compose에서는 ./logs:/app/logs 볼륨을 사용하므로 컨테이너를 다시 만들어도 파일이 유지된다.

logs/captures/2026-07-31/
  20260731T012345123456Z_a1b2c3d4e5f6.jpg
  20260731T012345123456Z_a1b2c3d4e5f6.json

JSON에는 요청의 client_id와 무게, 예측 클래스·신뢰도·bbox·상태, 이미지 SHA-256과 아래 검수 필드가 포함된다. API 키와 요청 헤더는 저장하지 않는다.

{
  "review": {
    "is_correct": null,
    "expected_class": null,
    "is_single_object": null,
    "is_dented": null,
    "has_label": null,
    "has_foreign_material": null,
    "notes": null
  }
}

기본 보존 기간은 90일, 최대 용량은 10GB이며 초과 시 오래된 이미지/JSON 쌍부터 제거한다. 운영 캡처는 자동 teacher의 tight/context 합의와 확신도 기준을 통과한 경우에만 hard sample로 재학습 데이터에 추가한다. 합의 실패·저신뢰·다중 객체는 -1로 마스킹해 학습에서 제외하며 사람 검토를 전제로 하지 않는다.

임시 crop 검증기 shadow 로그

VERIFIER_SHADOW_ENABLED=true이면 기존 YOLO가 만든 bbox를 임시 320px 검증기로 비동기 재검증하고 logs/verifier_shadow.jsonl에 YOLO/검증기 품목 일치 여부와 압착·라벨·외부 이물질 출력을 기록한다. 이 결과는 초기에는 API 응답, guidance, Spring 콜백을 변경하지 않는다. 임시 모델의 운영 분포 정확도를 확인한 뒤에만 판정에 사용한다.

crop 검증기 학습에는 원본 정답이 단일 객체이고 자동 teacher도 단일 주 객체로 판정한 이미지만 사용한다. labelforeign_material은 서로 독립된 정답이며, 네 조합(둘 다 없음/라벨만/외부 이물질만/둘 다 있음)을 그대로 기록한다.

객체 bbox를 crop한 뒤 9종 품목과 상태를 다시 확인하는 검증기의 확정 구조, 라벨 정책, NAS 실행 명령은 docs/CROP_VERIFIER_PLAN.md에 정리했다. 1일차 prototype은 고해상도 원본을 복제하지 않고 경로+bbox만 참조하며, 320px crop 생성 → 기존 naco-ollama와 동일 모델 볼륨을 공유하는 두 개의 qwen3.5:9b-q4_K_M 인스턴스로 적응형 tight/context 상태 pseudo-label을 만드는 순서로 진행한다. 384px tight 한 장의 1차 판정이 확실한 정상 샘플이면 종료하고, 양성·불확실 샘플과 10% 정상 감사 샘플만 640px wider-context 한 장으로 2차 합의를 수행한다. 서로 다른 두 시야의 판정을 비교하되 같은 tight 이미지를 재전송하지 않는다. 사람 검토는 두지 않는다. NAS 여유 공간 500GB와 새 crop 20GB 상한을 통과해야 전체 정제를 계속한다. label head를 사용하지 않는 can/paper/vinyl의 label_only는 2차 강제 대상에서 제외하고 정상 감사 표본에만 포함한다. 외부 이물질과 PET/plastic 라벨은 항상 2차로 확인한다. teacher 응답은 생성 시간을 줄이기 위해 decision으로부터 계산 가능한 label/foreign boolean을 보내지 않고 짧은 wire key와 근거 enum만 사용하지만, JSONL에는 기존 decision/has_removable_label/has_true_foreign_material 계약으로 복원해 저장한다. Ollama는 연속 이미지 prompt cache를 지원하는 0.32.0과 인스턴스당 8K context를 사용한다. Qwen3.5는 한 Ollama 서버의 parallel slot을 지원하지 않아 두 독립 서버에 worker를 하나씩 고정하며, 모델 파일은 중복 저장하지 않는다. 최종 v7은 2026-08-03 00:00 KST에 50,000건을 모두 처리했다. 자동 수용은 46,913건(93.826%), teacher 오류는 46건(0.092%), 무효 행은 0건이다. 이 결과에서 9종을 품목별 최대 10,000장으로 균형 선별한 90,274장과 실제 하드웨어 crop 103장을 결합해 MobileNetV3-Small 검증기를 완성했다. 1차 5배 학습 뒤 하드웨어 training 행을 증강 포함 100배(전체 training의 약 8%)로 높여 저학습률 보정했고, validation 행은 항상 한 번만 평가했다. 고정 하드웨어 holdout의 외부 계약 기준(PET→plastic 통합) 정확도는 기존 42.86%에서 74.29%, macro-F1은 0.598에서 0.679로 개선됐다. 기존 YOLO epoch 40은 bbox 검출기로 계속 유지하며 Spring 콜백 계약도 바꾸지 않는다. 외부 이물질은 실기기 양성 holdout이 생기기 전까지 런타임 판정에 활성화하지 않는다.


실행

pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000

API 문서: http://localhost:8000/docs

배포

main에 푸시하면 GitHub Actions에서 테스트를 실행한다. Pi5의 ai-autodeploy.timer가 5분마다 origin/main의 새 커밋을 확인하고 docker compose up -d --build로 자동 배포한다.

About

2026 동양미래 expo AI 레포지토리입니다

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages