A step-by-step tutorial to give your users some privacy

Matthew Enubuje
Jun 7 · 9 min read
Repo: https://github.com/shango44/blur-faces-using-react

The action of blurring faces in images/video is known as face redaction and it’s important in the age of GDPR/privacy. This tutorial should be easy to follow as it uses create-react-app and code is explained/commented.

Let’s get straight into it!


1. Setup

Create React App

npx create-react-app blur-faces

We will be using create-react-app to generate a React project.

Install dependencies

cd blur-facesnpm install bootstrap reactstrap react-input-range canvas facesoft 
--save
  • Reactstrap: React Bootstrap 4 Components
  • React Input Range: Range Slider
  • Canvas: Canvas implementation for Node JS
  • Facesoft: Face detection API

Clear

Delete files “app.test.js”, “logo.svg” and “serviceWorker.js” in the src folder.

Amend the files below:

index.js — Import bootstrap & remove serviceworker

import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

App.css — Remove some classes

.App {
text-align: center;
}
.App-header {
background-color: #ffffff;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: #000;
padding: 20px 0px;
}

App.js — Default React component

import React,  { Component } from 'react';
import './App.css';
export default class App extends Component {
render() {
return (
<div className="App">
</div>
)
}
}

2. App.js Setup

This file is where we control the state of the blur system and render the components.

Import dependencies

Add the code below to the imports.

import Facesoft from 'facesoft';
import { Container, Row, Col, Button, Input, Label } from 'reactstrap';
import InputRange from 'react-input-range';
import 'react-input-range/lib/css/index.css';

Constructor

constructor(props) {
super(props);

this.state = {
threshold: 9,
image: {},
data: [],
smooth: true,
}
this.facesoft = new Facesoft("INSERT_API_KEY");
}

In our state:

  • Threshold: Control the blur (0 = none | 10 = fully blurred)
  • Image: Store the uploaded image URI, width and height
  • Data: Store the face detection API data
  • Smooth: If false, then we will pixelate rather than blur

Click here to get a free face detection API Key (API key can be found in the dashboard when you log in).

Render

render() {
const { image, threshold, data, smooth } = this.state

return (
<div className="App">
<header className="App-header">
<h1>Blur Faces In Photos Using React.js</h1>
</header>
<Container>
<Row className="justify-content-md-center">
<Col md="1">
<strong>Smooth</strong>
</Col>
<Col md="3">
<Label>
<Input
type="checkbox"
id="cb-4"
checked={smooth}
onChange={e => this.setState(
{ smooth: e.target.checked }
)}
/>
(Will be pixelated if unchecked)
</Label>
</Col>
<Col md="1">
<strong>Threshold</strong>
</Col>
<Col md="7">
<InputRange
step={0.25}
maxValue={10}
minValue={0}
value={threshold}
onChange={threshold => this.setState({ threshold })}
/>
</Col>
<Col md="12" style={{paddingTop: 20}}>
<hr></hr>
</Col>
</Row>
<Row>
<Col md="6">
<div className="uploaded-image">
<input
type="file"
accept="image/x-png,image/gif,image/jpeg"
/>
{
image.hasOwnProperty("uri") &&
<img
src={image.uri}
alt="upload"
style={{maxWidth: "100%", margin: "10px 0px"}}
/>
}
</div>
{
image.hasOwnProperty("uri") &&
<Button
outline
color="primary"
size="lg"
>
Blur Faces
</Button>
}
</Col>
<Col md="6"></Col>
</Row>
</Container>
</div>
)
}

The code above will add the inputs and text for good UI, and you should now see the following below if you execute “npm start”.

3. App.js Functions

Now, we’re going to add two functions. One is for changing and setting the image upload, and one is for detecting and retrieving faces in the uploaded photo.

Handle change

Add this change event after the constructor. We will assign this to our file input next so we can retrieve the image.

handleChange(event) {
if(event.target.files.length < 1) return

const scope = this;
const uri = URL.createObjectURL(event.target.files[0]);
const img = new Image();
img.src = uri;
img.onload = () => {
scope.setState({
image: {
uri,
width: img.width,
height: img.height
}
})
}
}

In our input, let’s assign the handle change event to onChange.

<input
onChange={this.handleChange} // ADD THIS
type="file"
accept="image/x-png,image/gif,image/jpeg"
/>

Handle click

In this function, we will be using the Facesoft API passing in the image URI from the file input to get the face axis and dimensions so we can manipulate it.

handleClick(event) {
this.facesoft.detectFromURL(this.state.image.uri)
.then(result => this.setState({data: result}))
.catch(error => console.log(error))
}

We will assign the handle click function to the onClick method of our Blur Faces button.

<Button 
onClick={this.handleClick} // ADD THIS
outline
color="primary"
size="lg"
>
Blur Faces
</Button>

Binding

Before we head to the actual blurring mechanism, let us bind the functions so we can access our API and setState. Add to the constructor.

this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);

4. Blurring!

What we’ll be doing in this step is creating another file where we pass the state in App.js to, so we can blur the faces in the photo and then return a canvas.

blurFaces.js

In the src folder, create a new file called blurFaces.js and add the code below.

import React, { Component } from 'react';
import { createCanvas, loadImage } from 'canvas';
export default class BlurFaces extends Component {
componentDidUpdate(prevProps) {
}

render() {
const { width, height } = this.props.image;

return (
<div>
<p><strong>Output</strong></p>
<canvas
ref="canvas"
width={width}
height={height}
style={{maxWidth: "100%", maxHeight: "auto"}}
/>
</div>
)
}
BlurFaces.defaultProps = {
image: {
uri: "",
width: 0,
height: 0
},
threshold: 0,
data: [],
smooth: true
}

The code above imports createCanvas and loadImage, which are necessary because our blurring/pixelating technique will involve using multiple hidden canvases.

On render, we return a canvas and we’ve referenced our canvas so we can access it in componentDidUpdate which is were the blurring code will be added. The default props are there so errors don’t occur.

Get Faces method

As the data from the face detection API returns face coordinates based on its upper left and lower right, we can map it into x, y, width and height so things get much easier later on. Add the code below to the class.

getFaces(data) {
return data.map(face => ({
x: face.upperLeft.x,
y: face.upperLeft.y,
w: face.lowerRight.x - face.upperLeft.x,
h: face.lowerRight.y - face.upperLeft.y
}))
}

Component Did Update

Let’s head to the componentDidUpdate and start creating variables and statements. The statements are there to detect if no face detection data exists and if the user hasn’t clicked the blur button.

componentDidUpdate(prevProps) {
const { image, threshold, data, smooth } = this.props;
// If no data
if(data.length < 1) return;
// Output Canvas and Context
const outputCanvas = this.refs.canvas;
const outputCtx = outputCanvas.getContext('2d');
// Hidden Canvas and Context
const hiddenCanvas = createCanvas(image.width, image.height);
const hiddenCtx = hiddenCanvas.getContext('2d');

// If data, threshold and smooth is the same then clear and return (user has not clicked blur)
if(
JSON.stringify(prevProps.data) === JSON.stringify(data) &&
prevProps.threshold === threshold &&
prevProps.smooth === smooth
) {
outputCtx.clearRect(0,0, image.width, image.height);
return;
}
// NEXT CODE WILL BE ADDED HERE
}

Load Image

We will be using the loadImage method and pass in our image URI to access the uploaded photo.

// Load Image
loadImage(image.uri).then((newImage) => {
// NEXT CODE WILL BE ADDED HERE
}).catch(err => {
console.log(err)
})

BLUR

Steps:

  1. Create a canvas of the full image blurred according to the threshold
  2. Extract the faces from the blurred image using coordinates from API data
  3. Create a blank canvas and add the blurred faces to it
  4. Create a new canvas and draw the canvas of the blurred faces to apply feathering techniques
  5. Clear the visible canvas and draw the uploaded image onto it
  6. Draw the canvas of the blurred & feathered faces on top of the visible canvas

We’ll have an if statement to detect if the smooth checkbox was ticked or not. Then we’ll initialize two canvases for creating inverted masks of blurred images (for feathering). We will also change the global composite operation to the destination to create feathering (see globalCompositeOperation).

if(smooth){
// New canvases for applying blurring and feathering (canvases for inverted mask of blurred images)
const imaskCanvas = createCanvas(image.width, image.height);
const imaskCtx = imaskCanvas.getContext('2d');
const imaskCanvas2 = createCanvas(image.width, image.height);
const imaskCtx2 = imaskCanvas2.getContext('2d');
// Set global composite operation to destination in
imaskCtx.globalCompositeOperation = "destination-in";
// NEXT CODE WILL BE ADDED HERE
}

Drawing blurred faces to blank canvas

Here we’ll be utilizing two canvases — one for having uploaded image but blurred according to the threshold, and the other a blank canvas where we add the blurred faces.

For each loop, we’ll amend the threshold, because face data with a small width won’t blur well with a high pixel amount and larger widths require more amount of blur.

Also, when putting the image data onto the blank canvas, we amend the axis and dimensions due to feathering.

this.getFaces(data).forEach((face, i) => {
// Determine the blur amount by width of face
let blurAmount = threshold
if(face.w >= 300) blurAmount = threshold*2.5
else if(face.w <= 30) blurAmount = threshold*0.25
// Add blur filter
hiddenCtx.filter = `blur(${blurAmount}px)`;
// Draw original image to hidden canvas
hiddenCtx.drawImage(newImage, 0, 0, image.width, image.height);
// Add blurred faces to blank canvas
imaskCtx.putImageData(hiddenCtx.getImageData(
face.x-10,
face.y-10,
face.w+20,
face.h+20),
face.x-10,
face.y-10
)
})
// NEXT CODE WILL BE ADDED HERE

Creating feathering, then display to visible canvas

By using shadow blur, then drawing the canvas of blurred faces onto a new canvas and duplicating the process, it creates feathering.

  // Draw blurred faces onto 2nd inverted mask canvas
imaskCtx2.drawImage(imaskCanvas, 0, 0);
imaskCtx2.shadowColor = "black"; // Required for feathering
imaskCtx2.shadowBlur = 30;
imaskCtx2.globalCompositeOperation = "destination-in";
// Feathering
imaskCtx2.shadowBlur = 20;
imaskCtx2.drawImage(imaskCanvas,0,0);
imaskCtx2.shadowBlur = 10;
imaskCtx2.drawImage(imaskCanvas,0,0);
// Clear visible canvas then draw original image to it and then add the blurred images
outputCtx.clearRect(0,0, image.width, image.height);
outputCtx.drawImage(newImage, 0, 0);
outputCtx.drawImage(imaskCanvas2, 0, 0);
} // NEXT CODE WILL BE ADDED HERE

Pixelate

  1. Apply pixelation styling for the hidden canvas.
  2. Disable smoothing for the hidden canvas context.
  3. Calculate the scaled width and height by threshold (higher threshold = smaller width/height).
  4. Draw the uploaded image onto the hidden canvas with the scaled width and height.
  5. Stretch the scaled image to match the actual image width and height.
  6. Clear the visible canvas and draw the uploaded image onto it.
  7. For each loop, extract faces from pixelated canvas and draw it on top of the visible canvas.

Let’s start by changing the styling and modifying the context options.

else {
hiddenCanvas.style.cssText = 'image-rendering: optimizeSpeed;' +
'image-rendering: -moz-crisp-edges;' + // FireFox
'image-rendering: -o-crisp-edges;' + // Opera
'image-rendering: -webkit-crisp-edges;' + // Chrome
'image-rendering: crisp-edges;' + // Chrome
'image-rendering: -webkit-optimize-contrast;' + // Safari
'image-rendering: pixelated; ' + // Future browsers
'-ms-interpolation-mode: nearest-neighbor;'; // IE
// Use nearest-neighbor scaling when images are resized instead of the resizing algorithm to create blur hiddenCtx.webkitImageSmoothingEnabled = false;
hiddenCtx.mozImageSmoothingEnabled = false;
hiddenCtx.msImageSmoothingEnabled = false;
hiddenCtx.imageSmoothingEnabled = false;
// NEXT CODE WILL BE ADDED HERE
}

Now it’s time to create a pixelated version of the uploaded image and draw it onto our hidden canvas.

// We'll be pixelating the image by threshold
let percent = 0;
// Set threshold to 9.8 if it's 10 so the blurred faces aren't rendered white
threshold === 10 ?
percent = 1 - (9.8 / 10):
percent = 1 - (threshold / 10);
// Calculate the scaled dimensions
const scaledWidth = image.width * percent;
const scaledHeight = image.height * percent;
// Render image smaller
hiddenCtx.drawImage(newImage, 0, 0, scaledWidth, scaledHeight);
// Stretch the smaller image onto larger context
hiddenCtx.drawImage(hiddenCanvas, 0, 0, scaledWidth, scaledHeight, 0, 0, image.width, image.height);
// NEXT CODE WILL BE ADDED HERE

The last thing to do is to clear the visible canvas and draw the original image to it then draw the pixelated faces on top of it.

  // Clear visible canvas and draw original image to it
outputCtx.clearRect(0,0, image.width, image.height);
outputCtx.drawImage(newImage, 0, 0);
// Draw pixelated faces to canvas
this.getFaces(data).forEach(face =>
outputCtx.putImageData(
hiddenCtx.getImageData(
face.x,
face.y,
face.w,
face.h
),
face.x,
face.y
)
)
}

We’re almost done

The last thing to do is to head back into App.js, import “blurFaces.js” and add it to the render passing in the state.

import BlurFaces from './blurFaces';

In the last column we have in our “App.js” render (<Col md=“6”>); add the code inside it.

<BlurFaces
image={image}
threshold={threshold}
data={data}
smooth={smooth}
/>

Let’s test it out

Upload image:

Click “Blur Faces”:

Uncheck smooth:

You can right-click the blurred/pixelated image to save it or you could make an export function.

Have fun incorporating this into your next project and do great things!


If you enjoyed this, check out my face verification tutorial using HTML & JS

Better Programming

Advice for programmers.

Matthew Enubuje

Written by

Developer, Front-end, Graphics, Marketing.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade