얼굴 인식 사이트 구현

Category
Side Project
Status
Published
Tags
Vite
TypeScript
React
Tensorflow
FrontEnd
AI
Description
Published
Slug
Vite + React + TS
 
사이드 프로젝트로 tensorflow로 얼굴 인식 프로젝트를 진행하였습니다.
개발 전체 과정을 정리하였습니다.
 

사용 방법

  1. 웹 브라우저에서 애플리케이션이 열림
  1. "기준 이미지 업로드" 버튼을 클릭하여 비교할 인물의 사진을 업로드
  1. 카메라가 자동으로 활성화되고 얼굴 인식을 시작함
  1. "얼굴 비교 시작" 버튼을 클릭하여 카메라로 촬영된 얼굴과 기준 이미지의 일치도를 분석
  1. 화면에 얼굴 경계 상자와 일치도 퍼센트가 표시됨
  1. 일치도가 60% 이상이면 동일인으로 판단
  1. "얼굴 비교 중지" 버튼을 클릭하여 비교를 중지
notion image
 

1. 프로젝트 설정

Vite + React + TypeScript 프로젝트 생성

Vite로 React + TypeScript 프로젝트 생성
npm create vite@latest face-recognition-app -- --template react-ts cd face-recognition-app npm install

라이브러리 설치

face-api.js 라이브러리와 타입 선언 설치
npm install face-api.js
 

2. 모델 파일 준비

face-api.js 모델 파일 public 폴더에 추가
  1. public 폴더 내에 models 폴더 생성
  1. face-api.js GitHub 저장소에서 다음 모델 파일을 다운로드하여 public/models 폴더에 저장
→ 아래의 curl 명령어 사용하면 한번에 설치 가능
mkdir -p public/models cd public/models curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_landmark_68_model-weights_manifest.json curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_landmark_68_model-shard1 curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_recognition_model-weights_manifest.json curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_recognition_model-shard1 curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_recognition_model-shard2 curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/ssd_mobilenetv1_model-weights_manifest.json curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/ssd_mobilenetv1_model-shard1 curl -O https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/ssd_mobilenetv1_model-shard2 cd ../..
 

3. TypeScript 타입 정의

face-api.js의 타입 정의하기 위해
src/types 폴더 생성하고 face-api.d.ts 파일 추가
// src/types/face-api.d.ts declare module 'face-api.js';

4. 컴포넌트 구현

App.tsx는 아래와 같이 구성
import { useState } from 'react'; import './App.css'; import FaceRecognition from './components/FaceRecognition'; function App() { const [referenceImage, setReferenceImage] = useState<string | null>(null); const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { if (e.target?.result) { setReferenceImage(e.target.result as string); } }; reader.readAsDataURL(file); } }; return ( <div className="App"> <header className="App-header"> <h1>얼굴 인식 및 일치도 분석</h1> <div className="upload-section"> <h2>기준 이미지 업로드</h2> <input type="file" accept="image/*" onChange={handleImageUpload} /> </div> {referenceImage && ( <div className="reference-image-container"> <h3>기준 이미지</h3> <img src={referenceImage} alt="기준 얼굴" style={{ maxWidth: '300px' }} /> </div> )} <FaceRecognition referenceImage={referenceImage} /> </header> </div> ); } export default App;

4.2 FaceRecognition 컴포넌트

src/components 폴더 생성하고 FaceRecognition.tsx 파일 생성
import { useEffect, useRef, useState } from 'react'; import * as faceapi from 'face-api.js'; interface FaceRecognitionProps { referenceImage: string | null; } const FaceRecognition: React.FC<FaceRecognitionProps> = ({ referenceImage }) => { const videoRef = useRef<HTMLVideoElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null); const [modelsLoaded, setModelsLoaded] = useState<boolean>(false); const [stream, setStream] = useState<MediaStream | null>(null); const [similarity, setSimilarity] = useState<string | null>(null); const [isComparing, setIsComparing] = useState<boolean>(false); const [referenceDescriptor, setReferenceDescriptor] = useState<Float32Array | null>(null); const intervalRef = useRef<number | null>(null); // 모델 로드 useEffect(() => { const loadModels = async () => { const MODEL_URL = '/models'; try { await Promise.all([ faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL), faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL), faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL) ]); setModelsLoaded(true); console.log('Models loaded successfully'); } catch (error) { console.error('Error loading models:', error); } }; loadModels(); return () => { if (stream) { stream.getTracks().forEach(track => track.stop()); } if (intervalRef.current) { window.clearInterval(intervalRef.current); } }; }, []); // 카메라 접근 useEffect(() => { if (modelsLoaded) { startWebcam(); } }, [modelsLoaded]); // 기준 이미지에서 얼굴 특성 추출 useEffect(() => { if (referenceImage && modelsLoaded) { processReferenceImage(); } else { setReferenceDescriptor(null); } }, [referenceImage, modelsLoaded]); const startWebcam = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); if (videoRef.current) { videoRef.current.srcObject = stream; } setStream(stream); } catch (error) { console.error('Error accessing webcam:', error); } }; const processReferenceImage = async () => { if (!referenceImage) return; const img = new Image(); img.src = referenceImage; img.onload = async () => { try { const detections = await faceapi.detectSingleFace(img) .withFaceLandmarks() .withFaceDescriptor(); if (detections) { setReferenceDescriptor(detections.descriptor); console.log('Reference face descriptor extracted'); } else { console.error('No face detected in reference image'); setReferenceDescriptor(null); } } catch (error) { console.error('Error processing reference image:', error); } }; }; const startFaceComparison = () => { if (!modelsLoaded || !referenceDescriptor || !videoRef.current || !canvasRef.current) return; setIsComparing(true); intervalRef.current = window.setInterval(async () => { if (videoRef.current && canvasRef.current && referenceDescriptor) { const video = videoRef.current; const canvas = canvasRef.current; const displaySize = { width: video.videoWidth, height: video.videoHeight }; if (canvas.width !== displaySize.width || canvas.height !== displaySize.height) { faceapi.matchDimensions(canvas, displaySize); } const detections = await faceapi.detectAllFaces(video) .withFaceLandmarks() .withFaceDescriptors(); const context = canvas.getContext('2d'); if (context) { context.clearRect(0, 0, canvas.width, canvas.height); if (detections.length > 0) { const currentFaceDescriptor = detections[0].descriptor; const distance = faceapi.euclideanDistance(referenceDescriptor, currentFaceDescriptor); // 유사도 계산 (거리가 0.6 미만이면 동일 인물로 간주) const matchScore = Math.max(0, (1 - distance) * 100).toFixed(2); setSimilarity(matchScore); // 얼굴 경계 상자 그리기 const resizedDetections = faceapi.resizeResults(detections, displaySize); resizedDetections.forEach(detection => { const box = detection.detection.box; // 경계 상자 그리기 context.strokeStyle = parseFloat(matchScore) > 60 ? 'green' : 'red'; context.lineWidth = 2; context.strokeRect(box.x, box.y, box.width, box.height); // 일치도 텍스트 표시 context.font = '16px Arial'; context.fillStyle = parseFloat(matchScore) > 60 ? 'green' : 'red'; context.fillText(`일치도: ${matchScore}%`, box.x, box.y - 10); }); } else { setSimilarity(null); } } } }, 100); }; const stopFaceComparison = () => { setIsComparing(false); setSimilarity(null); if (intervalRef.current) { window.clearInterval(intervalRef.current); intervalRef.current = null; } if (canvasRef.current) { const context = canvasRef.current.getContext('2d'); if (context) { context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); } } }; return ( <div className="face-recognition-container"> <h2>카메라를 이용한 얼굴 인식</h2> <div className="video-container" style={{ position: 'relative' }}> <video ref={videoRef} autoPlay playsInline muted width="640" height="480" /> <canvas ref={canvasRef} style={{ position: 'absolute', top: 0, left: 0 }} /> </div> <div className="controls"> {!isComparing ? ( <button onClick={startFaceComparison} disabled={!referenceDescriptor} className="compare-btn" > 얼굴 비교 시작 </button> ) : ( <button onClick={stopFaceComparison} className="stop-btn" > 얼굴 비교 중지 </button> )} </div> {similarity && ( <div className="result"> <h3>얼굴 일치도: {similarity}%</h3> <p> {parseFloat(similarity) > 60 ? '✅ 동일 인물로 판단됩니다.' : '❌ 다른 인물로 판단됩니다.'} </p> </div> )} </div> ); }; export default FaceRecognition;
 

CSS 스타일 추가

src/App.css
#root { width: 100%; margin: 0 auto; text-align: center; } .App { width: 100%; display: flex; flex-direction: column; align-items: center; } .App-header { background-color: #242424; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; color: white; padding: 20px; width: 100%; max-width: 800px; } .upload-section { margin: 20px 0; width: 100%; } .reference-image-container { margin: 20px 0; width: 100%; } .face-recognition-container { margin-top: 30px; display: flex; flex-direction: column; align-items: center; width: 100%; } .video-container { border: 2px solid #666; border-radius: 8px; overflow: hidden; margin: 20px 0; width: 100%; max-width: 640px; } .controls { margin: 15px 0; } .compare-btn, .stop-btn { padding: 10px 20px; font-size: 16px; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } .compare-btn { background-color: #4CAF50; color: white; } .compare-btn:disabled { background-color: #cccccc; cursor: not-allowed; } .compare-btn:hover:not(:disabled) { background-color: #45a049; } .stop-btn { background-color: #f44336; color: white; } .stop-btn:hover { background-color: #d32f2f; } .result { margin-top: 20px; padding: 15px; background-color: rgba(255, 255, 255, 0.1); border-radius: 8px; width: 100%; max-width: 640px; }
 

vite-env.d.ts 수정

타입스크립트가 face-api.js를 인식할 수 있도록
src/vite-env.d.ts 파일에 다음 내용을 추가
/// <reference types="vite/client" /> declare module 'face-api.js';
 
 
  1. 프로젝트의 public 폴더에 models 디렉토리가 포함되어 있어야 함.
  1. 필요한 모든 모델 파일이 public/models/ 디렉토리에 있어야 함
      • face_landmark_68_model-weights_manifest.json
      • face_landmark_68_model-shard1
      • face_recognition_model-weights_manifest.json
      • face_recognition_model-shard1
      • face_recognition_model-shard2
      • ssd_mobilenetv1_model-weights_manifest.json
      • ssd_mobilenetv1_model-shard1
      • ssd_mobilenetv1_model-shard2
 
Vite는 public 폴더의 파일을 dist 폴더로 복사하므로
모델 파일이 올바르게 포함되어야 함
  1. 로컬에서 테스트 빌드를 실행 npm run build
  1. dist 폴더 내에 models 디렉토리와 모든 모델 파일이 있는지 확인