[ React ] OpenCV.js 객체인식 사용해보기

시훈·2024년 6월 7일
0
post-thumbnail

0. OpenCV란?

(출처: OpenCV.ai 공식 사이트)

OpenCV(Open Source Computer Vision Library)란 컴퓨터 비전과 머신 러닝을 위한 오픈 소스 라이브러리로, 얼굴 인식, 객체 추적, 자율 주행 자동차의 차선 인식, 그리고 의료 영상 분석 등에서 주로 사용됩니다.
최근 알약 판별 프로젝트를 진행하던 중, 알약을 인식하는 기능이 필요하게 되어 객체인식에 대해 찾아보다가 OpenCV를 발견하였습니다.

1. OpenCV를 React 프로젝트에 불러오기

프로젝트의 public/index.html 파일에 CDN을 이용하여 추가해줍니다.

<head>
	.
	.
	.
	<script async src="https://docs.opencv.org/4.x/opencv.js"></script>
</head>

2. 사진 업로드 컴포넌트 생성

src/components/ImageUpload.js

import React, { useState } from 'react';

const ImageUpload = ({ onImageUpload }) => {
    const [image, setImage] = useState(null);
    const handleImageChange = (e) => {
        const file = e.target.files[0];
        if (file) {
            const reader = new FileReader();
            reader.onloadend = () => {
                setImage(reader.result);
                onImageUpload(reader.result);
            };
            reader.readAsDataURL(file);
        }
    };

    return (
        <div>
            <input type="file" onChange={handleImageChange} />
            {image && <img src={image} alt="Uploaded" width="400" />}
        </div>
    );
};
export default ImageUpload;

3. 객체 인식 컴포넌트 생성

src/components/ObjectDetection.js

import React, { useRef, useEffect, useState } from 'react';

const ObjectDetection = ({ imageSrc }) => {
    const originalCanvasRef = useRef();
    const grayCanvasRef = useRef();
    const blurredCanvasRef = useRef();
    const sharpenedCanvasRef = useRef();
    const edgesCanvasRef = useRef();
    const finalCanvasRef = useRef();
    const [detectionResult, setDetectionResult] = useState('');

    useEffect(() => {
        if (imageSrc) {
            const img = new Image();
            img.src = imageSrc;
            img.onload = () => {
                detectPill(img);
            };
        }
    }, [imageSrc]);

    const detectPill = (img) => {
        const cv = window.cv;

        // 이미지 로드 및 처리
        const src = cv.imread(img);
        const gray = new cv.Mat();
        const blurred = new cv.Mat();
        const sharp = new cv.Mat();
        const edges = new cv.Mat();
        const thresh = new cv.Mat();
        const opening = new cv.Mat();
        const contours = new cv.MatVector();
        const hierarchy = new cv.Mat();

       // 원본 이미지 그리기
        cv.imshow(originalCanvasRef.current, src);

        // 그레이스케일 변환
        cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
        cv.imshow(grayCanvasRef.current, gray);

        // 블러 적용
        cv.bilateralFilter(gray, blurred, 9, 75, 75);
        cv.imshow(blurredCanvasRef.current, blurred);

        // 샤프닝 적용
        const kernel = cv.matFromArray(3, 3, cv.CV_32F, [-1, -1, -1, -1, 9, -1, -1, -1, -1]);
        cv.filter2D(blurred, sharp, cv.CV_8U, kernel);
        cv.imshow(sharpenedCanvasRef.current, sharp);

        // Canny 외곽선 추출
        cv.Canny(sharp, edges, 50, 100);
        cv.imshow(edgesCanvasRef.current, edges);

        // 이진화
        cv.threshold(edges, thresh, 150, 255, cv.THRESH_TOZERO);

        // 컨투어 검출
        cv.findContours(thresh, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);


        // 컨투어를 사용하여 알약 감지
        let foundPill = false;
        for (let i = 0; i < contours.size(); i++) {
            const contour = contours.get(i);
            const area = cv.contourArea(contour);
            const rect = cv.boundingRect(contour);
            const aspectRatio = rect.width / rect.height;

            // 면적 및 비율 기준으로 필터링
            if (area > 500 && aspectRatio > 0.5 && aspectRatio < 2) {
                const color = new cv.Scalar(0, 255, 0);
                cv.rectangle(src, new cv.Point(rect.x, rect.y), new cv.Point(rect.x + rect.width, rect.y + rect.height), color, 2);
                foundPill = true;
            }
        }

        // 최종 결과 그리기
        cv.imshow(finalCanvasRef.current, src);

        // 메모리 해제
        src.delete();
        gray.delete();
        blurred.delete();
        sharp.delete();
        edges.delete();
        thresh.delete();
        opening.delete();
        contours.delete();
        hierarchy.delete();

        // 감지 결과 업데이트
        if (foundPill) {
            setDetectionResult("알약 감지 성공");
        } else {
            setDetectionResult("알약 감지 실패");
        }
    };

    return (
        <div>
            <div style={{
                display: 'flex',
                justifyContent: 'space-evenly'}}>
                <div>
                    <h2>Original Image</h2>
                    <canvas ref={originalCanvasRef}></canvas>
                </div>
                <div>
                    <h2>Gray Image</h2>
                    <canvas ref={grayCanvasRef}></canvas>
                </div>
                <div>
                    <h2>Blurred Image</h2>
                    <canvas ref={blurredCanvasRef}></canvas>
                </div>
                <div>
                    <h2>Sharpened Image</h2>
                    <canvas ref={sharpenedCanvasRef}></canvas>
                </div>
                <div>
                    <h2>Edges Image</h2>
                    <canvas ref={edgesCanvasRef}></canvas>
                </div>
                <div>
                    <h2>Final Detection</h2>
                    <canvas ref={finalCanvasRef}></canvas>
                    <h2>{detectionResult}</h2>
                </div>
            </div>
        </div>
    );
};

export default ObjectDetection;

저는 알약 인식에 초점을 맞추어 다양한 함수(필터)들을 적용해 보았으며, 파라미터들을 조정해가며 정확도를 높여 갔습니다.

사용한 필터들

  • cvtColor(src, gray, cv.COLOR_RGBA2GRAY)
    역할: 컬러 이미지를 그레이스케일 이미지로 변환함으로써, 이후의 이미지 작업을 단순화 시킴
    주요 파라미터:
    • src: 입력 이미지
    • gray: 출력 이미지 (그레이스케일 이미지)
    • cv.COLOR_RGBA2GRAY: 색상 변환 코드

  • bilateralFilter(gray, blurred, 9, 75, 75)
    역할: 윤곽선을 유지하며 노이즈를 제거
    주요 파라미터:
    • gray: 입력 이미지
    • blurred: 출력 이미지 (블러링된 이미지)
    • 9: 필터 크기
    • 75: 색상 공간의 시그마 값
    • 75: 좌표 공간의 시그마 값

  • const kernel = matFromArray(3, 3, cv.CV_32F, [-1, -1, -1, -1, 9, -1, -1, -1, -1]), filter2D(blurred, sharp, cv.CV_8U, kernel)
    역할: 샤프닝 필터를 적용하여 이미지를 선명하게 함
    주요 파라미터:
    • kernel: 샤프닝 필터 행렬
    • blurred: 입력 이미지
    • sharp: 출력 이미지 (샤프닝된 이미지)
    • cv.CV_8U: 출력 이미지 타입

  • Canny(sharp, edges, 30, 70)
    역할: 이미지의 외곽선을 추출
    주요 파라미터:

    • sharp: 입력 이미지
    • edges: 출력 이미지 (외곽선 이미지)
    • 30: 최소 임계값
    • 70: 최대 임계값
  • threshold(edges, thresh, 150, 255, cv.THRESH_TOZERO)
    역할: 이미지를 이진화하여 픽셀 값을 0 ~ 255로 변환. 객체와 배경을 분리함
    주요 파라미터:

    • edges: 입력 이미지
    • thresh: 출력 이미지 (외곽선 이미지)
    • 150: 임계값
    • 255: 최대값
    • cv.THRESH_TOZERO: 이진화 유형

  • findContours(edges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
    역할: 이미지에서 객체의 외곽선을 검출
    주요 파라미터:
    • thresh: 입력 이미지
    • contours: 검출된 컨투어
    • hierarchy: 컨투어 계층 구조
    • cv.RETR_EXTERNAL: 외곽 컨투어만 검출
    • cv.CHAIN_APPROX_SIMPLE: 컨투어 단순화 방법

4. 컴포넌트 결합 & 결과물

src/App.js

import React, { useState } from 'react';
import ImageUpload from './components/ImageUpload';
import ObjectDetection from './components/ObjectDetection';
import './App.css';

function App() {
  const [imageSrc, setImageSrc] = useState(null);
  const handleImageUpload = (image) => {
    setImageSrc(image);
  };

  return (
    <div className="App">
      <h1>알약 탐지 with OpenCV.js</h1>
      <hr></hr>
      <ImageUpload onImageUpload={handleImageUpload} />
      <hr></hr>
      {imageSrc && <ObjectDetection imageSrc={imageSrc} />}
    </div>
  );
}
export default App;

결과물


5. 마치며

현재 진행중인 프로젝트는 카메라를 손쉽게 사용이 가능한 모바일 웹을 타겟으로 하고있습니다. 때문에 속도가 생명인 모바일 서비스 특성 상, 무거운 AI 모델을 사용하는것은 오히려 독이 될 수 있다고 판단하였습니다.
그래서 생각해낸 것이 사용자의 카메라 화면에 격자를 표시하고 어두운 배경에서 촬영을 하도록 가이드라인을 제공함으로써, 사진에 노이즈가 될만한 것들을 최소화하였습니다.

따라서 저는 어두운 배경이라는 전제조건에서 알약의 존재 유무만 판별하면 되기 때문에 의외로 간단하게 알약을 구별해낼 수 있었습니다.
이 정보가 다른 분들에게도 도움이 되기를 바라며, 다들 파이팅!!!

profile
Front-end 호소인

0개의 댓글