Anonymising images with Go and Machine Box

--

Go’s standard library ships with some powerful image manipulation capabilities in the image, image/* and draw packages. This tutorial will utilise them, along with the Machine Box Go SDK to detect faces in an image, and censor them — thus anonymising the image.

We’ll write a simple program that we can run on the command line, but the same code could be adjusted to create a web endpoint easily enough.

Step 1. Run Facebox

Facebox is Machine Box’s facial detection and recognition machine learning capability, which is neatly contained inside a Docker image that we can run locally, and deploy anywhere.

In a terminal, run Facebox locally by doing:

docker run -p 8080:8080 -e "MB_KEY=$MB_KEY" machinebox/facebox
  • You will need to setup the MB_KEY environment variable, which you can copy and paste from the https://machinebox.io/account page after signing in

Facebox lets us teach it what people look like, so that it can automatically identify them in new photos, but we aren’t going to use the recognition capability today; instead we just want to know where the faces appear in the image.

Once Facebox is running, you can head over to http://localhost:8080/ to access the interactive console embedded in the box to play with Facebox.

Step 2. Get the Machine Box Go SDK

In a terminal, get the SDK:

go get -u github.com/machinebox/sdk-go

Step 3. Write the anonymise function

We are going to write a Go function that takes a source image, and a list of facebox.Face objects, and generates a new JPEG with each detected face removed.

Create a new folder called anon, and add a main.go file.

Add the anonymise function:

func anonymise(src image.Image, faces []facebox.Face) image.Image {
dstImage := image.NewRGBA(src.Bounds())
draw.Draw(dstImage, src.Bounds(), src, image.ZP, draw.Src)
for _, face := range faces {
faceRect := image.Rect(
face.Rect.Left,
face.Rect.Top,
face.Rect.Left+face.Rect.Width,
face.Rect.Top+face.Rect.Height,
)
facePos := image.Pt(face.Rect.Left, face.Rect.Top)
draw.Draw(
dstImage,
faceRect,
&image.Uniform{color.Black},
facePos,
draw.Src)
}
return dstImage
}

We use the image.NewRGBA function to create a new image that matches the source image’s dimensions, and then draw the source image onto it using the draw.Draw function— this essentially creates a copy of the image.

We then iterate over each Face in the faces slice, and create the faceRect variable holding the rectangle coordinates of where the face appears, and the facePos variable holding the position of the face.

The facebox.Face and facebox.Rect types look like this:

type Face struct {
Rect Rect
ID string
Name string
Matched bool
}
type Rect struct {
Top, Left int
Width, Height int
}

The Rect of a Face represents the position and size of the face within the source image.

facebox.Rect gives us width and height, but the image.Rect type wants x1, y1 — x2, y2; that’s why we do face.Rect.Left+face.Rect.Width and face.Rect.Top+face.Rect.Height in the above code.

We then use the draw.Draw function again to draw a black square (&image.Uniform{color.Black} gives us a solid black image from which to copy more pixels) onto the new image, essentially blacking out the face.

We finally return the new destination image.

It is assumed that you’re using Goimports or similar to manage your imports, but if not you’ll need to manually import the github.com/machinebox/sdk-go/facebox package, along with a few others.

Step 4. Write a command line tool

In order for this function to be useful, we need a way to load the source image, and save the destination image to disk. We’ll add a simple main function to do this.

The usage of our command line should be simple; we’ll pass the path to the source image as the only argument.

Add the following code to validate the argument, open the file, and defer closing it:

func main() {
if len(os.Args) < 2 {
log.Fatalln(“usage: anon <image>”)
}
filename := os.Args[1]
f, err := os.Open(filename)
if err != nil {
log.Fatalln(err)
}
defer f.Close()

Next we will create a client that allows us to access the Facebox services:

fb := facebox.New("http://localhost:8080")

facebox.New returns a facebox.Client that provides a range of methods which are essentially making HTTP requests under the hood to the local Facebox Docker container we have running (see the documentation for the Facebox client for more information.)

To actually detect the faces, we just need to call the Check method:

log.Println("Detecting faces...")faces, err := fb.Check(f)
if err != nil {
log.Fatalln(err)
}

The faces variable is a slice of facebox.Face objects, perfect for our anonymise function.

Next we need to actually decode the source image into an image.Image type that we can play with and manipulate in Go.

Since the Check method takes an io.Reader and probably reads the entire file, we need to seek back to the start:

_, err = f.Seek(0, os.SEEK_SET)
if err != nil {
log.Fatalln(err)
}

This ugly bit of code is just saying seek (go to) the first byte in the file (byte zero). os.SEEK_SET indicates that we want to set the position of the reader to 0 (as opposed to a relative seeking operation).

If we weren’t using an os.File here, we might need an io.ReadSeeker, or if we did want to support io.Reader, we would first read the contents into a bytes.Buffer which would give us seeking capabilities and allow us to read the contents multiple times.

Next, we are going to decode the file bytes into an image.Image:

srcImage, _, err := image.Decode(f)
if err != nil {
log.Fatalln(err)
}

The image.Decode function decodes the data from the file by automatically detecting the file format. To support various image formats, we have to import the appropriate package into the file.

Importing the package is enough, since the init method inside the package will register itself with the image.Decode function but in Go it’s an error to import a package and not use it. To avoid compiler errors, we have to use an underscore to indicate that we know what we’re doing, and that we still want to import the package — even though it looks like we aren’t using it.

Add the following imports to the top of the main.go file:

import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)

Now we have the source image decoded, we can pass it into the anonymise function along with the faces slice:

dstImage := anonymise(srcImage, faces)

Assuming all is well, we are going to encode the dstImage image to a new file on disk, this is the output file.

Add the final code to the main function:

// fudge the filename to add the -anon suffix (before the ext)
filename = filepath.Base(filename)
ext := filepath.Ext(filename)
dstFilename := filename[:len(filename)-len(ext)] + “-anon” + ext
dstFile, err := os.Create(dstFilename)
if err != nil {
log.Fatalln(err)
}
defer dstFile.Close()
log.Println(“Saving image to “ + dstFilename + “...”)
err = jpeg.Encode(dstFile, dstImage, &jpeg.Options{Quality: 100})
if err != nil {
log.Fatalln(err)
}
log.Println(“Done.”)

Here we append the -anon suffix to the filename, and create that file. Calling jpeg.Encode will write the image contents to that file as a JPEG. You could use png.Encode if you’d rather write a PNG.

Notice that since we’re using the jpeg package now, we have to remove the underscore from the import statement that we added earlier.

Step 5. Test the program

Now we have everything finished, we can test our program. Find a photograph that contains some people, and run the program passing it in via the argument:

go run main.go family.jpg

You should see some output similar to:

matryer$ go run main.go testdata/thebeatles.jpg
2017/05/06 00:50:53 Detecting faces...
2017/05/06 00:50:54 Saving image to thebeatles-anon.jpg...
2017/05/06 00:50:54 Done.

Look at the file that was generated, and notice that the faces have been censored.

Conclusion

With not much code (most of it was boilerplate loading/saving/encoding/decoding images etc.) we were able to write an image anonymisation command line tool using the Go standard library and Machine Box.

What next?

--

--