FreshGuard – YOLOv8 × EfficientNet-B0로 구현한 과일 신선도 판별 시스템

성주(Seongju)·2025년 11월 26일

프로젝트

목록 보기
3/13
post-thumbnail

안녕하세요. 이번 글에서는 2025년에 진행 중인 과일 신선도 판별 프로젝트 FreshGuard의 AI 모델(YOLOv8 × EfficientNet-B0) 부분을 정리해 보려고 합니다.

“냉장고 문 열고, 먹어도 될지 고민하는 시간 줄여보자.”

FreshGuard과일 사진 한 장으로 과일 종류 + 신선도를 판별하는 프로젝트입니다.
이 글에서는 그중에서 FreshGuardAI 모델 파트를 중심으로 정리해 보려고 합니다.

  • 전체 팀 구성

    • 나는: EfficientNet-B0 기반 멀티태스크 모델 설계/학습 + YOLOv8n 파이프라인 실험
    • 팀원들: 안드로이드 앱 / Flask + MySQL 서버 개발
  • 이 글은 AI 모델 파트가 중심이고,
    마지막에 앱·서버 연동 구조도 간단히 정리하였습니다.

  • 코드와 로그는 여기 정리해 두었습니다. → GitHub – FreshGuard


1. 프로젝트 개요

FreshGuard가 목표로 하는 기능은 다음과 같습니다.

  • 입력: 과일 이미지 1장

  • 출력

    • 과일 종류: 10-class
    • 신선도: 3단계(fresh / normal / rotten)
  • 최종 목표

    • 나중에 앱에서 사진만 찍으면,

      • “이건 버려야 함”
      • “빨리 먹는 게 좋음”
      • “아직 여유 있음”
        정도의 안내를 바로 볼 수 있도록 만드는 것

이번 글은 모델에 집중하여 정리했고, 팀원의 앱과 서버도 간단하게 기록하였습니다.


2. 문제 정의 – 왜 이 문제를 선택했나

개인적으로 떠올렸던 키워드는 1인 가구 + 음식물 쓰레기였습니다.

  • 한 번에 많이 사 두고, 제대로 관리가 안 돼서 냉장고 안에서 썩어버리는 음식들
  • 유통기한은 남았는데 겉모습이 애매해서 그냥 버리는 경우
  • 반대로 애매하게 상했는데 “아직 괜찮겠지” 하고 먹는 경우

한국농촌경제연구원 자료에서도
1인 가구가 부적절한 보관으로 상한 음식물 비율이 높다는 내용이 나옵니다.

결국 이런 상황으로 정리할 수 있다.

“지금 이 과일 상태가 어느 정도인지, 눈으로만 보기에 애매하다.”

여기서 출발한 아이디어는 비교적 단순하다.

  • 과일을 카메라로 찍고

  • AI 모델이 과일 종류 + 신선도를 추정한 뒤

  • 그 결과를 바탕으로

    • 소비 우선순위
    • 폐기 추천
    • 레시피 추천

까지 연결하는 것을 목표로 했다.

이번 글에서는 그중에서도 신선도 판별 AI 모델과 추론 파이프라인에 집중해서 정리했고, 레시피 추천은 이후 버전에서 확장할 방향으로만 설계해 둔 상태다.


3. 서비스 구조 구상 & 현재 구현 상태

최종적으로 FreshGuard가 목표로 하는 흐름은 다음과 같다.

  1. 안드로이드 앱에서 과일 사진 촬영
  2. 서버(Flask + MySQL, Ubuntu)로 이미지 업로드
  3. 서버에서 YOLOv8n + EfficientNet-B0 멀티태스크 모델로
    • 과일 종류(10-class)
    • 신선도(fresh / normal / rotten)
      을 추론
  4. 앱에서 결과를 “버려야 함 / 빨리 먹는 게 좋음 / 여유 있음” 식으로 표시

현재 1차 버전에서는 다음까지 구현한 상태다.

  • 나는: 데이터 전처리 + EfficientNet-B0 멀티태스크 학습 + YOLOv8n 파이프라인 설계
  • 팀원: 안드로이드 앱 개발 + Flask 서버 / REST API / MySQL 연동

이 글에서는 이 중에서 AI 모델(EfficientNet-B0 멀티태스크)와 추론 파이프라인에 초점을 맞춰 정리한다.


4. 데이터셋 설계

4-1. 데이터 소스 & 규모

과일 종류와 신선도 라벨이 함께 들어 있는 구조라
처음부터 멀티태스크 모델로 접근하기 좋은 데이터셋이었다.

4-2. 클래스 구성

과일 클래스 (10종)

apple, banana, bell_pepper, carrot, cucumber,
mango, orange, potato, strawberry, tomato

신선도 클래스 (원본 라벨)

fresh / normal / rotten

이미지를 직접 쭉 확인해보면,

  • freshrotten은 비교적 구분이 잘 되지만
  • normal은 두 쪽(fresh/rotten)의 경계에 걸쳐 있는 경우가 많다

라벨 자체가 애매한 상태에서 3-class를 그대로 쓰면
모델이 애매한 경계를 억지로 나누느라 학습이 꼬일 수 있다고 판단했다.

그래서 신선도는 다음처럼 바꿔서 사용했다.

  • 학습 시: fresh vs rotten 2-class로 단순화
  • 추론 시: p_fresh를 이용해서 다시 3단계로 매핑
if p_fresh ≥ 0.7 → fresh
if p_fresh ≤ 0.3 → rotten
그 사이        → normal

완벽한 라벨이 아니라고 생각했기 때문에,

  • 극단(fresh/rotten)은 명확하게 잡고
  • 중간 구간은 normal로 모으는 방식으로 설계했다.

4-3. 전처리 & 데이터 증강

  • Input size: 224 × 224

  • Normalize: ImageNet mean / std

  • Augmentation

    • Random Horizontal Flip
    • Random Rotation
    • ColorJitter (밝기/대비/채도 조정)

실제 환경에서 과일이 찍힐 때 생기는
각도, 조명, 약간의 위치 차이 정도는 버틸 수 있도록 설정했다.


5. 모델 아키텍처 – EfficientNet-B0 멀티태스크

모델의 백본은 EfficientNet-B0 (ImageNet pretrained) 하나로 고정했다.
그 위에 Head를 두 개 얹어 멀티태스크 분류 형태로 구성했다.

  • Backbone

    • EfficientNet-B0
  • Head 1 – 과일 종류 (Head_fruit)

    • 출력: 10-class softmax
  • Head 2 – 신선도 (Head_fresh)

    • 출력: 2-class softmax (fresh / rotten)

Loss는 두 태스크의 loss를 단순 합으로 사용했다.

loss = loss_fruit + loss_fresh

과일 종류와 신선도는 둘 다 같은 이미지를 기반으로 한 판단이라

  • 피처를 공유하는 멀티태스크 구조가

    • 파라미터 효율도 좋고
    • 일반화 측면에서도 괜찮을 거라고 보고 설계했다.

6. 학습 세팅 & 로그 정리

중요한 학습 설정만 정리하면 다음과 같다.

  • 사용 이미지 수: 69,105장

  • Epoch: 7

  • Best Epoch: 6

    • val_f1_fruit + val_f1_fresh 합이 최대였던 시점
  • Best weight 경로

    • models/efficientnet_b0_freshguard_multitask.pt

로그 관리 방식:

  • 텍스트 로그: logs/log.txt
  • 지표/곡선 그래프: logs/*.jpeg

나중에 다시 볼 때도
“어떻게 학습했는지”를 바로 떠올릴 수 있도록
코드 / 모델 / 로그 / 노트북을 폴더 단위로 나눠 두었다.


7. 결과 – F1 스코어와 학습 곡선

7-1. 최종 성능 (Validation 기준)

멀티태스크 EfficientNet-B0 모델의 Validation 결과는 다음과 같다.

  • Fruit F1-score: 0.993
  • Freshness F1-score: 0.981

과일 종류는 거의 다 맞는 수준이고,
신선도도 0.98대면 1차 버전 기준으로는 꽤 안정적으로 나온 편이라고 느꼈다.


7-2. Train Loss Curve

그래프를 보면:

  • 1 epoch 기준: loss가 0.4 초반에서 시작
  • 7 epoch 기준: 0.04 근처까지 점진적으로 감소
  • 중간에 loss가 급격히 튀거나, 이상하게 다시 증가하는 구간은 거의 없음

전체적으로 봤을 때
학습 과정은 비교적 안정적으로 잘 진행된 편이라고 판단했다.


7-3. Validation F1 Curve (fruit / fresh)

  • val_f1_fruit

    • 초반: 약 0.983
    • 후반: 0.991 ~ 0.992 구간에서 수렴
  • val_f1_fresh

    • 초반: 약 0.971
    • 후반: 0.978 ~ 0.982 정도에서 안정화

train과 val 지표를 같이 봤을 때,

  • 대표적인 overfitting 패턴은 크게 보이지 않고

  • 2~3 epoch 이후부터는

    • 이미 “실사용 가능” 수준에 도달한 상태에서
    • 조금씩 성능을 다듬는 구간으로 들어간 느낌에 가깝다.

8. 실제 서비스에서의 사용 흐름

실제 동작 플로우는 다음과 같다.

  1. 앱 (Android, Kotlin)

    • 사용자가 과일 사진을 촬영하거나 앨범에서 선택
    • 이미지를 서버 REST API로 업로드
  2. 서버 (Ubuntu + Flask + MySQL)

    • 업로드된 이미지를 수신
    • YOLOv8n으로 이미지 안의 과일 여러 개를 bounding box로 감지
    • 각 crop을 efficientnet_b0_freshguard_multitask.pt에 넣어서
      • 과일 종류(10종)
      • 신선도(fresh / normal / rotten)
      • 신뢰도
        를 계산
  3. 응답 포맷

    • 예시 JSON 구조:
      [
        {"fruit": "apple", "state": "rotten", "confidence": 0.92},
        {"fruit": "banana", "state": "fresh", "confidence": 0.88}
      ]
  4. 앱 UI 표시

    • 서버 응답을 받아
      • “지금 버려야 할 것”
      • “빨리 먹는 게 좋은 것”
      • “조금 여유 있는 것”
        으로 나눠 보여주고,
    • 필요하면 판별 결과를 로컬/서버 DB에 저장해 히스토리 화면에서 다시 확인할 수 있도록 구성

실제 앱 UI

  1. 앱 메인 – 식재료 검사 진입

    FreshGuard 메인

    보관 중인 식재료와 최근 검사 기록을 한눈에 보고,
    하단 식재료 검사 버튼으로 촬영 화면으로 이동한다.

  2. 식재료 검사 화면


    식재료 검사 화면

    식재료 검사 완료된 화면

    카메라 촬영 또는 갤러리 불러오기를 통해
    신선도 판별에 사용할 이미지를 선택하고,
    결과를 다음 화면에서 확인할 수 있다.

  3. 검사 결과 리스트

    식재료 리스트

    AI가 판별한 결과가 히스토리 형태로 쌓여,
    어떤 식재료를 먼저 소비해야 할지 관리할 수 있다.

  4. 식재료 상세 확인

    식재료 상세

    개별 식재료에 대해 이미지, 촬영 날짜, 신선도 결과,
    소비기한 메모를 함께 관리할 수 있도록 구성했다.

  5. 추천 레시피 확인 – 목록 + 상세


    개별 레시피 목록 화면

    추천 레시피 상세 화면

    레시피 화면

    남아 있는 과일을 활용할 수 있는 레시피를 추천하고,
    목록에서 하나를 선택하면 재료와 조리 순서까지 확인할 수 있다.


9. 한계와 앞으로의 보완 방향

현재 모델에도 분명 한계점이 있다.
지금 기준으로 생각하고 있는 부분은 아래와 같다.

  1. 데이터 도메인 갭

    • Kaggle 이미지와 실제 집/마트/편의점 환경은 다를 수밖에 없다.
    • 실제 촬영 데이터로 평가해 보면
      성능이 더 낮게 나올 가능성이 있다.
  2. normal 라벨의 애매함

    • 사람마다 “이 정도면 먹어도 된다”에 대한 기준이 다르다.
    • 현재는 p_fresh 0.3~0.7 구간을 전부 normal로 묶고 있어서,
      사용자 입장에서 느끼는 기준과 완전히 일치하지 않을 수 있다.
  3. 카테고리 확장

    • 지금은 과일만 다루고 있다.

    • 냉장고 전체 관리로 확장하려면

      • 야채
      • 육류
      • 조리/가공 식품
        등으로 데이터 설계부터 다시 시작해야 한다.

앞으로 보완하고 싶은 방향은 다음과 같다.

  • 실제 촬영 데이터(집/마트/편의점 등)로 에러 케이스 수집

  • 수집된 케이스를 기반으로

    • p_fresh threshold 재조정
    • normal 구간 재설계
  • 앱/서버와 연결된 상태에서 실제 사용 피드백을 받아 보고,
    필요하다면 추가적인 fine-tuning이나 domain adaptation 진행


10. 마무리 – 이번 프로젝트에서 정리한 점들

FreshGuard 모델 버전 1을 만들면서,
개인적으로 정리된 포인트는 다음 세 가지였다.

  1. 라벨이 애매하면, 문제 정의부터 다시 정리하는 게 낫다.

    • 애매한 3-class를 그대로 쓰기보다는
      2-class + 후처리가 이번 데이터셋에서는 훨씬 다루기 편했다.
  2. 멀티태스크가 더 자연스러운 문제라면, 한 번에 묶어서 보는 것도 좋다.

    • 과일 종류와 신선도를 같이 학습하니
      데이터 효율과 성능 측면에서 모두 나쁘지 않았다.
  3. 결과뿐 아니라, 과정과 구조를 같이 남겨두는 게 중요하다.

    • 나중에 다시 개선할 때를 생각해서
      docs/, logs/, notebooks/ 등으로 레포 구조를 나눠 두었다.
    • “어디를 손봐야 할지”를 나중에 다시 볼 때도 금방 떠올릴 수 있도록 만드는 느낌에 가깝다.

앱/서버까지 붙어서 실제로
“냉장고 정리 도우미”에 가까운 형태가 되면,

  • 실사용 피드백
  • 에러 케이스 모음
  • 그걸 반영한 v2/v3 개선 과정

까지 한 번 더 정리해 볼 생각이다.

여기까지가

FreshGuard – YOLOv8 × EfficientNet-B0로 구현한 과일 신선도 판별 시스템 v1을 만든 기록

입니다.
혹시 비슷한 문제를 고민 중이거나,
코드가 궁금한 분들은 레포를 참고해 보셔도 좋을 것 같습니다. 😊

긴 글 읽어주셔서 감사합니다 :)

profile
프로젝트 및 해커톤 활동하는 오리

0개의 댓글