TensorFlow + Next.js + TypeScript: 웹 카메라로 배경 제거 및 가상 배경 이미지 추가

안녕 얘들아

원래 배경을 제거하고 새로운 가상 배경 이미지를 추가하는 응용 프로그램을 BodyPix으로 개발했습니다.



이 기사에서는 이 애플리케이션을 개발하는 방법에 대해 설명합니다.

데모→ https://travel-app-three.vercel.app/
github→ https://github.com/yuikoito/tensorflow-bodypix-sample

Next.js 애플리케이션 설정 및 react-webcam 설치



$ yarn create next-app <app-name>
$ cd <app-name>
$ touch tsconfig.json
$ yarn add --dev typescript @types/react

그런 다음 index.js 및 _app.js의 이름을 index.tsx, _app.tsx로 바꿉니다.

웹캠을 설치합니다.

$ yarn add react-webcam @types/react-webcam


이제 우리는 개발할 준비가 되었습니다 :)

TensorFlow.js 설치



TensorFlow.js를 TypeScript와 함께 사용할 때는 약간의 주의가 필요합니다.

나도 썼지만 TypeScript를 사용할 때 yarn add @tensorflow/tfjs만 하면 안 됩니다. 그렇지 않으면 유형 오류가 발생합니다.

그런 다음 아래와 같이 설치합니다.

$ yarn add @tensorflow-models/body-pix @tensorflow/tfjs-core @tensorflow/tfjs-converter @tensorflow/tfjs-backend-webgl


이 경우 body-pix model 도 사용하고 있으므로 함께 설치하십시오.

bodyPix 사용 설정



the official documentation에 명시된 바와 같이 사용이 매우 간단합니다.
1) bodyPix를 가져오고, 2) 불러오고, 3) 불러오기가 완료되면 분석하려는 이미지 데이터를 segmentPerson 함수의 인수에 넣기만 하면 됩니다.

이제 Next.js로 작성하고 있으므로 코드는 다음과 같습니다.

// first, import all you need
import { useRef, useState, useEffect } from "react";
import Head from "next/head";
import "@tensorflow/tfjs-core";
import "@tensorflow/tfjs-converter";
import "@tensorflow/tfjs-backend-webgl";
import styles from "../styles/Home.module.scss";
import * as bodyPix from "@tensorflow-models/body-pix";
import Webcam from "react-webcam";

function Home() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const webcamRef = useRef<Webcam>(null);
  // Manage the state of bodypixnet with useState
  const [bodypixnet, setBodypixnet] = useState<bodyPix.BodyPix>();

  // Run only when the page is first loaded
  useEffect(() => {
    bodyPix.load().then((net: bodyPix.BodyPix) => {
      setBodypixnet(net);
    });
  }, []);

  const drawimage = async (
    webcam: HTMLVideoElement
  ) => {
    const segmentation = await bodypixnet.segmentPerson(webcam);
    console.log(segmentation);
  };

  const clickHandler = async () => {
    const webcam = webcamRef.current.video as HTMLVideoElement;
    const canvas = canvasRef.current;
    // Make the canvas, webcam, and video size all the same size.
    webcam.width = canvas.width = webcam.videoWidth;
    webcam.height = canvas.height = webcam.videoHeight;
    const context = canvas.getContext("2d");
    context.clearRect(0, 0, canvas.width, canvas.height);
    // If it is clicked before bodypixnet is set, it will cause an error, so just in case.
    if (bodypixnet) {
      drawimage(webcam);
    }
  };
  return (
    <div className={styles.container}>
      <Head>
        <title>Travel App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/static/logo.jpg" />
      </Head>
      <header className={styles.header}>
        <h1 className={styles.title}>Title</h1>
      </header>
      <main className={styles.main}>
        <div className={styles.videoContainer}>
          <Webcam audio={false} ref={webcamRef} className={styles.video} />
          <canvas ref={canvasRef} className={styles.canvas} />
        </div>
        <div className={styles.right}>
          <h4 className={styles.title}>{t.select}</h4>
          <div className={styles.buttons}>
            <button onClick={clickHandler}>
              Button
            </button>
          </div>
        </div>
      </main>
    </div>
  );
}

export default Home;


bodypix가 제대로 작동하는지 확인합시다.

버튼을 클릭하면 웹캠, 캔버스, 비디오 데이터의 모든 요소가 동일한 크기로 설정됩니다.
drawimage 에서 bodypixnet.segmentPerson 의 인수에 넣어 비디오 데이터(웹캠)를 구문 분석할 수 있습니다. (다음 부분)

const segmentation = await bodypixnet.segmentPerson(webcam);


세그멘테이션이 로그에 올바르게 출력되면 모든 것이 정상입니다.



그런데 배경을 흐리게 하거나 사람과 다른 사람의 색상을 분리하고 싶다면 다음과 같이 쉽게 할 수 있습니다.

const coloredPartImage = bodyPix.toMask(segmentation);
const opacity = 0.7;
const flipHorizontal = false;
const maskBlurAmount = 0;
const canvas = document.getElementById('canvas');
// Draw the mask image on top of the original image onto a canvas.
// The colored part image will be drawn semi-transparent, with an opacity of
// 0.7, allowing for the original image to be visible under.
bodyPix.drawMask(
    canvas, img, coloredPartImage, opacity, maskBlurAmount,
    flipHorizontal);


필요한 경우 https://github.com/tensorflow/tfjs-models/tree/master/body-pix#output-visualization-utility-functions을 읽으십시오.
bodyPix.drawMask 를 사용하면 투명도 지정, 테두리 흐림, 왼쪽 및 오른쪽 뒤집기 등을 쉽게 수행할 수 있습니다.

여기서는 bodyPix.drawMask 를 사용하지 않고 캔버스에 직접 drawImage() 를 사용합니다.

배경을 제거하여 가상 배경 만들기



배경을 제거한다고 썼지만 실제로는 투명한 이미지를 만들기 위해 배경을 비우는 것이 아닙니다.

조사를 좀 해봤는데 배경을 완전히 제거하려면 WebGL을 사용해야 할 수도 있습니다.

참조→ Using BodyPix segmentation in a WebGL shader

그래서 캔버스destination-out를 사용하기로 했다.
(xor도 가능합니다.)

아래 그림에서 볼 수 있듯이 destination-out 겹치는 영역을 제거합니다.



따라서 임시 캔버스 요소를 만들고 그 위에 bodyPix.toMask()에서 얻은 이미지를 넣으면 마스크 영역만 제거되고 가상 배경을 얻을 수 있습니다.

또한 로그에서 bodyPix.toMask()의 내용이 ImageData로 저장되는 것을 볼 수 있습니다.



자, 이제 코드를 작성할 시간입니다!

  const drawimage = async (
    webcam: HTMLVideoElement,
    context: CanvasRenderingContext2D,
    canvas: HTMLCanvasElement
  ) => {
    // create tempCanvas
    const tempCanvas = document.createElement("canvas");
    tempCanvas.width = webcam.videoWidth;
    tempCanvas.height = webcam.videoHeight;
    const tempCtx = tempCanvas.getContext("2d");
    const segmentation = await bodypixnet.segmentPerson(webcam);
    const mask = bodyPix.toMask(segmentation);
    (async function drawMask() {
      requestAnimationFrame(drawMask);
      // draw mask on tempCanvas
      const segmentation = await bodypixnet.segmentPerson(webcam);
      const mask = bodyPix.toMask(segmentation);
      tempCtx.putImageData(mask, 0, 0);
      // draw original image
      context.drawImage(webcam, 0, 0, canvas.width, canvas.height);
      // use destination-out, then only masked area will be removed
      context.save();
      context.globalCompositeOperation = "destination-out";
      context.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
      context.restore();
    })();
  };


예이! 이제 배경이 투명해졌으므로 캔버스 요소에 배경 이미지를 추가할 수 있습니다.
canvas.style.backgroundImage 를 사용하여 배경 이미지를 직접 추가할 수 있지만 여기서는 이미지가 즉시 로드되지 않을 가능성이 있으므로 클래스 이름을 추가하는 것이 좋습니다.

그래서 이렇게 썼습니다.

  const clickHandler = async (className: string) => {
    const webcam = webcamRef.current.video as HTMLVideoElement;
    const canvas = canvasRef.current;
    webcam.width = canvas.width = webcam.videoWidth;
    webcam.height = canvas.height = webcam.videoHeight;
    const context = canvas.getContext("2d");
    context.clearRect(0, 0, canvas.width, canvas.height);
    canvas.classList.add(className);
    if (bodypixnet) {
      drawimage(webcam, context, canvas);
    }
  };


css는 다음과 같습니다.

.turky {
  background-image: url(../assets/turky.jpg);
  background-size: cover;
}


그러면 turky 클래스가 추가되면 /assets/turky.jpg가 배경 이미지로 배치됩니다.

참고로 저처럼 클래스 이름을 여러 개 준비해야 한다면 클래스 이름을 지우고 새로 추가해야 합니다.

    if (prevClassName) {
      canvas.classList.remove(prevClassName);
      setPrevClassName(className);
    } else {
      setPrevClassName(className);
    }
    canvas.classList.add(className);


그럼 끝!

참고로 배경화면은 예전에 찍은 사진들입니다.
빨리 여행할 수 있었으면 좋겠습니다!

일종의 광고인데 버튼 레이아웃용으로 개발한 ui-components 에 복사 붙여넣기 했습니다.
편리합니다!

그게 다야!



이 글은 매주 적어도 하나의 글을 쓰려고 노력하는 열 번째 주입니다.

궁금하신 분들은 지난 포스팅을 참고해주세요!
곧 봐요!










  • 연락하다



    일자리를 제안하고 싶거나 저에게 무언가를 물어보고 싶다면 저에게 메시지를 보내주세요.

    [email protected]

    좋은 웹페이지 즐겨찾기