Webcam character remover with Tensorflow

Eugene Musebe

Introduction

This article demonstrates how to remove a character from a webcam in real-time view in NextJS.

The final GitHub code can be viewed here.

Codesandbox

The final demo on Codesandbox.

Prerequisites

Entry-level knowledge in both javascript and react.

Project setup

Use the command npx create-next-app webcam_xter to create a new Next.js project You can head to the directory using cd videomerge when the project is ready.

We will begin with the backend configuration. The project backend will involve setting up a nextjs handler function that will upload our media files to Cloudinary website for online storage. The

You can start by using this link to log in or create your own cloudinary account. On the website, you will be provided a dashboard that will contain environment variables, your cloudinary name, api key, and api secret necessary for the upload configuration. To include them in your project, create a file named .env in your root directory and paste inside the following

1".env"
2
3CLOUDINARY_CLOUD_NAME =
4
5CLOUDINARY_API_KEY =
6
7CLOUDINARY_API_SECRET=

Replace the blacks above with values from your cloudinary project and restart your project using npm-run-dev.

The next move is to create a file namedupload.js in the projects' pages/api directory. In the upload file start by configuring the cloudinary environment keys which will prevent code duplication.

1var cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUDINARY_NAME,
5 api_key: process.env.CLOUDINARY_API_KEY,
6 api_secret: process.env.CLOUDINARY_API_SECRET,
7});

We can then introduce a handler function that will handle our upload post request.

1export default async function handler(req, res) {
2 if (req.method === "POST") {
3 let url = ""
4 try {
5 let fileStr = req.body.data;
6 const uploadedResponse = await cloudinary.uploader.upload_large(
7 fileStr,
8 {
9 resource_type: "video",
10 chunk_size: 6000000,
11 }
12 );
13 url = uploadedResponse.url
14 } catch (error) {
15 res.status(500).json({ error: "Something wrong" });
16 }
17
18 res.status(200).json({data: url});
19 }
20}

The function above will receive our frontend request body and upload it to cloudinary. It will then capture the uploaded media file's cloudinary url and send it back to the front end as a response.

This concludes our backend. Let us now proceed to create our application.

Front End

To execute our application, we'll need to download Tensorflow. Tensorflow is a javascript library used for training and deploying machine learning models. You can use this link to explore its capabilities. Let's begin!

Start by installing the TensorFlow dependencies of our application.

npm install @tensorflow/tfjs @tensorflow-models/body-pix

Next, we create our UI elements. We will need a video tag element set to muted, loop, and controls and also an output canvas to display the processed video. We will also create several buttons to fire our app's respective commands. We will use useRef to reference the video elements.

Your code at this point to implement the above should look like this:

1import React, { useRef, useEffect, useState } from "react";
2import * as tf from "@tensorflow/tfjs";
3import * as bodyPix from "@tensorflow-models/body-pix";
4
5
6export default function Home() {
7 let ctx_out, video_in, ctx_tmp, c_tmp, c_out;
8
9 const processedVid = useRef();
10 const rawVideo = useRef();
11 const startBtn = useRef();
12 const closeBtn = useRef();
13 const videoDownloadRef = useRef();
14 const [model, setModel] = useState(null);
15
16 return(
17 <>
18 <div className="container">
19 <div className="header">
20 <h1 className="heading">
21 Remove character from webcam
22 </h1>
23 </div>
24 <div className="row">
25 <div className="column">
26 <video
27 className="display"
28 width={800}
29 height={450}
30 ref={rawVideo}
31 autoPlay
32 playsInline
33 ></video>
34 </div>
35 <div className="column">
36 <canvas className="display" width={800} height={450} ref={processedVid}></canvas>
37 </div>
38 </div>
39 <div className="buttons">
40 <button className="button" onClick={startCamHandler} ref={startBtn}>
41 Start Webcam
42 </button>
43 <button className="button" onClick={stopCamHandler} ref={closeBtn}>
44 Close and upload original video
45 </button>
46 <button className="button">
47 <a ref={videoDownloadRef} href={videoUrl}>
48 Get Original video
49 </a>
50 </button>
51 </div>
52 </div>
53 )}
54
55 </>
56 )
57}

You can use the GitHub repo provided earlier in the article to duplicate the additional css configurations or use your own preference.

With the DOM elements ready, let us begin with the neural network architecture. In this article, we will involve the MobileNetV1 model configuration. We use this because, despite its lower accuracy, it involves faster configurations which are preferable for our learning process. Use the following code to create the MobileNetV1 model configuration.

1"pages/index.js"
2
3const modelConfig = {
4 architecture: "MobileNetV1",
5 outputStride: 16,
6 multiplier: 1,
7 quantBytes: 4,
8};

In the code above, the architecture param determines which bodypix architecture to load. We can choose to use ResNet, which is more accurate but in our case, it will be larger and too slow for a learning process. You can use this Link to learn more about these configurations. There are 2 strides supported by MobileNetV1's outputStride param. That is the 8 and 16. They are used to specify the output stride of the BodyPix model. The multiplier is the float multiplier for the number of channels for all convolution ops. It can be one of 1.0, 0.75 or 0.5 quantBytes control bytes are used for weight quantization. Its available option is 1, 2, and 4.

Next, we work on our segmentation configuration.

1"pages/index.js"
2
3 const segmentationConfig = {
4 internalResolution: "full",
5 segmentationThreshold: 0.1,
6 scoreThreshold: 0.4,
7 flipHorizontal: true,
8 maxDetections: 1,
9 };

In the above configuration, we set internalResolution to full which will prevent resizing. For better performance, you can downsize the input image before processing. segmentationThreshold refers to the minimum pixel confidence threshold before it identified a human body. The scoreThreshold represents the minimum confident threshold to recognize an entire human body. The default We can then load the bodypix model with the model configuration inside a useEffect hook. we will also flip our video horizontally set the proper orientation for the pose and segmentation. maxDetections detects the maximum number of person to detect per image.

We can load our bodypix model with our model configuration inside a useEffect hook.

1"pages/index.js"
2
3
4useEffect(() => {
5 if (model) return;
6 const start_time = Date.now() / 1000;
7
8 bodyPix.load(modelConfig).then((m) => {
9 setModel(m);
10 const end_time = Date.now() / 1000;
11 console.log(`model loaded successfully, ${end_time - start_time}`);
12 });
13 }, []);

Declare the following variables. We'll use them to capture our stream and capture our videos.

1"pages/index.js"
2
3let recordedChunks = [];
4 let localStream = null;
5 let options = { mimeType: "video/webm; codecs=vp9" };
6 let mediaRecorder = null;
7 let videoUrl = null;

Create a function startCamHandler after the above variables.

1const startCamHandler = async () => {
2 console.log("Starting webcam and mic ..... ");
3 localStream = await navigator.mediaDevices.getUserMedia({
4 video: true,
5 audio: false,
6 });
7
8 // console.log(model);
9
10 //populate video element
11 rawVideo.current.srcObject = localStream;
12 video_in = rawVideo.current;
13 rawVideo.current.addEventListener("loadeddata", (ev) => {
14 console.log("loaded data.");
15 transform();
16 });
17
18 mediaRecorder = new MediaRecorder(localStream, options);
19 mediaRecorder.ondataavailable = (event) => {
20 console.log("data-available");
21 if (event.data.size > 0) {
22 recordedChunks.push(event.data);
23 }
24 };
25 mediaRecorder.start();
26 };

Above, we activate the user webcam while leaving the audio stream disabled. We then populate the video element with the webcam stream and run the function named transform which will capture the canvas element's context and populate it with the current video frame using a drawImage method. We then use the getImageData method to get the pixel data on the canvas. Use this pixel data with the segmentPerson method to begin to execute the analysis. Using a nested loop, iterate through each frame's pixel. We'll use variable 'n' to iterate through each array which is stored in single dimensional format.

Using an if statement, we'll loop each pixel to check if it belongs to a human or not. If it does, the pixel data will be copied. If not, we skip the updating process.

1let transform = () => {
2 // let ;
3 c_out = processedVid.current;
4 ctx_out = c_out.getContext("2d");
5
6 c_tmp = document.createElement("canvas");
7 c_tmp.setAttribute("width", 800);
8 c_tmp.setAttribute("height", 450);
9
10 ctx_tmp = c_tmp.getContext("2d");
11
12 computeFrame();
13 };
14
15 let computeFrame = () => {
16 ctx_tmp.drawImage(
17 video_in,
18 0,
19 0,
20 video_in.videoWidth,
21 video_in.videoHeight
22 );
23
24 let frame = ctx_tmp.getImageData(
25 0,
26 0,
27 video_in.videoWidth,
28 video_in.videoHeight
29 );
30
31 model.segmentPerson(frame, segmentationConfig).then((segmentation) => {
32 let output_img = ctx_out.getImageData(
33 0,
34 0,
35 video_in.videoWidth,
36 video_in.videoHeight
37 );
38
39 for (let x = 0; x < video_in.videoWidth; x++) {
40 for (let y = 0; y < video_in.videoHeight; y++) {
41 let n = x + y * video_in.videoWidth;
42 if (segmentation.data[n] == 0) {
43 output_img.data[n * 4] = frame.data[n * 4]; // R
44 output_img.data[n * 4 + 1] = frame.data[n * 4 + 1]; // G
45 output_img.data[n * 4 + 2] = frame.data[n * 4 + 2]; // B
46 output_img.data[n * 4 + 3] = frame.data[n * 4 + 3]; // A
47 }
48 }
49 }
50 // console.log(segmentation);
51 ctx_out.putImageData(output_img, 0, 0);
52 setTimeout(computeFrame, 30);
53 });
54 };

...and that's it! We have successfully achieved the process of removing a human character from the webcam view. The final UI experience should be as displayed below

Try out our developing and demo to enjoy the experience.

Happy coding!

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.