Skip to the content.

go-recognizer

Face detection and recognition for Go, built on top of dlib via go-face. It wraps the lower-level go-face API into a small, batteries-included Recognizer type: load a photo, find faces, identify them against a labeled dataset, and draw the results back onto the image — in a handful of method calls.

CI Go Reference Latest tag MIT Licensed

[!NOTE] dlib’s face pipeline (shape-predictor landmarks + a custom ResNet-29 metric-learning descriptor) predates most of the last decade’s face recognition research – even the newest dlib-compatible alternatives (2021/2024) are incremental tweaks to that same older approach, not a leap to what’s state of the art today. For actively-developed, modern models (YuNet, RetinaFace, ArcFace-family, SFace, GhostFaceNet…), see go-onnxface.

Features

Requirements

go-recognizer depends on go-face, which in turn requires dlib (>= 19.10) and the libjpeg development headers to compile.

go-face uses cgo, so CGO_ENABLED=1 is required at build time (this is the default on most setups, but some environments/CI images turn it off). If you see errors like undefined: face.NewRecognizer or undefined: face.Descriptor instead of a compiler error, that’s almost always CGO being disabled — run go env -w CGO_ENABLED=1 or set the env var for the build.

Ubuntu 18.10+, Debian sid

Latest versions of Ubuntu and Debian provide a suitable dlib package, so just run:

# Ubuntu
sudo apt-get install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg-turbo8-dev
# Debian
sudo apt-get install libdlib-dev libblas-dev libatlas-base-dev liblapack-dev libjpeg62-turbo-dev

macOS

Make sure you have Homebrew installed.

brew install dlib

Windows

Make sure you have MSYS2 installed.

  1. Run MSYS2 MSYS shell from Start menu
  2. Run pacman -Syu and if it asks you to close the shell do that
  3. Run pacman -Syu again
  4. Run pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-dlib 5.
    1. If you already have Go and Git installed and available in PATH uncomment set MSYS2_PATH_TYPE=inherit line in msys2_shell.cmd located in MSYS2 installation folder
    2. Otherwise run pacman -S mingw-w64-x86_64-go git
  5. Run MSYS2 MinGW 64-bit shell from Start menu to compile and use go-face

Other systems

Try installing dlib/libjpeg with your distribution’s package manager, or compile dlib from source. go-face won’t work with old dlib packages such as libdlib18. If your system isn’t covered here, open an issue with the distribution/version and we’ll try to help.

Docker

examples/Dockerfile builds dlib from source (Alpine has no dlib package) and compiles the detection example against it, ending with a ~50MB runtime image. Useful as a reference for containerized builds, and for the compiler/CMake compatibility patches it applies – dlib’s released source doesn’t build out of the box with GCC 15+ or CMake 4.x.

Installation

go get github.com/leandroveronezi/go-recognizer
import "github.com/leandroveronezi/go-recognizer"

Models

shape_predictor_5_face_landmarks.dat, mmod_human_face_detector.dat and dlib_face_recognition_resnet_model_v1.dat are required at runtime. Download them from the dlib-models repo:

mkdir models && cd models
wget https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2
bunzip2 shape_predictor_5_face_landmarks.dat.bz2
wget https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2
bunzip2 dlib_face_recognition_resnet_model_v1.dat.bz2
wget https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2
bunzip2 mmod_human_face_detector.dat.bz2

Optional: shape_predictor_68_face_landmarks.dat for full facial contour landmarks (see rec.Model.Landmark below). It’s a much larger download (~95MB uncompressed, vs ~9MB for the 5-point model), so it’s opt-in rather than required.

wget https://github.com/davisking/dlib-models/raw/master/shape_predictor_68_face_landmarks.dat.bz2
bunzip2 shape_predictor_68_face_landmarks.dat.bz2

Examples

Runnable versions of the examples below live in examples/, one per subfolder. Run them from inside examples/ so the relative fotos/models paths resolve, e.g. cd examples && go run ./detection.

Face detection

package main

import (
	"fmt"
	"path/filepath"

	"github.com/leandroveronezi/go-recognizer"
)

const fotosDir = "fotos"
const dataDir = "models"

func main() {

	rec := recognizer.Recognizer{}
	err := rec.Init(dataDir)

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.Tolerance = 0.4
	rec.UseGray = true
	rec.UseCNN = false
	defer rec.Close()

	faces, err := rec.RecognizeMultiples(filepath.Join(fotosDir, "elenco3.jpg"))

	if err != nil {
		fmt.Println(err)
		return
	}

	img, err := rec.DrawFaces2(filepath.Join(fotosDir, "elenco3.jpg"), faces)

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.SaveImage("faces2.jpg", img)

}

Face detection result

Face recognition

package main

import (
	"errors"
	"fmt"
	"path/filepath"

	"github.com/leandroveronezi/go-recognizer"
)

const fotosDir = "fotos"
const dataDir = "models"

func addFile(rec *recognizer.Recognizer, Path, Id string) {

	err := rec.AddImageToDataset(Path, Id)

	switch {
	case errors.Is(err, recognizer.ErrNoFace), errors.Is(err, recognizer.ErrNotSingleFace):
		fmt.Printf("%s: not exactly one face, skipping\n", Path)
	case err != nil:
		fmt.Println(err)
	}

}

func main() {

	rec := recognizer.Recognizer{}
	err := rec.Init(dataDir)

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.Tolerance = 0.4
	rec.UseGray = true
	rec.UseCNN = false
	defer rec.Close()

	addFile(&rec, filepath.Join(fotosDir, "amy.jpg"), "Amy")
	addFile(&rec, filepath.Join(fotosDir, "bernadette.jpg"), "Bernadette")
	addFile(&rec, filepath.Join(fotosDir, "howard.jpg"), "Howard")
	addFile(&rec, filepath.Join(fotosDir, "penny.jpg"), "Penny")
	addFile(&rec, filepath.Join(fotosDir, "raj.jpg"), "Raj")
	addFile(&rec, filepath.Join(fotosDir, "sheldon.jpg"), "Sheldon")
	addFile(&rec, filepath.Join(fotosDir, "leonard.jpg"), "Leonard")

	// No rec.SetSamples() call needed here: AddImageToDataset already
	// keeps the identifier in sync incrementally as each face is added.

	faces, err := rec.IdentifyMultiples(filepath.Join(fotosDir, "elenco3.jpg"))

	if err != nil {
		fmt.Println(err)
		return
	}

	for _, f := range faces {
		fmt.Printf("%s: distance=%.4f confidence=%.2f%%\n", f.Id, f.Distance, f.Confidence*100)
	}

	img, err := rec.DrawFaces(filepath.Join(fotosDir, "elenco3.jpg"), faces)

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.SaveImage("faces.jpg", img)

}

Face recognition result

Face landmarks

package main

import (
	"errors"
	"fmt"
	"path/filepath"

	face "github.com/leandroveronezi/go-face"
	"github.com/leandroveronezi/go-recognizer"
)

const fotosDir = "fotos"
const dataDir = "models"

func main() {

	rec := recognizer.Recognizer{}
	err := rec.Init(dataDir)

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.Tolerance = 0.4
	rec.UseGray = true
	rec.UseCNN = false
	defer rec.Close()

	f, err := rec.RecognizeSingle(filepath.Join(fotosDir, "amy.jpg"))

	switch {
	case errors.Is(err, recognizer.ErrNotSingleFace):
		fmt.Println("amy.jpg doesn't have exactly one face")
		return
	case err != nil:
		fmt.Println(err)
		return
	}

	fmt.Printf("found %d landmark points\n", len(f.Shapes))

	img, err := rec.DrawLandmarks(filepath.Join(fotosDir, "amy.jpg"), []face.Face{f})

	if err != nil {
		fmt.Println(err)
		return
	}

	rec.SaveImage("landmarks.jpg", img)

}

Face landmarks result

This uses the default 5-point model. For the full facial contour, download shape_predictor_68_face_landmarks.dat (see Models) and set rec.Model.Landmark before Init:

rec := recognizer.Recognizer{}
rec.Model.Landmark = "shape_predictor_68_face_landmarks.dat"
err := rec.Init(dataDir)

rec.Model.Descriptor and rec.Model.CNN work the same way, for the face-descriptor (ResNet) and CNN detector model files respectively — useful if you’re using differently-named or fine-tuned dlib models. All three must be set before calling Init; they’re read once, at load time.

Errors

The “expected” failure conditions – no face detected, more than one face detected, no dataset match, a missing dataset file – are exposed as sentinel errors, so callers can branch on them with errors.Is instead of matching on the error message text (which isn’t part of the API contract and may change):

faces, err := rec.Identify(path)
switch {
case errors.Is(err, recognizer.ErrNotSingleFace):
	// the image doesn't have exactly one face
case errors.Is(err, recognizer.ErrNoMatch):
	// no Dataset entry within Tolerance
case err != nil:
	// something else went wrong (I/O, decode, ...)
}
Error Returned by
ErrNoFace AddImageToDataset, when the image has no detected face
ErrNotSingleFace AddImageToDataset, RecognizeSingle, Identify, when the image has more than one detected face (or, for RecognizeSingle/Identify, doesn’t have exactly one)
ErrNoMatch Identify, when the face doesn’t match any Dataset entry within Tolerance
ErrDatasetFileNotFound LoadDataset, when Path doesn’t exist

Any other error (I/O, image decoding, etc.) is wrapped with %w, so errors.Unwrap/errors.As still reach the underlying cause.

Contributing

Issues and pull requests are welcome. If you’re reporting a build problem, please include your OS/distribution, Go version, and the full compiler output.

License

MIT