Face detection and recognition using golang
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.
[!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
- Detection β find one or many faces in an image, sorted left to right.
- Recognition β identify detected faces against a dataset of known people.
- Incremental dataset updates β
AddImageToDatasetkeeps the identifier in
- Match distance/confidence β
Identify/IdentifyMultiplesreturn the
Distance and a normalized Confidence score, not just an ID.
- Landmarks β detected faces carry their
Shapes(facial landmark
rec.Model.Landmark before Init to opt into the 68-point model for
full facial contour (jawline, eyebrows, nose bridge, eyes, lips).
- Swappable model files β
rec.Model.Landmark/Descriptor/CNNlet
- Configurable matching β tune the distance
Toleranceused to accept a match. - CNN or HOG detector β trade speed for accuracy with
UseCNN. - Grayscale preprocessing β optional, via
UseGray. - Beyond JPEG input β go-face's own file loader only understands JPEG,
UseGray = true, go-recognizer decodes the source
image with Go's standard image package first (JPEG and PNG are
supported out of the box) and re-encodes it before handing it to
go-face, so PNG sources work without extra steps. This doesn't apply
when UseGray = false: the original file is passed straight through,
so it must already be a JPEG.
- Dataset persistence β save/load known faces to/from a JSON file.
- Drawing helpers β annotate the source image with boxes, labels, and
- Typed errors β
AddImageToDataset/RecognizeSingle/Identify/
LoadDataset return sentinel errors (ErrNoFace, ErrNotSingleFace,
ErrNoMatch, ErrDatasetFileNotFound) checkable with errors.Is,
instead of matching on error text. See Errors below.
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.
- Run
MSYS2 MSYSshell from Start menu - Run
pacman -Syuand if it asks you to close the shell do that - Run
pacman -Syuagain - Run
pacman -S mingw-w64-x8664-gcc mingw-w64-x8664-dlib
set MSYS2PATHTYPE=inherit line in msys2_shell.cmd located in MSYS2
installation folder
2. Otherwise run pacman -S mingw-w64-x86_64-go git
- Run
MSYS2 MinGW 64-bitshell 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
shapepredictor5facelandmarks.dat, mmodhumanface_detector.dat and dlibfacerecognitionresnetmodel_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/shapepredictor5facelandmarks.dat.bz2
bunzip2 shapepredictor5facelandmarks.dat.bz2
wget https://github.com/davisking/dlib-models/raw/master/dlibfacerecognitionresnetmodel_v1.dat.bz2
bunzip2 dlibfacerecognitionresnetmodel_v1.dat.bz2
wget https://github.com/davisking/dlib-models/raw/master/mmodhumanface_detector.dat.bz2
bunzip2 mmodhumanface_detector.dat.bz2
Optional: shapepredictor68facelandmarks.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/shapepredictor68facelandmarks.dat.bz2
bunzip2 shapepredictor68facelandmarks.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 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 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)
}

This uses the default 5-point model. For the full facial contour, download shapepredictor68facelandmarks.dat (see Models) and set rec.Model.Landmark before Init:
rec := recognizer.Recognizer{}
rec.Model.Landmark = "shapepredictor68facelandmarks.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.