안녕하세요! 웹 개발에서 실시간 영상 처리 기술은 다양한 분야에서 활용되고 있죠.
이번 포스팅에서는 WebRTC를 이용해 웹캠 영상을 가져오고, Google의 MediaPipe 라이브러리를 활용해 실시간으로 이미지 분할을 구현하는 방법에 대해 알아볼게요.
특히 개발 과정에서 만날 수 있는 메시지(INFO: Created TensorFlow Lite XNNPACK delegate for CPU.)에 대해서도 함께 다루어 궁금증을 해소해 드릴게요.
WebRTC는 웹 브라우저 간에 플러그인 없이 실시간으로 음성, 영상 통신을 가능하게 하는 기술 표준이에요.
이를 통해 웹캠이나 마이크 같은 사용자 미디어 장치에 접근하고, 데이터를 스트리밍할 수 있죠.
이번 예제에서는 WebRTC의 navigator.mediaDevices.getUserMedia() 메서드를 사용해 웹캠 영상을 가져옵니다.
enumerateDevices(): 사용 가능한 미디어 입력 장치(특히 videoinput 타입)를 열거해 드롭다운 메뉴에 표시합니다.
requestPermissionAndEnumerateDevices(): getUserMedia를 호출해 사용자에게 카메라 및 마이크 접근 권한을 요청합니다.
이는 권한을 얻은 후 enumerateDevices()를 호출해 장치 정보를 정확히 가져오기 위함이에요.
requestMediaStream(): 선택된 장치 ID를 사용해 getUserMedia를 다시 호출하고, 가져온 MediaStream을 <video>
요소에 연결해 웹캠 영상을 화면에 표시합니다.
import React, { useState, useRef, useEffect } from 'react';
import { FilesetResolver, ImageSegmenter } from '@mediapipe/tasks-vision';
function App() {
const [mediaDevices, setMediaDevices] = useState([]);
const [selectedDeviceId, setSelectedDeviceId] = useState('');
const [mediaStream, setMediaStream] = useState(null);
const mediaPlayerRef = useRef(null);
const imageCanvasRef = useRef(null);
const maskCanvasRef = useRef(null);
const imageSegmenterRef = useRef(null);
// 미디어 장치 목록을 가져오는 함수
const enumerateDevices = async () => {
try {
const deviceInfos = await navigator.mediaDevices.enumerateDevices();
const videoInputDevices = deviceInfos.filter(
(deviceInfo) => deviceInfo.kind === 'videoinput' && deviceInfo.deviceId !== 'default'
);
setMediaDevices(videoInputDevices);
} catch (error) {
console.error('장치 조회 실패:', error);
}
};
// 권한 요청 및 장치 열거
const requestPermissionAndEnumerateDevices = async () => {
try {
const newMediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
newMediaStream.getTracks().forEach((track) => track.stop()); // 권한 확인 후 스트림 중지
enumerateDevices();
createImageSegmenter(); // 이미지 세그멘터 생성
} catch (error) {
console.error('권한 획득 실패:', error);
}
};
// 선택된 장치 변경 핸들러
const handleDeviceChange = (event) => {
setSelectedDeviceId(event.target.value);
};
// 미디어 스트림 요청
const requestMediaStream = async () => {
if (mediaStream) {
mediaStream.getTracks().forEach((track) => track.stop());
}
try {
const constraints = { video: { deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined }, audio: false };
const newMediaStream = await navigator.mediaDevices.getUserMedia(constraints);
if (mediaPlayerRef.current) {
mediaPlayerRef.current.srcObject = newMediaStream;
}
setMediaStream(newMediaStream);
} catch (error) {
console.error('MediaStream 요청 실패:', error);
setMediaStream(null);
}
};
// 컴포넌트 마운트 시 권한 요청 및 장치 열거
useEffect(() => {
requestPermissionAndEnumerateDevices();
}, []);
MediaPipe는 Google에서 개발한 크로스 플랫폼 프레임워크로, 실시간 머신러닝 솔루션을 제공해요. 얼굴 인식, 손 추적, 자세 추정 등 다양한 기능을 웹 환경에서도 손쉽게 구현할 수 있도록 도와주죠.
여기서는 MediaPipe의 Image Segmenter를 사용해 영상에서 사람과 배경을 분리하는 이미지 분할을 진행합니다.
FilesetResolver.forVisionTasks()
를 사용해 MediaPipe Vision 작업에 필요한 WASM 파일을 로드합니다.ImageSegmenter.createFromOptions()
를 호출해 이미지 분할기를 생성합니다.modelAssetPath
에는 selfie_segmenter.tflite
모델 경로를 지정해 셀카 분할에 최적화된 모델을 사용해요.runningMode: 'VIDEO'
를 설정해 비디오 스트림 처리에 최적화된 모드로 실행합니다.mediaPlayerRef
에 연결된 비디오 요소를 imageCanvasRef
에 그립니다.
imageCanvasContext.getImageData()
를 통해 현재 프레임의 이미지 데이터를 가져옵니다.
imageSegmenter.segmentForVideo()
를 호출해 해당 이미지 데이터에 대해 이미지 분할을 수행하고, 결과는 segmentVideo
콜백 함수로 전달돼요.
segmentVideo()
segmentResult
에서 categoryMask
를 추출합니다.
이 마스크는 각 픽셀이 어떤 카테고리(예: 사람, 배경)에 속하는지 나타내죠.
drawMaskImage()
함수를 호출해 이 마스크를 maskCanvasRef
에 그립니다.
categoryMask.getAsUint8Array()
를 사용해 마스크 데이터를 Uint8Array
형태로 가져옵니다.마스크 값에 따라 색상을 지정합니다.
이 예제에서는 마스크 값이 0보다 크면 녹색으로, 아니면 빨간색으로 표시해 분할된 영역을 시각적으로 구분합니다.
새로운 ImageData
객체를 생성하고, 이를 maskCanvasContext.putImageData()
를 사용해 마스크 캔버스에 그립니다.
// 이미지 세그멘터 생성
const createImageSegmenter = async () => {
if (!imageSegmenterRef.current) {
try {
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
const newSegmenter = await ImageSegmenter.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite"
},
outputCategoryMask: true,
outputConfidenceMasks: false,
runningMode: 'VIDEO'
});
console.log("Segmenter 생성 성공", newSegmenter);
imageSegmenterRef.current = newSegmenter;
} catch (error) {
console.error("Segmenter 생성 실패:", error.stack);
}
} else {
console.log("ImageSegmenter 이미 생성됨");
}
};
// 비디오 프레임 캡처 및 세그먼트
const captureVideo = () => {
if (imageSegmenterRef.current && mediaPlayerRef.current && imageCanvasRef.current) {
const imageSegmenter = imageSegmenterRef.current;
const mediaPlayer = mediaPlayerRef.current;
const imageCanvas = imageCanvasRef.current;
const imageCanvasContext = imageCanvas.getContext('2d');
if (imageCanvasContext) {
const { videoWidth, videoHeight } = mediaPlayer;
imageCanvas.width = videoWidth;
imageCanvas.height = videoHeight;
imageCanvasContext.drawImage(mediaPlayer, 0, 0, videoWidth, videoHeight);
const imageData = imageCanvasContext.getImageData(0, 0, videoWidth, videoHeight);
const startTimeMs = performance.now();
imageSegmenter.segmentForVideo(imageData, startTimeMs, segmentVideo);
}
}
};
// 세그먼트 결과 처리 콜백
const segmentVideo = (segmentResult) => {
drawMaskImage(segmentResult.categoryMask, maskCanvasRef.current);
};
// 마스크 이미지를 캔버스에 그리는 함수
const drawMaskImage = (categoryMask, maskCanvas) => {
if (maskCanvas) {
const maskArray = categoryMask.getAsUint8Array();
const imageWidth = categoryMask.width;
const imageHeight = categoryMask.height;
const maskImageArray = new Uint8ClampedArray(imageWidth * imageHeight * 4);
for (let i = 0; i < maskArray.length; i++) {
const maskValue = maskArray[i];
// 마스크 값이 0보다 크면 녹색 (사람), 아니면 검정 (배경)
const red = 0;
const green = maskValue > 0 ? 255 : 0;
const blue = 0;
const alpha = 255;
maskImageArray[i * 4] = red;
maskImageArray[i * 4 + 1] = green;
maskImageArray[i * 4 + 2] = blue;
maskImageArray[i * 4 + 3] = alpha;
}
const maskImageData = new ImageData(maskImageArray, imageWidth, imageHeight);
maskCanvas.width = imageWidth;
maskCanvas.height = imageHeight;
const maskCanvasContext = maskCanvas.getContext('2d');
if (maskCanvasContext) {
maskCanvasContext.putImageData(maskImageData, 0, 0);
}
}
};
return (
<>
<div className="horizontal-start-box">
<input type="button" value="장치 조회 및 권한 요청" onClick={requestPermissionAndEnumerateDevices} />
<select value={selectedDeviceId} onChange={handleDeviceChange}>
<option value=''>기본 장치</option>
{mediaDevices.map((device, index) => (
<option key={`mediadevice-${index}-${device.deviceId}`} value={device.deviceId}>{device.label}</option>
))}
</select>
<input type="button" value="미디어 요청" onClick={requestMediaStream} />
<input type="button" value="캡처 및 세그먼트" disabled={!mediaStream} onClick={captureVideo} />
</div>
<div className="vertical-center-box">
<div className='horizontal-start-box'>
<video ref={mediaPlayerRef} autoPlay muted style={{ width: '640px', height: '320px', background: 'black', objectFit: 'contain' }} />
{/* 세그먼트 결과를 보여줄 비디오 요소는 필요 없을 수 있습니다. */}
{/* <video ref={segmentMediaPlayerRef} autoPlay muted style={{width: '640px', height: '320px', background: 'black', objectFit: 'contain'}} /> */}
</div><br />
<div className='horizontal-start-box'>
<canvas ref={imageCanvasRef} style={{ width: '340px', height: '180px', background: 'black', objectFit: 'contain' }} />
<canvas ref={maskCanvasRef} style={{ width: '340px', height: '180px', background: 'black', objectFit: 'contain' }} />
{/* 세그먼트된 최종 이미지를 보여줄 캔버스 (마스크와 원본 이미지를 합성) */}
{/* <canvas ref={segementCanvasRef} style={{width: '340px', height: '180px', background: 'black',objectFit: 'contain'}} /> */}
</div>
</div>
</>
);
}
export default App;
INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
이 메시지는 무엇일까요?WebRTC와 MediaPipe를 함께 사용해 개발을 진행하다 보면 콘솔에 INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
와 같은 메시지가 출력되는 것을 볼 수 있어요.
처음 이 메시지를 접하면 "혹시 에러인가?" 하고 놀라실 수 있죠.
하지만 걱정하지 마세요! 이 메시지는 에러가 아니라 오히려 정상적인 동작을 나타내는 정보성 메시지입니다.
TensorFlow Lite: 모바일 및 임베디드 장치에서 머신러닝 모델을 효율적으로 실행하기 위해 Google에서 개발한 라이브러리예요.
MediaPipe는 내부적으로 TensorFlow Lite를 사용해 모델을 실행합니다.
XNNPACK: TensorFlow Lite의 최신 고성능 CPU 백엔드 라이브러리 중 하나입니다.
CPU에서 신경망 연산을 더욱 빠르게 수행할 수 있도록 최적화되어 있죠.
Delegate: TensorFlow Lite에서 특정 하드웨어 가속기 또는 특정 최적화 라이브러리를 사용해 모델 실행을 위임하는 메커니즘이에요.
즉, 이 메시지는 "MediaPipe가 내부적으로 TensorFlow Lite를 사용하고 있으며, 모델 연산을 더욱 효율적으로 수행하기 위해 XNNPACK이라는 CPU 최적화 라이브러리를 성공적으로 활성화했다"는 의미입니다.
이는 여러분의 애플리케이션이 머신러닝 연산을 정상적으로 시작했음을 알려주는 긍정적인 신호예요.
이번 포스팅에서는 WebRTC를 활용해 웹캠 영상을 가져오고, MediaPipe
의 Image Segmenter를 이용해 실시간으로 이미지 분할을 구현하는 과정을 살펴보았어요.
또한 개발 중 만날 수 있는 INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
메시지가 무엇을 의미하는지 알아보았죠.
이 기술들을 활용하면 웹 기반의 가상 배경, 증강 현실 효과 등 다양한 흥미로운 애플리케이션을 만들 수 있어요.
직접 코드를 수정하고 다양한 기능을 추가해보면서 WebRTC와 MediaPipe의 강력함을 경험해보세요!