face

package module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2026 License: CC0-1.0 Imports: 6 Imported by: 2

README

CI Hits Renovate enabled

go-face

go-face implements face recognition for Go using dlib, a popular machine learning toolkit. Read Face recognition with Go article for some background details if you're new to FaceNet concept.

Component Technology
Language Go 1.26.2
Native bindings C++ via CGo
Recognition engine dlib ≥ 19.10
Image decoding libjpeg (turbo)
Testing go test (race, coverage)
Containers Docker buildx (multi-arch)
CI/CD GitHub Actions
Static analysis golangci-lint 2.11.4, hadolint 2.14.0
Security govulncheck 1.1.4, gosec 2.22.12
Dependency updates Renovate

Quick Start

# Consume the library
go get github.com/AndriyKalashnykov/go-face

# Develop locally
make deps         # check Go is installed
make testdata     # clone dlib models + test images
make static-check # format + lint + vulncheck + gosec
make test         # run tests with coverage

Note: go-face wraps dlib via CGo. Install dlib and libjpeg natively (see dlib Installation) before running make test, or run tests inside the pre-built ghcr.io/andriykalashnykov/dlib-docker image used by CI.

Prerequisites

Tool Version Purpose
GNU Make 3.81+ Build orchestration
Git latest Clone testdata and release tags
Go 1.26.2 Go compiler and runtime (derived from go.mod)
Docker latest Container image builds and act runs
dlib ≥ 19.10 Face detection/recognition C++ library
golangci-lint 2.11.4 Static analysis (auto-installed by make deps-lint)
hadolint 2.14.0 Dockerfile linting (auto-installed by make deps-hadolint)
gosec 2.22.12 Go security scanner (auto-installed by make deps-gosec)
govulncheck 1.1.4 Go vulnerability scanner (auto-installed by make deps-govulncheck)
act 0.2.87 Run GitHub Actions locally (optional, auto-installed by make deps-act)

Install Go and Docker first, then let make install the rest lazily as needed:

make deps
dlib Installation
Ubuntu 18.10+, Debian
# 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
    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 to install dlib/libjpeg with package manager of your distribution or compile from sources. Note that go-face won't work with old packages of dlib such as libdlib18. Alternatively create issue with the name of your system and someone might help you with the installation process.

Available Make Targets

Run make help to see all available targets.

Dependencies
Target Description
make deps Check Go is installed
make deps-docker Check Docker is installed
make deps-lint Install golangci-lint for static analysis
make deps-hadolint Install hadolint for Dockerfile linting
make deps-gosec Install gosec security scanner
make deps-govulncheck Install govulncheck vulnerability scanner
make deps-act Install act for local CI
Build & Run
Target Description
make build Build the Go project
make run Run the example
make testdata Get test data
make clean Remove build artifacts
make format Format Go code
Code Quality
Target Description
make format-check Verify Go code is formatted
make lint Run golangci-lint and hadolint
make vulncheck Run govulncheck vulnerability scanner
make sec Run gosec security scanner
make static-check Run composite static-analysis gate (format, lint, vuln, sec)
make test Run tests with coverage
CI
Target Description
make ci Run full local CI pipeline
make ci-run Run GitHub Actions workflow locally using act
make ci-run-tag Run the tag-gated docker job under act (simulates tag push)
Docker
Target Description
make image-bootstrap Bootstrap Docker buildx multi-platform builder
make image-build Build Docker image (amd64)
make image-run Run Docker image interactively (amd64)
make image-stop Stop any running go-face container
Utilities
Target Description
make help List available tasks
make update Update dependency packages to latest versions
make release Create and push a new tag
make tag-delete Delete a git tag locally and remotely (TAG=vN.N.N)
make renovate-bootstrap Install nvm and node for Renovate
make renovate-validate Validate Renovate configuration

CI/CD

GitHub Actions runs on every push to main, tags (v*), and pull requests. The workflow is also workflow_call-able for downstream reuse.

Per-lineage matrix

go-face publishes one container image per supported dlib-docker major version. The set of supported lineages is declared in .dlib-versions.json and fans out across the entire CI pipeline — static-check, build, test, and docker each run once per active entry, in parallel. A single tag push produces one published image per lineage, and each lineage ends up in its own GHCR sub-package:

Lineage dlib-docker base Image
dlib19 dlib-docker:v19.24.4 ghcr.io/andriykalashnykov/go-face/dlib19

Adding a new lineage is a one-line change to .dlib-versions.json — or you can let the automation do it (see "Automated releases" below).

Jobs
Job Triggers Description
setup push, PR, tags Reads .dlib-versions.json and emits the matrix used by downstream jobs
static-check push, PR, tags make static-check (format-check, lint, vulncheck, sec), once per active dlib lineage
build push, PR, tags make build, once per active dlib lineage
test push, PR, tags make test, once per active dlib lineage
docker push, PR, tags Hardened image pipeline, once per active dlib lineage. Builds and cosign-signs on tag pushes; validation-only on non-tag pushes
ci-pass always Aggregator gate. Fails if any matrix expansion of any job failed or was cancelled

A separate cleanup workflow removes old workflow runs and caches weekly.

The docker job authenticates to GHCR using the built-in GITHUB_TOKEN — no additional secrets are required for publishing. Renovate keeps dependencies (including dlib-docker tag+digest pairs in .dlib-versions.json) up to date with platform automerge enabled.

Pre-push image hardening

The docker job runs on every push (not just tags) so multi-arch build regressions and cosign-installer breakage surface on the commit that introduced them. Login, push, and cosign signing are gated at step-level to tag pushes. Every matrix entry goes through the same five gates — one bad lineage does not block the others (fail-fast: false), but ci-pass still fails the run so no image is published partially:

# Gate Catches Tool
1 Build local single-arch image Build regressions on the runner architecture docker/build-push-action with load: true
2 Trivy image scan (CRITICAL/HIGH blocking) CVEs in the dlib-docker base image, OS packages, and build layers aquasecurity/trivy-action with image-ref:
3 Smoke test Image boots, Go toolchain runs, source files and testdata/ are present docker run invariant checks
4 Multi-arch build + conditional push linux/arm64 cross-compile regressions; publishes both platforms on tag pushes docker/build-push-action
5 Cosign keyless OIDC signing Sigstore signature on the manifest digest (tag-only) sigstore/cosign-installer + cosign sign

Buildkit in-manifest attestations (provenance + sbom) are deliberately disabled so the OCI image index stays free of unknown/unknown platform entries — that lets the GHCR Packages UI render the "OS / Arch" tab for the multi-arch manifest. Cosign keyless signing still provides the Sigstore signature for supply-chain verification. The base image name and digest are recorded in each published manifest as org.opencontainers.image.base.name and org.opencontainers.image.base.digest OCI labels so consumers can see exactly which dlib-docker build produced a given image.

Verify a published image's signature:

cosign verify ghcr.io/andriykalashnykov/go-face/dlib19:<tag> \
  --certificate-identity-regexp 'https://github\.com/AndriyKalashnykov/go-face/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Replace dlib19 with whichever lineage you are pulling.

Automated releases

New dlib-docker releases flow into new go-face releases automatically. There are three workflows involved:

Workflow Trigger What it does
dlib-poll.yml Daily cron + workflow_dispatch Queries the dlib-docker GHCR package, compares the set of observed major versions against .dlib-versions.json, and opens an auto-merging PR that adds any new majors.
renovate (SaaS) On its own schedule Bumps the dlib_docker_tag + dlib_docker_digest pair of existing lineages when dlib-docker repushes an existing tag with a new digest or ships a new patch within an existing major line.
auto-release.yml push to main touching .dlib-versions.json Bumps the patch in version.txt, commits as Cut vX.Y.Z release, creates the matching git tag, and pushes both. The tag push retriggers ci.yml, which matrix-builds and cosign-signs every active lineage.

Together:

  1. dlib-docker ships a new version →
  2. Renovate (for digest bumps) or dlib-poll (for new majors) opens a PR →
  3. CI runs against the proposed update; auto-merge squashes it into main on green →
  4. auto-release bumps version.txt patch, cuts the next tag, pushes →
  5. ci.yml fans out across every active lineage, publishes, and cosign-signs →
  6. Consumers pulling ghcr.io/andriykalashnykov/go-face/dlib19:<latest> get the new build.

Minor and major version bumps stay manual — when you change go-face's own code (not just the base image), use make release to cut a tag directly.

Required secret: RELEASE_PAT

auto-release.yml needs a Personal Access Token to push tags, because tags pushed with GITHUB_TOKEN deliberately do not trigger downstream workflows. Without RELEASE_PAT, the new tag would land in the repo but ci.yml would not fire against it.

Create a classic PAT with these scopes:

  • contents: write — push commits and tags
  • workflow — allows the token to push changes under .github/workflows/ (not strictly required by auto-release.yml today, but future-proofs against workflow edits needing to ship alongside a release)

Add it at Settings → Secrets and variables → Actions → New repository secret with the name RELEASE_PAT. The workflow fails fast with a clear error message if the secret is missing.

A fine-grained PAT scoped to this single repo also works, with the same permissions: Contents: Read and write and Workflows: Read and write. A GitHub App installation token is the production-grade alternative if you want to avoid PAT rotation, but requires more setup.

Models

Currently shape_predictor_5_face_landmarks.dat, mmod_human_face_detector.dat and dlib_face_recognition_resnet_model_v1.dat are required. You may download them from go-face-testdata repo:

make testdata

Or manually:

mkdir testdata
cd testdata
wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat

Usage

To use go-face in your Go code:

import "github.com/AndriyKalashnykov/go-face"

To install go-face in your $GOPATH:

go get github.com/AndriyKalashnykov/go-face

For further details see pkg.go.dev documentation.

Example

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/AndriyKalashnykov/go-face"
)

// Path to directory with models and test images. Here it's assumed it
// points to the <https://github.com/Kagami/go-face-testdata> clone.
const dataDir = "testdata"

var (
	modelsDir = filepath.Join(dataDir, "models")
	imagesDir = filepath.Join(dataDir, "images")
)

// This example shows the basic usage of the package: create an
// recognizer, recognize faces, classify them using few known ones.
func main() {
	// Init the recognizer.
	rec, err := face.NewRecognizer(modelsDir)
	if err != nil {
		log.Fatalf("Can't init face recognizer: %v", err)
	}
	// Free the resources when you're finished.
	defer rec.Close()

	// Test image with 10 faces.
	testImagePristin := filepath.Join(imagesDir, "pristin.jpg")
	// Recognize faces on that image.
	faces, err := rec.RecognizeFile(testImagePristin)
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if len(faces) != 10 {
		log.Fatalf("Wrong number of faces")
	}

	// Fill known samples. In the real world you would use a lot of images
	// for each person to get better classification results but in our
	// example we just get them from one big image.
	var samples []face.Descriptor
	var cats []int32
	for i, f := range faces {
		samples = append(samples, f.Descriptor)
		// Each face is unique on that image so goes to its own category.
		cats = append(cats, int32(i))
	}
	// Name the categories, i.e. people on the image.
	labels := []string{
		"Sungyeon", "Yehana", "Roa", "Eunwoo", "Xiyeon",
		"Kyulkyung", "Nayoung", "Rena", "Kyla", "Yuha",
	}
	// Pass samples to the recognizer.
	rec.SetSamples(samples, cats)

	// Now let's try to classify some not yet known image.
	testImageNayoung := filepath.Join(imagesDir, "nayoung.jpg")
	nayoungFace, err := rec.RecognizeSingleFile(testImageNayoung)
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if nayoungFace == nil {
		log.Fatalf("Not a single face on the image")
	}
	catID := rec.Classify(nayoungFace.Descriptor)
	if catID < 0 {
		log.Fatalf("Can't classify")
	}
	// Finally print the classified label. It should be "Nayoung".
	fmt.Println(labels[catID])
}

Run with:

mkdir -p ~/go && cd ~/go  # Or cd to your $GOPATH
mkdir -p src/go-face-example && cd src/go-face-example
git clone https://github.com/Kagami/go-face-testdata testdata
# Save the example above to main.go
go mod init go-face-example
go get github.com/AndriyKalashnykov/go-face
go run main.go

FAQ

How to improve recognition accuracy

There are few suggestions:

  • Try CNN recognizing
  • Try different tolerance values of ClassifyThreshold
  • Try different size/padding/jittering values of NewRecognizerWithConfig
  • Provide more samples of each category to SetSamples if possible
  • Implement better classify heuristics (see classify.cc)
  • Train network (dlib_face_recognition_resnet_model_v1.dat) on your own test data

License

go-face is licensed under CC0.

Documentation

Overview

Package face implements face recognition for Go using dlib, a popular machine learning toolkit.

Example (Basic)

This example shows the basic usage of the package: create an recognizer, recognize faces, classify them using few known ones.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/AndriyKalashnykov/go-face"
)

// Path to directory with models and test images. Here it's assumed it
// points to the <https://github.com/Kagami/go-face-testdata> clone.
const dataDir = "testdata"

// This example shows the basic usage of the package: create an
// recognizer, recognize faces, classify them using few known ones.
func main() {
	// Init the recognizer.
	rec, err := face.NewRecognizer(filepath.Join(dataDir, "models"))
	if err != nil {
		log.Fatalf("Can't init face recognizer: %v", err)
	}
	// Free the resources when you're finished.
	defer rec.Close()

	// Test image with 10 faces.
	testImagePristin := filepath.Join(dataDir, "images", "pristin.jpg")
	// Recognize faces on that image.
	faces, err := rec.RecognizeFile(testImagePristin)
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if len(faces) != 10 {
		log.Fatalf("Wrong number of faces")
	}

	// Fill known samples. In the real world you would use a lot of images
	// for each person to get better classification results but in our
	// example we just get them from one big image.
	var samples []face.Descriptor
	var cats []int32
	for i, f := range faces {
		samples = append(samples, f.Descriptor)
		// Each face is unique on that image so goes to its own category.
		cats = append(cats, int32(i))
	}
	// Name the categories, i.e. people on the image.
	labels := []string{
		"Sungyeon", "Yehana", "Roa", "Eunwoo", "Xiyeon",
		"Kyulkyung", "Nayoung", "Rena", "Kyla", "Yuha",
	}
	// Pass samples to the recognizer.
	rec.SetSamples(samples, cats)

	// Now let's try to classify some not yet known image.
	testImageNayoung := filepath.Join(dataDir, "images", "nayoung.jpg")
	nayoungFace, err := rec.RecognizeSingleFile(testImageNayoung)
	if err != nil {
		log.Fatalf("Can't recognize: %v", err)
	}
	if nayoungFace == nil {
		log.Fatalf("Not a single face on the image")
	}
	catID := rec.Classify(nayoungFace.Descriptor)
	if catID < 0 {
		log.Fatalf("Can't classify")
	}
	// Finally print the classified label. It should be "Nayoung".
	fmt.Println(labels[catID])
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func SquaredEuclideanDistance

func SquaredEuclideanDistance(d1 Descriptor, d2 Descriptor) (sum float64)

Types

type Descriptor

type Descriptor [128]float32

Descriptor holds 128-dimensional feature vector.

type Face

type Face struct {
	Rectangle  image.Rectangle
	Descriptor Descriptor
	Shapes     []image.Point
}

Face holds coordinates and descriptor of the human face.

func New

func New(r image.Rectangle, d Descriptor) Face

New creates new face with the provided parameters.

func NewWithShape

func NewWithShape(r image.Rectangle, s []image.Point, d Descriptor) Face

type ImageLoadError

type ImageLoadError string

An ImageLoadError is returned when provided image file is corrupted.

func (ImageLoadError) Error

func (e ImageLoadError) Error() string

type Recognizer

type Recognizer struct {
	// contains filtered or unexported fields
}

A Recognizer creates face descriptors for provided images and classifies them into categories.

func NewRecognizer

func NewRecognizer(modelDir string) (rec *Recognizer, err error)

NewRecognizer returns a new recognizer interface. modelDir points to directory with shape_predictor_5_face_landmarks.dat and dlib_face_recognition_resnet_model_v1.dat files.

func NewRecognizerWithConfig

func NewRecognizerWithConfig(modelDir string, size int, padding float32, jittering int) (rec *Recognizer, err error)

func (*Recognizer) Classify

func (rec *Recognizer) Classify(testSample Descriptor) int

Classify returns class ID for the given descriptor. Negative index is returned if no match. Thread-safe.

func (*Recognizer) ClassifyThreshold

func (rec *Recognizer) ClassifyThreshold(testSample Descriptor, tolerance float32) int

Same as Classify but allows to specify max distance between faces to consider it a match. Start with 0.6 if not sure.

func (*Recognizer) Close

func (rec *Recognizer) Close()

Close frees resources taken by the Recognizer. Safe to call multiple times. Don't use Recognizer after close call.

func (*Recognizer) Recognize

func (rec *Recognizer) Recognize(imgData []byte) (faces []Face, err error)

Recognize returns all faces found on the provided image, sorted from left to right. Empty list is returned if there are no faces, error is returned if there was some error while decoding/processing image. Only JPEG format is currently supported. Thread-safe.

func (*Recognizer) RecognizeCNN

func (rec *Recognizer) RecognizeCNN(imgData []byte) (faces []Face, err error)

func (*Recognizer) RecognizeFile

func (rec *Recognizer) RecognizeFile(imgPath string) (faces []Face, err error)

Same as Recognize but accepts image path instead.

func (*Recognizer) RecognizeFileCNN

func (rec *Recognizer) RecognizeFileCNN(imgPath string) (faces []Face, err error)

func (*Recognizer) RecognizeSingle

func (rec *Recognizer) RecognizeSingle(imgData []byte) (face *Face, err error)

RecognizeSingle returns face if it's the only face on the image or nil otherwise. Only JPEG format is currently supported. Thread-safe.

func (*Recognizer) RecognizeSingleCNN

func (rec *Recognizer) RecognizeSingleCNN(imgData []byte) (face *Face, err error)

func (*Recognizer) RecognizeSingleFile

func (rec *Recognizer) RecognizeSingleFile(imgPath string) (face *Face, err error)

Same as RecognizeSingle but accepts image path instead.

func (*Recognizer) RecognizeSingleFileCNN

func (rec *Recognizer) RecognizeSingleFileCNN(imgPath string) (face *Face, err error)

func (*Recognizer) SetSamples

func (rec *Recognizer) SetSamples(samples []Descriptor, cats []int32)

SetSamples sets known descriptors so you can classify the new ones. Thread-safe.

type SerializationError

type SerializationError string

A SerializationError is returned when provided model is corrupted.

func (SerializationError) Error

func (e SerializationError) Error() string

type UnknownError

type UnknownError string

An UnknownError represents some nonclassified error.

func (UnknownError) Error

func (e UnknownError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL