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.
- The
ID
,Name
andMatched
fields are used when Facebox has recognised who the face belongs to — but that’s a different story
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 anio.ReadSeeker
, or if we did want to supportio.Reader
, we would first read the contents into abytes.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” + extdstFile, 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?
- Check out the source for this tutorial
- See how you can use Facebox to actually teach it who people are, and get matches back from the Go SDK
- Follow me @matryer on Twitter for more Go goodies, and why not follow @machineboxio too