2023 드림랜드 해커톤에 참여하며 생각한 것들 (2)

한시온·2023년 9월 9일
1

지난 포스팅에서 식품 이미지로부터 품목명을 식별하는 것과 관련된 고민을 나누었다. 대안으로 OCR 기술을 가장 먼저 떠올렸지만, 식품 포장지에는 수많은 텍스트가 있고 그 중에서 공공 데이터 API의 파라미터로 질의할 품목명을 특정할 수 없다는 점이 고민이었다. 그래서 이미지로부터 품목명을 추출하는 대신, 품목명을 레이블로 하는 이미지 인식 모델을 도입하기로 했다. 대상 이미지를 모델에 넣고 모델에서 예측 레이블이 나오면 그 레이블을 공공 데이터에 질의할 파라미터로 그대로 이용하는 흐름이다. 이미지 샘플이 충분히 확보된다면 가능성이 있다고 생각했다.

준비

다음은 식품 공공 데이터의 응답 결과 일부이다. 여기서 prdlstNm은 품목명에 해당하는 필드로 이것을 이미지의 레이블로 설정할 것이다.

1

이미지 모델 학습을 위해 구글의 Teachable Machine 을 이용한다. 이 플랫폼은 이미지 인식 모델을 비롯한 여러 모델을 매우 손쉽게 학습시킬 수 있다. 특히 웸캠을 이용해 많은 수의 이미지 샘플을 생성하는데 탁월하다. 클래스별로 이미지 샘플을 약 300장 정도 생성하고 각 클래스 이름을 예측할 품목명으로 한다.

2

모델 학습까지 마치고 나면, 미리보기를 통해 학습한 모델을 이용한 이미지 인식 결과를 확인해볼 수 있다. 이미지를 업로드해 확인해보니 의도한 대로 예측이 잘 된다.

3

4

적용

이제 이 모델을 우리 서비스에서 사용할 수 있도록 해야 한다. 학습한 모델을 구글 클라우드에 내보내 저장하고 불러와보자. 최초 한번만 불러오기 위해 라우터 레벨에서 모델을 로드한다.

import * as tmImage from "@teachablemachine/image";

function Router() {
  const [model, setModel] = useState(null);

  useEffect(() => {
    const loadModel = async () => {
      const URL = process.env.REACT_APP_TM_MODEL_URL;
      const modelURL = URL + "model.json";
      const metadataURL = URL + "metadata.json";
      const model = await tmImage.load(modelURL, metadataURL);
      setModel(model);
    };
    loadModel();
  }, []);
  
  // ...
}

모델에 넣을 입력 이미지는 input 태그를 통해 촬영하거나 갤러리에서 선택한 이미지로 한다. 이미지가 선택되면 change 이벤트가 트리거되고 이때 선택된 이미지를 모델의 입력값으로 넣으면 된다. 그런데 모델의 predict() 함수에서 인자로 받는 타입이 HTMLImageElement 이기 때문에, Blob 타입에서 HTMLImageElement 타입으로 변환하는 과정이 필요하다. 타입 변환은 FileReader 를 이용해 데이터 URL로 렌더링을 한 뒤 Image 객체의 src 값으로 넣으면 된다. 예측 결과의 레이블은 className으로 접근할 수 있고 이걸 공공 데이터 API를 호출할 때 품목명에 대한 파라미터로 질의하면 된다 !!

function Camera({ model }) {
  const history = useHistory();

  const onImageChange = async (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    const image = new Image();

    reader.onload = async () => (image.src = reader.result);
    image.onload = async () => {
      const prediction = await model.predict(image, false);

      prediction.sort(
        (a, b) => parseFloat(b.probability) - parseFloat(a.probability)
      );
      history.push(`/Product/${prediction[0].className}`);
    };
    // start reading the file
    reader.readAsDataURL(file);
  };

  return (
    <div style={{ display: "none" }} className="container">
      <input
        type="file"
        accept="image/jpg, image/jpeg, image/png, image/bmp"
        onChange={onImageChange}
      />
    </div>
  );
}

export default Camera;

결과

https://youtube.com/shorts/X5lUMzIIGKM?feature=share

profile
가볍고 무겁게

0개의 댓글