(출처: OpenCV.ai 공식 사이트)
OpenCV(Open Source Computer Vision Library)란 컴퓨터 비전과 머신 러닝을 위한 오픈 소스 라이브러리로, 얼굴 인식, 객체 추적, 자율 주행 자동차의 차선 인식, 그리고 의료 영상 분석 등에서 주로 사용됩니다.
최근 알약 판별 프로젝트를 진행하던 중, 알약을 인식하는 기능이 필요하게 되어 객체인식에 대해 찾아보다가 OpenCV를 발견하였습니다.
프로젝트의 public/index.html
파일에 CDN을 이용하여 추가해줍니다.
<head>
.
.
.
<script async src="https://docs.opencv.org/4.x/opencv.js"></script>
</head>
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;
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)
bilateralFilter(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)
Canny(sharp, edges, 30, 70)
역할: 이미지의 외곽선을 추출
주요 파라미터:
threshold(edges, thresh, 150, 255, cv.THRESH_TOZERO)
역할: 이미지를 이진화하여 픽셀 값을 0 ~ 255로 변환. 객체와 배경을 분리함
주요 파라미터:
findContours(edges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
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;
현재 진행중인 프로젝트는 카메라를 손쉽게 사용이 가능한 모바일 웹을 타겟으로 하고있습니다. 때문에 속도가 생명인 모바일 서비스 특성 상, 무거운 AI 모델을 사용하는것은 오히려 독이 될 수 있다고 판단하였습니다.
그래서 생각해낸 것이 사용자의 카메라 화면에 격자를 표시하고 어두운 배경에서 촬영을 하도록 가이드라인을 제공함으로써, 사진에 노이즈가 될만한 것들을 최소화하였습니다.
따라서 저는 어두운 배경이라는 전제조건에서 알약의 존재 유무만 판별하면 되기 때문에 의외로 간단하게 알약을 구별해낼 수 있었습니다.
이 정보가 다른 분들에게도 도움이 되기를 바라며, 다들 파이팅!!!