Demo: https://editor.p5js.org/heyozramos/sketches/LVsjLSPt7
A while back I came across a really fun tweet by @cassidoo on Twitter
At the end of the clip, there's a scene where a million copies of her are singing together. It's a really cool effect and in this first tutorial in a series on Segmentation Effects, I'll show you how to recreate it with JavaScript!
Our tools
We'll be using:
- p5.js in global mode. p5.js is a beginner-friendly creative coding library
- Google's Bodypix. Bodypix is a segmentation and pose estimation computer vision library powered by TensorFlow. We'll initialize BodyPix through Handsfree.js
- Handsfree.js is my library for interacting with web pages handsfree
<head>
<script src="https://cdn.jsdelivr.net/npm/p5@0.10.2/lib/p5.js"></script>
<script src="https://unpkg.com/handsfree@6.1.7/dist/handsfree.js"></script>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/handsfree@6.1.7/dist/handsfree.css">
</head>
Instantiating Handsfree.js
Handsfree.js is a library that wraps popular computer vision models like Jeeliz Weboji (head tracking), Google's BodyPix (segmentation and pose estimation), and soon, eye tracking through WebGazer and Handtracking through Handtrack.js. I originally started it as an assistive tech tool to help people browse the web handsfree through face gestures, but it also doubles as a controller for games, Algoraves, and other creative coding projects!
Oz Ramos@heyozramosDay 1 of #100DaysofCode and #100DaysofMLCode
My goal with Handsfree.js will be to wrap JavaScipt based computer vision libraries under a single API for the purpose of interacting with web pages (with head, body, hand tracking)
Here's a preview (link at end is broken, will fix!)00:36 AM - 05 Nov 2019
So to begin, we need to instantiate Handsfree.js. We instantiate once for every camera that we want to use (if you have a multi-cam setup or using a mobile device with front/back webcams). Instantiation looks like this:
/**
* This method gets called once automatically by p5.js
*/
function setup () {
// Instantiate Handsfree. "H"andsfree is the class, "h"andsfree is the instance
handsfree = new Handsfree({
autostart: true,
models: {
head: {enabled: false},
bodypix: {
enabled: true,
// Uncomment this if it runs slow for you
// modelConfig: {
// architecture: 'MobileNetV1',
// outputStride: 8,
// multiplier: 0.75,
// quantBytes: 2
// }
}
}
})
// Disable default Handsfree.js plugins
Handsfree.disableAll()
}
Creating a buffer canvas
The next thing we'll need to do is create a "buffer" canvas. This canvas will contain the cutout, which we'll then paste into the p5 canvas a bunch of times:
function setup () {
// ...
// Create a canvas that'll contain our segmentation
$buffer = document.createElement('canvas')
$buffer.style.display = 'none'
bufferCtx = $buffer.getContext('2d')
document.body.appendChild($buffer)
// This part's important, match the buffer dimensions to the webcam feed
$buffer.width = handsfree.debugger.video.width
$buffer.height = handsfree.debugger.video.height
widthHeightRatio = $buffer.width / $buffer.height
// Create a slider to adjust the number of clones
slider = createSlider(3, 12, 4, 1)
// Finally, create the p5 canvas
renderer = createCanvas(600, 600)
$canvas = renderer.canvas
canvasCtx = $canvas.getContext('2d')
}
Creating a mask
(GIF source: https://blog.tensorflow.org/2019/11/updated-bodypix-2.html)
We'll be using BodyPix as a kind of "smart green screen" that's able to remove all the background pixels. We'll do this in a method called draw
that is run automatically by p5 every few milliseconds:
function draw() {
background(220);
// Just for convenience
const model = handsfree.model
const body = handsfree.body
// Only paint when we have data
if (body.data) {
// Create a mask, with all non segmented pixels as magenta
mask = model.bodypix.sdk.toMask(
body.data,
{r: 0, g: 0, b: 0, a: 0}, // foreground
{r: 255, g: 0, b: 255, a: 255} // background
)
// Paint the mask into a buffer canvas
model.bodypix.sdk.drawMask(
$buffer,
handsfree.debugger.video,
mask,
1, 0, 0
)
// Make all magenta pixels in the buffer mask transparent
let imageData = bufferCtx.getImageData(0, 0, $buffer.width, $buffer.height)
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i] === 255 && imageData.data[i + 1] === 0 && imageData.data[i + 2] === 255) {
imageData.data[i + 3] = 0
}
}
bufferCtx.putImageData(imageData, 0, 0)
// Dimensions of each mask
w = $buffer.width / slider.value()
h = w * widthHeightRatio
// Paste the mask a bunch of times
for (row = 0; row < slider.value() * 3; row++) {
for (col = 0; col < slider.value(); col++) {
// Stagger every other row
x = col * w - (w / 2) * (row % 2)
y = row * h / 3 - h / 2 * (1 + row / 10)
canvasCtx.drawImage($buffer, x, y, w, h)
}
}
}
}
On each webcam frame, Handsfree.js captures segmentation data into handsfree.body.data
which we'll use to create the mask.
// Create a mask, with all background pixels as magenta (255, 0, 255)
mask = model.bodypix.sdk.toMask(
body.data,
{r: 0, g: 0, b: 0, a: 0}, // foreground
{r: 255, g: 0, b: 255, a: 255} // background
)
// Paint the mask into a buffer canvas
model.bodypix.sdk.drawMask(
$buffer,
handsfree.debugger.video,
mask,
1, 0, 0
)
Next, we'll go through each pixel and make any magenta pixels transparent:
// Make all magenta pixels in the buffer mask transparent
let imageData = bufferCtx.getImageData(0, 0, $buffer.width, $buffer.height)
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i] === 255 && imageData.data[i + 1] === 0 && imageData.data[i + 2] === 255) {
imageData.data[i + 3] = 0
}
}
bufferCtx.putImageData(imageData, 0, 0)
Finally, we paste the buffer into the p5 canvas a bunch of times:
// Dimensions of each mask
w = $buffer.width / slider.value()
h = w * widthHeightRatio
// Paste the mask a bunch of times
for (row = 0; row < slider.value() * 3; row++) {
for (col = 0; col < slider.value(); col++) {
// Stagger every other row
x = col * w - (w / 2) * (row % 2)
y = row * h / 3 - h / 2 * (1 + row / 10)
canvasCtx.drawImage($buffer, x, y, w, h)
}
}
And that's all there is to it!
Let me know in the comments if anything is unclear. I hope you enjoy playing around with this demo, it's the first in a set of tutorials on creating Instagram SparkAR-like filters for the web with p5.js.
Top comments (0)