0. WebGL 기반 hand pose detection
오늘은 TensorFlow.js의 backend가 무엇이 있는 지 알아보고 사용가능한 backend 중 하나인 WebGL을 기반으로 hand pose detection을 해볼것입니다.
hand pose detection만 하면 재미가 없으니 hand pose 에 따라 다음과 같이 이모티콘을 보여줄 수 있도록 해봅니다. 엄지를 위로 올리면 엄지척하는 이모티콘이 나오도록 하고 아래로 내리면 OMG하는 이모티콘을 나오도록 하겠습니다.
1. TensorFlow.js backend
TensorFlow.js에는 다양한 backend가 존재합니다. 일단 backend란 모델 그래프의 연산들을 수행하는 내부적인 플랫폼이라고 이해하시면 됩니다. 그래서 어떤 backend를 사용하느냐에 따라 같은 모델이라도 inference time 성능에 영향을 미치게 되는 것이죠!
1.1 CPU (순수 javascript)
CPU backend는 가장 기본적인 backend입니다. 가용성과 보편성을 가지고 작은 오버헤드와 자동으로 메모리관리를 해주는 장점을 갖지만 단일 thread로 실행되므로 하드웨어 가속의 장점은 가져갈 수 없습니다. 가용 리소스는 javascript runtime에 의해 제한되는 것도 문제이죠. 결국 많이 느리다는 문제로 일반적으로 웹 애플리케이션에서 사용되지 않는 backend입니다.
1.2 WebGL
WebGL API를 사용하는 backend입니다. WebGL은 웹에서의 그래픽 처리에 사용되는 표준 API입니다. 고수준의 병렬 처리로 가능한 작업들을 활용할 수 있도록 Shader 프로그램을 사용하여 커널 연산들을 구현합니다. WebGL backend는 텐서를 GPU에 올릴 수 있는 텍스쳐 형식으로 저장하고 각 텍스쳐 좌표마다 GPU를 통해 병렬로 처리하게 되는 것입니다. 그래서 WebGL backend사용시 cpu보다 100배 빠르다고 하네요!
Shader란? 컴퓨터 그래픽스 분야에서 그래픽 하드웨어의 랜더링 효과를 계산하는 데 쓰이는 소프트웨어 명령의 집합
또 다른 특징으로는 TensorFlow.js는 shader 자원을 최대한 활용하기 위해 미리 컴파일된 shader 프로그램을 따로 캐시에 저장둡니다. 그래서 model의 inference을 수행하기 전 준비 프로세스는 컴파일된 코들르 캐시에 복사해 둡니다. 그리고 캐시에 저장된 코드는 이후 연산의 실행이 발생할 때마다 재사용하여 shader 컴파일 과정의 오버헤드를 줄이게 됩니다. 특히나 ML 어플리케이션의 경우 같은 연산을 반복하는 경우가 많기 때문에 해당 특징은 inference time을 줄이는 데 효과적입니다.
1.3 Node.js
Node.js는 서버 사이드 javascript 플랫폼이며 웹 어플리케이션 만들때 사용많이합니다. javascript 런타임으로 v8을 사용합니다.
v8이란? V8 엔진은 구글이 만들었으며 오픈소스이고 C++로 제작됩니다. 구글크롬에서 사용 중입니다.
Node.js는 이벤트 중심의 I/O를 사용하여 여러 네트워크 연결을 범위성 있게 다룰 수 있도록합니다. 이러한 동시 연결 모델은 ML과 같이 CPU자원을 집중적으로 사용하는 작업일 경우 좋은 선택지는 아닌데 TenosorFlow.js에서 Node.js backend를 선택한 이유는 Node.js의 잠재력때문입니다.
Node.js backend는 Node.js를 C언어로 확장한 것인데 이 backend는 Tensorflow의 C언어 API를 사용가능하도록 하기 때문에 GPU와 TPU 사용 측면에서 많은 잠재력을 지닙니다. Tensorflow의 C언어 구현부는 하드웨어 가속에 최적화 되어있기때문에 Node.js backend만 잘 갖춰진다면 웹어플리케이션 배포까지의 최적화가 모두 이루어지는 것입니다.
2. hand pose detection 코드
오늘 사용할 model은 tfjs-models에서 제공하는 model을 사용할것이고 webcam을 통해서 live로 hand pose detection을 수행하도록 하겠습니다. 해당 detection은 위에서 말씀드린 webGL backend을 사용하게 됩니다. 추가적으로 손모양에 따라 이모티콘이 보여질 수 있도록 하는 것을 목적으로 합니다.
2.1 HTML skeleton
HTML를 통해 뼈대부터 만들어보죠.
<div id="main">
<div class="container">
<div class="canvas-wrapper">
<canvas id="output"></canvas>
<video id="video"
playsinline=""
style="-webkit-transform: scaleX(-1);
transform: scaleX(-1);
visibility: hidden;
width: auto; height: auto;">
</video>
</div>
<div id="emo"> </div>
</div>
</div>
중요하게 보실 내용은 다음과 같습니다.
- output Id를 가지는 canvas를 통해 hand pose detection의 결과가 webcam 위에 그려짐
- video element를 통해 webcam이 실시간으로 streaming됨
-webkit-transform
과transform
을 통해 좌우반전시킴
- emo id를 가지는 div를 통해 hand pose에 따라 이모티콘 출력
2.2 CDN을 통한 필요한 javascript import
<!-- Require the peer dependencies of hand-pose-detection. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter"></script>
<!-- You must explicitly require a TF.js backend if you're not using the TF.js union bundle. -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/hand-pose-detection"></script>
WebGL backend를 사용하기 위해 3번째 script 코드를 작성하였고 4번째 script 코드는 저희가 사용할 hand pose detection model을 사용하려면 필요한 javascript입니다. 해당 javascript사용하기 위해 tfjs-core와 tfjs-converter가 필요하기 때문에 위의 2개 script코드를 추가하였습니다.
2.3 webcam을 통한 hand pose detection javascript
app이라는 function을 다음과 같이 만들어 큰 틀부터 잡고 시작하죠!
<script>
async function app() {
camera = await Camera.setupCamera(); //webcam 셋팅
detector = await createDetector(); // hand pose detection model 셋팅
console.log(tf.getBackend()); // 사용되는 TensorFlow.js backend확인
renderPrediction(); // detection을 통한 result를 draw
};
app(); // app function 실행
</script>
해당 script를 통해 app
함수가 실행될 건데 해당 함수에서는 webcam과 detection model을 load하고 webcam을 입력으로 detection model이 도출한 결과를 renderPrediction을 통해 보여지는 flow를 가집니다.
이제 하나하나 위의 함수에 대한 구현부와 그에 대한 주석을 보면서 이해해보죠. (함수들의 구현부를 모두 보여드리는 것은 너무 길어서 중요부분만 보여드리고 설명 할수도 있다는 점 알려드릴게요. 그래두 전체 코드는 제 github tfjs_tutorial 에 올려두었으니 걱정마세여)
2.3.1 webcam 설정 및 활성화
function isiOS() {
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
}
function isAndroid() {
return /Android/i.test(navigator.userAgent);
}
function isMobile() { // mobile인지 확인
return isAndroid() || isiOS();
}
class Camera {
constructor() {
this.video = document.getElementById('video'); //video id를 가진 HTML code의 element가져옴
this.canvas = document.getElementById('output');
this.ctx = this.canvas.getContext('2d');
}
static async setupCamera() {
... //생략
const $size = { width: 640, height: 480 }; //desktop용 사이즈
const $m_size = { width: 360, height: 270 }; //mobile용 사이즈
const videoConfig = {
'audio': false,
'video': {
facingMode: 'user',
width: isMobile() ? $m_size.width : $size.width,
height: isMobile() ? $m_size.height : $size.height,
}
};
const stream = await navigator.mediaDevices.getUserMedia(videoConfig);
const camera = new Camera();
camera.video.srcObject = stream; // webcam의 live stream을 video id가진 HTML코드의 video element에 할당
await new Promise((resolve) => {
camera.video.onloadedmetadata = () => {
resolve(video);
};
});
camera.video.play();
const videoWidth = camera.video.videoWidth;
const videoHeight = camera.video.videoHeight;
camera.video.width = videoWidth;
camera.video.height = videoHeight;
// canvas는 나중에 detection result를 그리는데 사용 됨
camera.canvas.width = videoWidth; // videoWidth와 일치시켜 detection result가 video cam위에 맵핑되도록함
camera.canvas.height = videoHeight;
const canvasContainer = document.querySelector('.canvas-wrapper');
canvasContainer.style = `width: ${videoWidth}px; height: ${videoHeight}px`; // css부분도 video cam과 같은 크기로 할당
// 기본적으로 camera가 mirroring되어있으므로 horizontal flipping함
camera.ctx.translate(camera.video.videoWidth, 0);
camera.ctx.scale(-1, 1);
return camera;
}
코드의 설명은 주석으로 이해하시면 되고 중요 내용은 다음과 같습니다.
document.getElementById
를 통해 해당 Id를 가진 HTML element를 가져옴- desktop용, mobile용 webcam사이즈를 달리함
- video와 canvas의 크기 설정을 동일시함
- webcam이 mirroring되어있으므로 horizontal flipping
2.3.2 hand pose detection model 셋업
async function createDetector() {
const hands = handPoseDetection.SupportedModels.MediaPipeHands; //mediapipe에서 제공하는 hand pose detection model사용
return handPoseDetection.createDetector(hands, {
runtime: 'tfjs', //runtime을 tfjs로 설정함에 따라 webGL을 Default로 사용함
modelType: 'full', //full(큰 모델) or lite(작은 모델)
maxHands: 1, // or 2~10 : detect할 손의 개수
})
}
runtime
을 tfjs설정하면 자동으로 webGL backend가 사용됩니다. HTML skeleton에서 작성했던 console.log(tf.getbackend())
의 결과를 먼저 보여드리면 다음과 같이 webGL을 사용하는것을 알 수 있습니다.
2.3.3 hand pose detection
async function renderResult() {
... // 생략
let hands = null;
if (detector != null) {
try {
hands = await detector.estimateHands(
camera.video,
{ flipHorizontal: false }); //hand pose detection 결과를 hands에 반환
} catch (error) {
detector.dispose(); //detector에대한 tensor memory를 없앰
... // 생략
}
}
... // 생략
if (hands && hands.length > 0) {
camera.drawResults(hands); // detection결과인 hands를 인자로 결과를 visualize하는 drawResults 실행
}
}
async function renderPrediction() {
await renderResult();
rafId = requestAnimationFrame(renderPrediction); //실시간으로 renderPrediction을 계속 실행
};
HTML skeleton 에서 renderPrediction
에 대한 함수입니다. 해당 함수에서는 renderResult
함수를 지속적으로 실행시키게 됩니다. renderResult
는 실제 hand pose detection을 실행하며 결과를 web에 그려주기 위해 drawResults
를 호출하게 됩니다.
2.3.4 detection result 그리기
detection result를 그리는 코드를 알아보기전에 hand pose detection의 결과값의 의미부터 해석해보시죠.
detection결과에서 중요하게 볼것은 왼손인지 오른손인지 handness로 확인가능하며 keypoints 배열에 각 hand keypoint에 대한 이름과 좌표를 담고 있습니다. 총 hand keypoint는 0~20까지 이므로 총 21가지 존재하겠죠. 이제 위의 결과를 이해하셨다면 코드를 보러 가보죠.
const fingerLookupIndices = {
thumb: [0, 1, 2, 3, 4],
indexFinger: [0, 5, 6, 7, 8],
middleFinger: [0, 9, 10, 11, 12],
ringFinger: [0, 13, 14, 15, 16],
pinky: [0, 17, 18, 19, 20],
}; // 각 keypoint(손가락)을 이어주는 연결을 표현하기 위함
class Camera {
... //생략
static async setupCamera() {
... //생략
}
drawResults(hands) {
... // 생략
for (let i = 0; i < hands.length; ++i) {
this.drawResult(hands[i]); //detection된 모든 hand에 모두에 대해
}
}
drawResult(hand) {
if (hand.keypoints != null) {
this.drawKeypoints(hand.keypoints, hand.handedness);
const emo_type = this.drawEmoticon(hand.keypoints) // keypoints을 Parsing해서 emo_type을 반환합니다.
//위의 drawEoticon은 github에서 확인하세여
if (emo_type == 'up') { // 엄지가 위로 올라갈경우 따봉 이모티콘
emo.innerHTML = '<figure contenteditable="false" data-ke-type="emoticon" data-ke-align="alignCenter" data-emoticon-type="friends1" data-emoticon-name="032" data-emoticon-isanimation="false" data-emoticon-src="https://t1.daumcdn.net/keditor/emoticon/friends1/large/032.gif"><img src="https://t1.daumcdn.net/keditor/emoticon/friends1/large/032.gif" width="150" /></figure>';
}
else if (emo_type == 'down') { //엄지가 아래로 내려갈경우 OMG 이모티콘
emo.innerHTML = '<figure contenteditable="false" data-ke-type="emoticon" data-ke-align="alignCenter" data-emoticon-type="niniz" data-emoticon-name="029" data-emoticon-isanimation="false" data-emoticon-src="https://t1.daumcdn.net/keditor/emoticon/niniz/large/029.gif"><img src="https://t1.daumcdn.net/keditor/emoticon/niniz/large/029.gif" width="150" /></figure>'
}
else { // 이외일 경우 아무것도 보여주지 않음
emo.innerHTML = '<p></p>'
}
}
}
drawKeypoints(keypoints, handedness) {
const keypointsArray = keypoints;
this.ctx.fillStyle = handedness === 'Left' ? 'Red' : 'Blue'; //왼손, 오른손에 따라 색 구분
this.ctx.strokeStyle = 'White'; // keypoints를 이어주는 색을 흰색으로
this.ctx.lineWidth = 2;
for (let i = 0; i < keypointsArray.length; i++) {
const y = keypointsArray[i].x;
const x = keypointsArray[i].y;
this.drawPoint(x - 2, y - 2, 3);
}
const fingers = Object.keys(fingerLookupIndices);
for (let i = 0; i < fingers.length; i++) {
const finger = fingers[i];
const points = fingerLookupIndices[finger].map(idx => keypoints[idx]); //기준 keypoint와 연결된 keypoint들을 맵핑
this.drawPath(points, false);
}
}
drawPath(points, closePath) { // hand keypoints끼리 연결된 경우 연결(Path)을 시각화
const region = new Path2D();
region.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
const point = points[i];
region.lineTo(point.x, point.y); // points[0]과 연결된 points[1:]의 path를 그림
}
... // 생략
}
drawPoint(y, x, r) { // hand keypoint(Point)을 시각화
this.ctx.beginPath();
this.ctx.arc(x, y, r, 0, 2 * Math.PI);
this.ctx.fill();
}
}
많은 부분이 생략되었지만 제가 생각하기에 중요한 부분은 모두 주석으로 설명드린거 같네요. 정리하면 다음과 같습니다.
drawResults
는 detect된 손의 수만큼을 rendering함drawResult
는 (1) detect된 손의 keypoint와 keypoint간의 연결을 그리는drawkeypoints
함수, (2)손의 모양에 따라 emoticon을 보여줄 수 있는drawEmoticon
함수를 사용drawPoint
는 hand keypoint자체를 시각화drawPath
는 hand keypoint간의 연결된 부분을 시각화 (이때fingerLookupIndices
를 참조)emo.innerHTML
을 통해 emo라는 id를 가지는 HTML element에 할당된 HTML code(이모티콘)를 넣음
이제 코드는 모두 설명드렸습니다. 생략된부분이 모두 포함된 코드는 tfjs_tutorial 에 hand_pose_detection.html이라는 파일에 모아두었습니다.
3. hand pose detection 결과
webcam사용을 승낙하셧다면 아래에 detection결과가 짜라짠짜!!!! 엄지를 위로 하는 따봉이나 엄지를 아래로 하는 hand pose를 취하시면 이모티콘이 나타나실거예요!!
'AI Engineering > TensorFlow' 카테고리의 다른 글
TensorFlow.js (4) YOLOv5 Live demo (7) | 2022.04.11 |
---|---|
TensorFlow.js (3) TensorFlow.js 변환 (0) | 2022.04.03 |
TensorFlow.js (1) - TensorFlow.js 이해 및 detection 예제 (1) | 2022.03.17 |
Mediapipe (2) - custom segmentation model with mediapipe (0) | 2022.03.14 |
TFLite 뽀개기 (3) - Quantization (2) | 2022.03.09 |