배경

이미지 분류는 딥러닝 분야에서 가장 흔하게 사용되는 예제중에 하나입니다. 이미지 파일 자체가 상당히 복잡한 데이터 형식(3차원 Tensor로 표현된다고 하죠😅)이고, 이를 다양한 범주로 분류하는 것이 현재와 같은 신경망 학습 모델이 없었다면 불가능했기 때문이 아닌가 생각이 되네요.
이미지를 분류하는데 높은 성능을 내는 신경망 모형중에 합성곱 신경망(CNN, Convolutional Neural Network)이라는 모델이 있습니다. 이번 포스트에서는 합성곱 신경망 중에 대표적인 모형인 MobileNet v1의 Javascript api를 이용해 브라우저용 이미지 분류기 앱을 만들어 보려고 합니다.

준비사항

웹브라우저에서 웹캠(Webcam) 스트리밍하기

아래 코딩작업을 따라가기 위해서는 웹캠이 반드시 필요합니다!(노트북에 내장된 웹캠이어도 무관합니다.) 또한 프로젝트에서 사용하게 될 웹브라우저는 최신 버전의 Chrome이나 Firefox 등을 사용하셔야 합니다.
웹캠이 PC에 제대로 설치되어 있다면 웹캠의 스트리밍 화면을 나타나게 하는 html은 아래와 같이 간단하게 작성할 수 있습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <video
      width="320"
      height="240"
      autoplay
      playsinline
      muted
      id="player"
    ></video>    <!-- 이 video tag를 통해 webcam feed를 재생합니다. -->
    <script>
      // video tag의 DOM element를 player로 지정합니다.
      var player = document.getElementById("player");        
	  // 웹캠 사용권한이 승인되는 경우 스트리밍 영상을 player의 재생대상으로 지정 
      var handleSuccess = function (stream) {
        player.srcObject = stream;          
      };

      navigator.mediaDevices.getUserMedia({ video: true }).then(handleSuccess);
        // 현재 사용중인 브라우저 객체(navigator)의 mediaDevices 인터페이스를 
        // 이용하여 사용자의 미디어 입력장치 사용권한을 받습니다.
    </script>
  </body>
</html>
index.html

만들어진 index.html 파일을 Chrome 브라우저로 실행해 보면 html 페이지에 웹캠의 재생화면(사이즈는 video tag에서 정의한 대로 320 X 240)이 실행되게 됩니다.😄

웹캠의 재생화면이 브라우저에 나타나게 됩니다! (사이즈는 320 x 240 px 로 설정하였습니다)

mobilenet javascript api

mobilenet은 (224 pixel X 224 pixel) 이미지를 1000개의 카테고리(오렌지, 사과, 핸드폰, 노트북, ...)로 분류하는 신경망 학습 모형입니다. 합성곱 신경망(CNN)의 초기 모델인 VGG network 보다 개선된 모형으로, 네트워크 구조를 크게 단순화시키면서 기존의 이미지 분류 성능을 유지한 획기적인 모형으로 평가 받고 있습니다.
네트워크를 똑같이 복제하여 실제 데이터로 훈련해보는 것도 크게 어렵지 않은 듯한데요, 일단 이번에는 딥러닝 맛보기 차원에서 미리 훈련된(pretrained) 네트워크의 api를 사용해보려고 합니다. 공식 Github 저장소에 가보니 아래와 같이 상당히 간단한 api로 구성이 되어 있네요~

model.classify() 함수를 사용하면 mobilenet을 통해 분류된 카테고리 결과를 바로 볼수 있습니다.

이제 웹캠을 통해 캡쳐된 이미지를 텐서로 변환하고 이를 mobilenet의 classify() 함수로 전달하여 반환된 결과를 html에 출력하는 코드를 작성하면 될 것 같습니다.

Let's Build!

CDN Script Library 추가

앞에서 언급된 mobilenet javascript api를 사용하기 위해 해당 CDN 라이브러리 링크를 추가하고, 이미지를 Tensor로 전환하는 Tensorflow js 라이브러리 역시 script tag로 추가하여 줍니다.(<head></head> 태그내에 추가하면 됩니다)

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
index.html

캡쳐된 이미지의 Tensor를 생성하여 mobilenet api에 전달하는 함수 작성

이제 video tag에서 재생되는 웹캠 화면을 캡쳐하여 Tensor로 변환하는 함수를 작성하여 보겠습니다.
Tensorflow js의 tf.data.webcam() 함수를 이용하면 손쉽게 웹캠 스트리밍을 Tensor로 변환하는 것이 가능합니다. 또한 capture() 함수를 통해 특정 시점의 이미지 텐서값을 반환할수 있게 됩니다.
<script></script> 태그내에 하단과 같은 javascript 구문을 추가하여 원하는 기능을 구현해 봅니다.

// html구문에서 id="capture"인 <button>을 생성하고 해당 element를 지정
var captureButton = document.getElementById("capture");

captureButton.addEventListener("click", async function () {
  // mobilenet을 로딩합니다.
  net = await mobilenet.load();
  console.log("Successfully loaded model");

  // Tensorflow.js data API를 이용해 웹캠에서 이미지를 캡쳐하여 
  // 텐서로 저장합니다(mobilenet 전달을 위해 224 x 224 크기로 resize합니다.)
  const webcam = await tf.data.webcam(player, {
    resizeWidth: 224,
    resizeHeight: 224,
  });
  // 특정 시점의 이미지 텐서를 캡쳐합니다.
  const img = await webcam.capture();
  // 로딩된 mobilenet 모델에 전달합니다.
  document.getElementById("console").
  const result = await net.classify(img);
  // classify를 통해 전달받은 결과를 html element에 전달합니다. 
  innerText = `
  prediction: ${result[0].className}\n
  probability: ${result[0].probability}
  `;
  // img 텐서를 지웁니다.
  img.dispose();
});
index.html

파일 <script></script> 태그내 삽입구문추가적으로 "stop" 버튼을 하나 생성하여 웹캠 스트리밍 재생을 중지하는 기능을 추가하였습니다. 필수적인 요소는 아니기 때문에 자세한 설명은 생략하도록 하겠습니다.

(옵션) Semantic-ui 라이브러리를 이용해 html 화면 꾸미기

html화면을 좀더 보기 좋게하기 위해 semantic-ui 라이브러리를 사용해 간단히 스타일링 해보았습니다. 자세한 부분은 코드를 참고하여 주세요.🎨

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
    <!-- Semantic UI Library를 추가합니다 -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"
    />
    <!-- 하단에 작성한 index.css CSS 파일을 추가합니다 -->
    <link rel="stylesheet" type="text/css" href="index.css" />
    <title>My Project</title>
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
    <div class="ui one column centered grid" style="margin: 100px 0">
      <div class="app_container">
        <h3>Tensorflowjs MobileNet Project</h3>
        <div class="ui card">
          <div class="stream">
            <video
              width="320"
              height="240"
              autoplay
              playsinline
              muted
              id="player"
            ></video>
          </div>
          <div class="content">
            <div class="button_container">
              <button class="ui primary button" id="capture">Capture</button>
              <button class="ui button" id="stop">Stop</button>
            </div>
            <div class="ui sub header">Result</div>
            <div class="description">
              <div id="console"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script>
      var player = document.getElementById("player");
      var captureButton = document.getElementById("capture");
      var stopButton = document.getElementById("stop");

      var handleSuccess = function (stream) {
        player.srcObject = stream;
      };

      navigator.mediaDevices.getUserMedia({ video: true }).then(handleSuccess);

      stopButton.addEventListener("click", function () {
        stream = player.srcObject;
        tracks = stream.getTracks();
        tracks.forEach(function (track) {
          track.stop();
        });
        player.srcObject = null;
      });
      captureButton.addEventListener("click", async function () {
        net = await mobilenet.load();
        console.log("Successfully loaded model");
          
        const webcam = await tf.data.webcam(player, {
          resizeWidth: 224,
          resizeHeight: 224,
        });

        const img = await webcam.capture();
        const result = await net.classify(img);

        document.getElementById("console").innerText = `
      	prediction: ${result[0].className}\n
      	probability: ${result[0].probability}
        `;
       
        img.dispose();
      });
    </script>
  </body>
</html>
index.html
.app_container {
  width: 500px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.content {
  display: flex;
  flex-direction: column;
}
.button_container {
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  margin: 15px;
}
body {
  background-color: linen;
}
.stream {
  display: flex;
  overflow: hidden;
}
.card {
  min-width: 320px;
}
index.css

자, 이제 Chrome 브라우저로 작성된 html을 실행시켜봅니다. 브라우저에서 웹캠사용에 대한 권한 허용여부를 묻게되면 "허용"으로 체크하여 주시면 됩니다~

웹캠 카메라 사용권한 요청에 대해 "허용"을 선택해주세요~

테스트

분류를 원하는 물체를 웹캠 화면에 비추고 "Capture" 버튼을 클릭하여 출력된 결과를 살펴보았는데요, 아래와 같이 인식이 잘 된 경우도 있지만 이상한 결과도 자주 나오는 것 같네요~😥

바나나를 캡쳐하여 얻은 테스트 결과 입니다.

마치며

상당부분 만들어진 api를 이용하였기 때문에 딥러닝의 핵심인 신경망 모델을 생성하고 학습하는 부분이 대부분 스킵되긴 했지만, Tensorflowjs나 mobilenet 같은 훌륭한 라이브러리를 이용해 이미지 분류 앱을 손쉽게 만들어 보았다는 점에서 의미가 있는 것 같습니다. 완성된 코드는 제 GitHub 저장소에 올려놨으니 참고하여 주세요~
저도 포스팅을 위해 상당히 긴 시간을 공부하였는데 점점 딥러닝의 매력에 푹 빠져드는 것 같습니다. 그럼 모두 Happy Coding하시길 바라며 궁금하신 사항은 아래 게시판이나 Contact를 통해 언제든지 연락주세요~😎