本文提供两种实现方案,接口设计完全一致(人员登记、多样本、单人/多人识别、统计等):
- 方案一:基于 go-face (dlib),需要编译 CGO 依赖,特征为 128 维,使用欧氏距离分类
- 方案二(推荐,效果更好):基于 go-insightface (SCRFD 检测 + ArcFace 特征提取),纯 Go + ONNX Runtime 动态加载,无需 CGO/OpenCV,特征为 512 维,使用余弦相似度 + Top-1/Top-2 margin 精确匹配,识别精度显著更高
方案一:基于 go-face(dlib)
需要下载相关的模型 https://github.com/Kagami/go-face-testdata 下面的models
代码如下
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"unsafe"
"github.com/Kagami/go-face"
"github.com/gorilla/mux"
)
// Config 配置结构
type Config struct {
Port string `json:"port"`
ModelsDir string `json:"models_dir"`
UploadsDir string `json:"uploads_dir"`
TempDir string `json:"temp_dir"`
DataFile string `json:"data_file"`
MaxFileSize int64 `json:"max_file_size"`
DefaultThreshold float32 `json:"default_threshold"`
LogLevel string `json:"log_level"`
}
// Person 人员结构(支持多样本)
type Person struct {
ID int `json:"id"`
Name string `json:"name"`
Samples []FaceSample `json:"samples"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// FaceSample 人脸样本
type FaceSample struct {
ID int `json:"id"`
PersonID int `json:"person_id"`
Descriptor string `json:"descriptor"` // base64编码的特征向量
ImagePath string `json:"image_path"`
ImageURL string `json:"image_url"`
Quality float32 `json:"quality"` // 人脸质量评分
Created time.Time `json:"created"`
}
// RecognitionResult 识别结果
type RecognitionResult struct {
PersonID int `json:"person_id"`
PersonName string `json:"person_name"`
Confidence float32 `json:"confidence"`
Distance float32 `json:"distance"`
SampleID int `json:"sample_id"`
}
// FaceDetection 人脸检测结果
type FaceDetection struct {
Index int `json:"index"`
Rectangle map[string]int `json:"rectangle"`
Recognized bool `json:"recognized"`
Result *RecognitionResult `json:"result,omitempty"`
Message string `json:"message,omitempty"`
}
// Response 通用响应结构
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// Statistics 统计信息
type Statistics struct {
TotalPersons int `json:"total_persons"`
TotalSamples int `json:"total_samples"`
RecognitionCount int `json:"recognition_count"`
RegistrationCount int `json:"registration_count"`
}
// FaceService 人脸识别服务
type FaceService struct {
config *Config
recognizer *face.Recognizer
persons map[int]*Person
samples map[int]*FaceSample
mu sync.RWMutex
nextPersonID int
nextSampleID int
stats Statistics
// 用于分类的数据
classifierSamples []face.Descriptor
classifierCats []int32
classifierLabels []string
}
// NewFaceService 初始化人脸识别服务
func NewFaceService(config *Config) (*FaceService, error) {
// 初始化人脸识别器
rec, err := face.NewRecognizer(config.ModelsDir)
if err != nil {
return nil, fmt.Errorf("无法初始化人脸识别器: %v", err)
}
// 创建必要的目录
dirs := []string{config.UploadsDir, config.TempDir}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("创建目录 %s 失败: %v", dir, err)
}
}
fs := &FaceService{
config: config,
recognizer: rec,
persons: make(map[int]*Person),
samples: make(map[int]*FaceSample),
nextPersonID: 1,
nextSampleID: 1,
classifierSamples: make([]face.Descriptor, 0),
classifierCats: make([]int32, 0),
classifierLabels: make([]string, 0),
}
// 加载已保存的数据
if err := fs.loadData(); err != nil {
log.Printf("加载数据失败: %v", err)
}
return fs, nil
}
// Close 关闭资源
func (fs *FaceService) Close() {
fs.recognizer.Close()
}
// 数据持久化相关方法
func (fs *FaceService) saveData() error {
fs.mu.RLock()
defer fs.mu.RUnlock()
data := struct {
Persons map[int]*Person `json:"persons"`
Samples map[int]*FaceSample `json:"samples"`
NextPersonID int `json:"next_person_id"`
NextSampleID int `json:"next_sample_id"`
Stats Statistics `json:"stats"`
}{
Persons: fs.persons,
Samples: fs.samples,
NextPersonID: fs.nextPersonID,
NextSampleID: fs.nextSampleID,
Stats: fs.stats,
}
file, err := os.Create(fs.config.DataFile)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}
func (fs *FaceService) loadData() error {
if _, err := os.Stat(fs.config.DataFile); os.IsNotExist(err) {
return nil // 文件不存在,使用默认值
}
file, err := os.Open(fs.config.DataFile)
if err != nil {
return err
}
defer file.Close()
var data struct {
Persons map[int]*Person `json:"persons"`
Samples map[int]*FaceSample `json:"samples"`
NextPersonID int `json:"next_person_id"`
NextSampleID int `json:"next_sample_id"`
Stats Statistics `json:"stats"`
}
if err := json.NewDecoder(file).Decode(&data); err != nil {
return err
}
fs.mu.Lock()
fs.persons = data.Persons
fs.samples = data.Samples
fs.nextPersonID = data.NextPersonID
fs.nextSampleID = data.NextSampleID
fs.stats = data.Stats
fs.mu.Unlock()
// 重建分类器
fs.updateClassifier()
log.Printf("加载数据成功: %d个人员, %d个样本", len(fs.persons), len(fs.samples))
return nil
}
// 特征向量转换方法
func descriptorToString(d face.Descriptor) string {
b := (*[128 * 4]byte)(unsafe.Pointer(&d))
return base64.StdEncoding.EncodeToString(b[:])
}
func stringToDescriptor(s string) (face.Descriptor, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return face.Descriptor{}, err
}
if len(b) != 128*4 {
return face.Descriptor{}, fmt.Errorf("invalid descriptor length: %d", len(b))
}
return *(*face.Descriptor)(unsafe.Pointer(&b[0])), nil
}
// 计算人脸质量评分(简单实现)
func (fs *FaceService) calculateFaceQuality(faceData face.Face) float32 {
// 基于人脸区域大小和位置计算质量评分
rect := faceData.Rectangle
width := rect.Max.X - rect.Min.X
height := rect.Max.Y - rect.Min.Y
area := width * height
// 面积越大,质量越高(简化评分)
quality := float32(area) / 10000.0
if quality > 1.0 {
quality = 1.0
}
return quality
}
// 更新分类器
func (fs *FaceService) updateClassifier() {
fs.classifierSamples = make([]face.Descriptor, 0)
fs.classifierCats = make([]int32, 0)
fs.classifierLabels = make([]string, 0)
catID := int32(0)
for _, person := range fs.persons {
for _, sample := range person.Samples {
descriptor, err := stringToDescriptor(sample.Descriptor)
if err != nil {
log.Printf("解析样本 %d 的特征向量失败: %v", sample.ID, err)
continue
}
fs.classifierSamples = append(fs.classifierSamples, descriptor)
fs.classifierCats = append(fs.classifierCats, catID)
fs.classifierLabels = append(fs.classifierLabels, fmt.Sprintf("%d:%d", person.ID, sample.ID))
catID++
}
}
if len(fs.classifierSamples) > 0 {
fs.recognizer.SetSamples(fs.classifierSamples, fs.classifierCats)
log.Printf("分类器更新完成: %d个样本", len(fs.classifierSamples))
}
}
// RegisterPerson 人员登记接口
func (fs *FaceService) RegisterPerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
// 解析表单数据
err := r.ParseMultipartForm(fs.config.MaxFileSize)
if err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
// 获取姓名
name := r.FormValue("name")
if name == "" {
fs.sendErrorResponse(w, "姓名不能为空", http.StatusBadRequest)
return
}
// 检查姓名是否已存在
fs.mu.RLock()
for _, person := range fs.persons {
if person.Name == name {
fs.mu.RUnlock()
fs.sendErrorResponse(w, "该姓名已存在", http.StatusConflict)
return
}
}
fs.mu.RUnlock()
// 获取上传的图片文件
file, handler, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 处理图片并创建样本
sample, err := fs.processImageFile(file, handler, 0) // personID为0,稍后更新
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
// 创建人员记录
fs.mu.Lock()
person := &Person{
ID: fs.nextPersonID,
Name: name,
Samples: []FaceSample{*sample},
Created: time.Now(),
Updated: time.Now(),
}
// 更新样本的人员ID
sample.PersonID = person.ID
fs.persons[person.ID] = person
fs.samples[sample.ID] = sample
fs.nextPersonID++
fs.stats.TotalPersons++
fs.stats.TotalSamples++
fs.stats.RegistrationCount++
// 更新分类器
fs.updateClassifier()
fs.mu.Unlock()
// 保存数据
go fs.saveData()
fs.sendSuccessResponse(w, "人员登记成功", map[string]interface{}{
"person_id": person.ID,
"name": person.Name,
"sample_id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
})
}
// AddSample 为已存在人员添加样本
func (fs *FaceService) AddSample(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personIDStr := vars["person_id"]
personID, err := strconv.Atoi(personIDStr)
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
// 检查人员是否存在
fs.mu.RLock()
person, exists := fs.persons[personID]
if !exists {
fs.mu.RUnlock()
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
fs.mu.RUnlock()
// 解析表单数据
err = r.ParseMultipartForm(fs.config.MaxFileSize)
if err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
// 获取上传的图片文件
file, handler, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 处理图片并创建样本
sample, err := fs.processImageFile(file, handler, personID)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
// 添加样本
fs.mu.Lock()
person.Samples = append(person.Samples, *sample)
person.Updated = time.Now()
fs.samples[sample.ID] = sample
fs.stats.TotalSamples++
// 更新分类器
fs.updateClassifier()
fs.mu.Unlock()
// 保存数据
go fs.saveData()
fs.sendSuccessResponse(w, "样本添加成功", map[string]interface{}{
"person_id": personID,
"sample_id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"total_samples": len(person.Samples),
})
}
// processImageFile 处理上传的图片文件
func (fs *FaceService) processImageFile(file io.Reader, handler *multipart.FileHeader, personID int) (*FaceSample, error) {
// 生成文件名
ext := filepath.Ext(handler.Filename)
if ext == "" {
ext = ".jpg"
}
fs.mu.Lock()
sampleID := fs.nextSampleID
fs.nextSampleID++
fs.mu.Unlock()
savedImagePath := filepath.Join(fs.config.UploadsDir, fmt.Sprintf("sample_%d%s", sampleID, ext))
imageURL := fmt.Sprintf("/uploads/sample_%d%s", sampleID, ext)
// 保存图片文件
dst, err := os.Create(savedImagePath)
if err != nil {
return nil, fmt.Errorf("保存图片失败: %v", err)
}
defer dst.Close()
_, err = io.Copy(dst, file)
if err != nil {
os.Remove(savedImagePath)
return nil, fmt.Errorf("保存图片失败: %v", err)
}
// 人脸识别
faceResult, err := fs.recognizer.RecognizeSingleFile(savedImagePath)
if err != nil {
os.Remove(savedImagePath)
var imageLoadError face.ImageLoadError
if errors.As(err, &imageLoadError) {
return nil, fmt.Errorf("图片格式不支持或已损坏")
}
return nil, fmt.Errorf("人脸识别失败: %v", err)
}
if faceResult == nil {
os.Remove(savedImagePath)
return nil, fmt.Errorf("未检测到人脸")
}
// 计算人脸质量
quality := fs.calculateFaceQuality(*faceResult)
// 创建样本
sample := &FaceSample{
ID: sampleID,
PersonID: personID,
Descriptor: descriptorToString(faceResult.Descriptor),
ImagePath: savedImagePath,
ImageURL: imageURL,
Quality: quality,
Created: time.Now(),
}
return sample, nil
}
// RecognizeFace 人脸识别接口
func (fs *FaceService) RecognizeFace(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
// 解析表单数据
err := r.ParseMultipartForm(fs.config.MaxFileSize)
if err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
// 获取阈值参数
threshold := fs.config.DefaultThreshold
if thresholdStr := r.FormValue("threshold"); thresholdStr != "" {
if t, err := strconv.ParseFloat(thresholdStr, 32); err == nil {
threshold = float32(t)
}
}
// 处理上传的图片
tempFile, err := fs.saveTemporaryFile(r)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
defer os.Remove(tempFile)
fs.mu.RLock()
if len(fs.persons) == 0 {
fs.mu.RUnlock()
fs.sendErrorResponse(w, "暂无已登记的人脸数据", http.StatusBadRequest)
return
}
fs.mu.RUnlock()
// 识别人脸
detectedFace, err := fs.recognizer.RecognizeSingleFile(tempFile)
if err != nil {
var imageLoadError face.ImageLoadError
if errors.As(err, &imageLoadError) {
fs.sendErrorResponse(w, "图片格式不支持或已损坏", http.StatusBadRequest)
} else {
fs.sendErrorResponse(w, "人脸识别失败", http.StatusInternalServerError)
}
return
}
if detectedFace == nil {
fs.sendErrorResponse(w, "未检测到人脸", http.StatusBadRequest)
return
}
// 执行分类
result := fs.classifyFace(detectedFace.Descriptor, threshold)
fs.mu.Lock()
fs.stats.RecognitionCount++
fs.mu.Unlock()
go fs.saveData()
if result == nil {
fs.sendSuccessResponse(w, "未找到匹配的人脸", map[string]interface{}{
"recognized": false,
})
} else {
fs.sendSuccessResponse(w, "人脸识别成功", map[string]interface{}{
"recognized": true,
"result": result,
})
}
}
// RecognizeMultipleFaces 多人脸识别接口
func (fs *FaceService) RecognizeMultipleFaces(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
// 解析表单数据
err := r.ParseMultipartForm(fs.config.MaxFileSize)
if err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
// 获取阈值参数
threshold := fs.config.DefaultThreshold
if thresholdStr := r.FormValue("threshold"); thresholdStr != "" {
if t, err := strconv.ParseFloat(thresholdStr, 32); err == nil {
threshold = float32(t)
}
}
// 处理上传的图片
tempFile, err := fs.saveTemporaryFile(r)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
defer os.Remove(tempFile)
fs.mu.RLock()
if len(fs.persons) == 0 {
fs.mu.RUnlock()
fs.sendErrorResponse(w, "暂无已登记的人脸数据", http.StatusBadRequest)
return
}
fs.mu.RUnlock()
// 识别所有人脸
faces, err := fs.recognizer.RecognizeFile(tempFile)
if err != nil {
var imageLoadError face.ImageLoadError
if errors.As(err, &imageLoadError) {
fs.sendErrorResponse(w, "图片格式不支持或已损坏", http.StatusBadRequest)
} else {
fs.sendErrorResponse(w, "人脸识别失败", http.StatusInternalServerError)
}
return
}
if len(faces) == 0 {
fs.sendErrorResponse(w, "未检测到人脸", http.StatusBadRequest)
return
}
// 处理每个检测到的人脸
var detections []FaceDetection
for i, detectedFace := range faces {
detection := FaceDetection{
Index: i,
Rectangle: map[string]int{
"left": detectedFace.Rectangle.Min.X,
"top": detectedFace.Rectangle.Min.Y,
"right": detectedFace.Rectangle.Max.X,
"bottom": detectedFace.Rectangle.Max.Y,
},
}
// 尝试识别
result := fs.classifyFace(detectedFace.Descriptor, threshold)
if result != nil {
detection.Recognized = true
detection.Result = result
} else {
detection.Recognized = false
detection.Message = "未找到匹配的人脸"
}
detections = append(detections, detection)
}
fs.mu.Lock()
fs.stats.RecognitionCount++
fs.mu.Unlock()
go fs.saveData()
fs.sendSuccessResponse(w, fmt.Sprintf("检测到%d张人脸", len(faces)), detections)
}
// classifyFace 分类人脸
func (fs *FaceService) classifyFace(descriptor face.Descriptor, threshold float32) *RecognitionResult {
fs.mu.RLock()
defer fs.mu.RUnlock()
if len(fs.classifierSamples) == 0 {
return nil
}
catID := fs.recognizer.ClassifyThreshold(descriptor, threshold)
if catID < 0 || catID >= len(fs.classifierLabels) {
return nil
}
// 解析标签(格式:personID:sampleID)
label := fs.classifierLabels[catID]
var personID, sampleID int
if n, err := fmt.Sscanf(label, "%d:%d", &personID, &sampleID); n != 2 || err != nil {
return nil
}
person, personExists := fs.persons[personID]
sample, sampleExists := fs.samples[sampleID]
if !personExists || !sampleExists {
return nil
}
// 计算相似度
sampleDescriptor, err := stringToDescriptor(sample.Descriptor)
if err != nil {
return nil
}
distance := fs.calculateDistance(descriptor, sampleDescriptor)
confidence := (1 - distance) * 100
if confidence < 0 {
confidence = 0
}
return &RecognitionResult{
PersonID: person.ID,
PersonName: person.Name,
Confidence: confidence,
Distance: distance,
SampleID: sample.ID,
}
}
// calculateDistance 计算欧几里得距离
func (fs *FaceService) calculateDistance(desc1, desc2 face.Descriptor) float32 {
var sum float64
for i := 0; i < len(desc1); i++ {
diff := float64(desc1[i] - desc2[i])
sum += diff * diff
}
return float32(math.Sqrt(sum))
}
// saveTemporaryFile 保存临时文件
func (fs *FaceService) saveTemporaryFile(r *http.Request) (string, error) {
file, handler, err := r.FormFile("image")
if err != nil {
return "", fmt.Errorf("获取图片文件失败: %v", err)
}
defer file.Close()
tempFile := filepath.Join(fs.config.TempDir, fmt.Sprintf("temp_%d_%s", time.Now().UnixNano(), handler.Filename))
dst, err := os.Create(tempFile)
if err != nil {
return "", fmt.Errorf("创建临时文件失败: %v", err)
}
defer dst.Close()
_, err = io.Copy(dst, file)
if err != nil {
os.Remove(tempFile)
return "", fmt.Errorf("保存图片失败: %v", err)
}
return tempFile, nil
}
// GetPersonList 获取人员列表
func (fs *FaceService) GetPersonList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
defer fs.mu.RUnlock()
var personList []map[string]interface{}
for _, person := range fs.persons {
personInfo := map[string]interface{}{
"id": person.ID,
"name": person.Name,
"sample_count": len(person.Samples),
"created": person.Created,
"updated": person.Updated,
}
// 添加样本信息
var samples []map[string]interface{}
for _, sample := range person.Samples {
samples = append(samples, map[string]interface{}{
"id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"created": sample.Created,
})
}
personInfo["samples"] = samples
personList = append(personList, personInfo)
}
fs.sendSuccessResponse(w, "获取成功", personList)
}
// DeletePerson 删除人员
func (fs *FaceService) DeletePerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
fs.sendErrorResponse(w, "只支持DELETE方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personIDStr := vars["person_id"]
personID, err := strconv.Atoi(personIDStr)
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
person, exists := fs.persons[personID]
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
// 删除所有样本文件和数据
for _, sample := range person.Samples {
if sample.ImagePath != "" {
if err := os.Remove(sample.ImagePath); err != nil {
log.Printf("删除图片文件失败: %v", err)
}
}
delete(fs.samples, sample.ID)
fs.stats.TotalSamples--
}
// 删除人员数据
delete(fs.persons, personID)
fs.stats.TotalPersons--
// 更新分类器
fs.updateClassifier()
// 保存数据
go fs.saveData()
fs.sendSuccessResponse(w, "删除成功", nil)
}
// DeleteSample 删除样本
func (fs *FaceService) DeleteSample(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
fs.sendErrorResponse(w, "只支持DELETE方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
sampleIDStr := vars["sample_id"]
sampleID, err := strconv.Atoi(sampleIDStr)
if err != nil {
fs.sendErrorResponse(w, "无效的样本ID", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
sample, exists := fs.samples[sampleID]
if !exists {
fs.sendErrorResponse(w, "样本不存在", http.StatusNotFound)
return
}
person, personExists := fs.persons[sample.PersonID]
if !personExists {
fs.sendErrorResponse(w, "关联的人员不存在", http.StatusNotFound)
return
}
// 检查是否为最后一个样本
if len(person.Samples) <= 1 {
fs.sendErrorResponse(w, "不能删除最后一个样本,请删除整个人员", http.StatusBadRequest)
return
}
// 删除图片文件
if sample.ImagePath != "" {
if err := os.Remove(sample.ImagePath); err != nil {
log.Printf("删除图片文件失败: %v", err)
}
}
// 从人员的样本列表中移除
for i, s := range person.Samples {
if s.ID == sampleID {
person.Samples = append(person.Samples[:i], person.Samples[i+1:]...)
break
}
}
person.Updated = time.Now()
// 删除样本数据
delete(fs.samples, sampleID)
fs.stats.TotalSamples--
// 更新分类器
fs.updateClassifier()
// 保存数据
go fs.saveData()
fs.sendSuccessResponse(w, "样本删除成功", nil)
}
// GetStatistics 获取统计信息
func (fs *FaceService) GetStatistics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
stats := fs.stats
fs.mu.RUnlock()
fs.sendSuccessResponse(w, "获取统计信息成功", stats)
}
// GetPersonDetail 获取人员详情
func (fs *FaceService) GetPersonDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personIDStr := vars["person_id"]
personID, err := strconv.Atoi(personIDStr)
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
fs.mu.RLock()
person, exists := fs.persons[personID]
fs.mu.RUnlock()
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
// 构建详细信息
personDetail := map[string]interface{}{
"id": person.ID,
"name": person.Name,
"sample_count": len(person.Samples),
"created": person.Created,
"updated": person.Updated,
}
// 添加样本详情
var samples []map[string]interface{}
for _, sample := range person.Samples {
samples = append(samples, map[string]interface{}{
"id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"created": sample.Created,
})
}
personDetail["samples"] = samples
fs.sendSuccessResponse(w, "获取人员详情成功", personDetail)
}
// UpdatePersonName 更新人员姓名
func (fs *FaceService) UpdatePersonName(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
fs.sendErrorResponse(w, "只支持PUT方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personIDStr := vars["person_id"]
personID, err := strconv.Atoi(personIDStr)
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
// 解析JSON请求体
var requestData struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
fs.sendErrorResponse(w, "解析请求数据失败", http.StatusBadRequest)
return
}
if requestData.Name == "" {
fs.sendErrorResponse(w, "姓名不能为空", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
person, exists := fs.persons[personID]
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
// 检查新姓名是否已存在
for _, p := range fs.persons {
if p.ID != personID && p.Name == requestData.Name {
fs.sendErrorResponse(w, "该姓名已存在", http.StatusConflict)
return
}
}
// 更新姓名
person.Name = requestData.Name
person.Updated = time.Now()
// 保存数据
go fs.saveData()
fs.sendSuccessResponse(w, "姓名更新成功", map[string]interface{}{
"id": person.ID,
"name": person.Name,
})
}
// HealthCheck 健康检查
func (fs *FaceService) HealthCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
totalPersons := len(fs.persons)
totalSamples := len(fs.samples)
fs.mu.RUnlock()
fs.sendSuccessResponse(w, "服务正常", map[string]interface{}{
"status": "healthy",
"total_persons": totalPersons,
"total_samples": totalSamples,
"timestamp": time.Now(),
})
}
// 响应辅助方法
func (fs *FaceService) sendSuccessResponse(w http.ResponseWriter, message string, data interface{}) {
fs.sendResponse(w, true, message, data, http.StatusOK)
}
func (fs *FaceService) sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
fs.sendResponse(w, false, message, nil, statusCode)
}
func (fs *FaceService) sendResponse(w http.ResponseWriter, success bool, message string, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
response := Response{
Success: success,
Message: message,
Data: data,
Timestamp: time.Now(),
}
json.NewEncoder(w).Encode(response)
}
// 加载配置
func loadConfig(configPath string) (*Config, error) {
// 默认配置
config := &Config{
Port: ":8080",
ModelsDir: "models",
UploadsDir: "uploads",
TempDir: "temp",
DataFile: "face_data.json",
MaxFileSize: 10 << 20, // 10MB
DefaultThreshold: 0.6,
LogLevel: "info",
}
// 如果配置文件存在,则加载配置
if _, err := os.Stat(configPath); err == nil {
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("打开配置文件失败: %v", err)
}
defer file.Close()
if err := json.NewDecoder(file).Decode(config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %v", err)
}
}
return config, nil
}
// 定期保存数据的后台任务
func (fs *FaceService) startAutoSave() {
go func() {
ticker := time.NewTicker(5 * time.Minute) // 每5分钟保存一次
defer ticker.Stop()
for range ticker.C {
if err := fs.saveData(); err != nil {
log.Printf("自动保存数据失败: %v", err)
}
}
}()
}
func main() {
// 加载配置
configPath := "config.json"
if len(os.Args) > 1 {
configPath = os.Args[1]
}
config, err := loadConfig(configPath)
if err != nil {
log.Printf("加载配置失败,使用默认配置: %v", err)
config = &Config{
Port: ":8080",
ModelsDir: "models",
UploadsDir: "uploads",
TempDir: "temp",
DataFile: "face_data.json",
MaxFileSize: 10 << 20,
DefaultThreshold: 0.6,
LogLevel: "info",
}
}
// 初始化人脸识别服务
faceService, err := NewFaceService(config)
if err != nil {
log.Fatal("初始化人脸识别服务失败:", err)
}
defer faceService.Close()
// 启动自动保存
faceService.startAutoSave()
// 创建路由
r := mux.NewRouter()
// API路由
api := r.PathPrefix("/api/v1").Subrouter()
// 人员管理
api.HandleFunc("/person/register", faceService.RegisterPerson).Methods("POST")
api.HandleFunc("/person/list", faceService.GetPersonList).Methods("GET")
api.HandleFunc("/person/{person_id}", faceService.GetPersonDetail).Methods("GET")
api.HandleFunc("/person/{person_id}", faceService.UpdatePersonName).Methods("PUT")
api.HandleFunc("/person/{person_id}", faceService.DeletePerson).Methods("DELETE")
// 样本管理
api.HandleFunc("/person/{person_id}/sample", faceService.AddSample).Methods("POST")
api.HandleFunc("/sample/{sample_id}", faceService.DeleteSample).Methods("DELETE")
// 识别接口
api.HandleFunc("/face/recognize", faceService.RecognizeFace).Methods("POST")
api.HandleFunc("/face/recognize-multiple", faceService.RecognizeMultipleFaces).Methods("POST")
// 统计和健康检查
api.HandleFunc("/statistics", faceService.GetStatistics).Methods("GET")
api.HandleFunc("/health", faceService.HealthCheck).Methods("GET")
// 静态文件服务
r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.UploadsDir))))
// CORS中间件
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
})
// 启动服务
fmt.Printf("=== 人脸识别服务启动 ===\n")
fmt.Printf("端口: %s\n", config.Port)
fmt.Printf("模型路径: %s\n", config.ModelsDir)
fmt.Printf("上传目录: %s\n", config.UploadsDir)
fmt.Printf("数据文件: %s\n", config.DataFile)
fmt.Printf("默认阈值: %.2f\n", config.DefaultThreshold)
fmt.Printf("最大文件大小: %d MB\n", config.MaxFileSize/(1024*1024))
fmt.Println("\n=== API接口列表 ===")
fmt.Println("人员管理:")
fmt.Println(" POST /api/v1/person/register - 人员登记")
fmt.Println(" GET /api/v1/person/list - 获取人员列表")
fmt.Println(" GET /api/v1/person/{id} - 获取人员详情")
fmt.Println(" PUT /api/v1/person/{id} - 更新人员姓名")
fmt.Println(" DELETE /api/v1/person/{id} - 删除人员")
fmt.Println("\n样本管理:")
fmt.Println(" POST /api/v1/person/{id}/sample - 添加样本")
fmt.Println(" DELETE /api/v1/sample/{id} - 删除样本")
fmt.Println("\n识别接口:")
fmt.Println(" POST /api/v1/face/recognize - 单人脸识别")
fmt.Println(" POST /api/v1/face/recognize-multiple - 多人脸识别")
fmt.Println("\n系统接口:")
fmt.Println(" GET /api/v1/statistics - 获取统计信息")
fmt.Println(" GET /api/v1/health - 健康检查")
fmt.Println("\n静态文件:")
fmt.Println(" GET /uploads/* - 图片文件访问")
log.Fatal(http.ListenAndServe(config.Port, r))
配置文件 (config.json)
{
"port": ":8080",
"models_dir": "models",
"uploads_dir": "uploads",
"temp_dir": "temp",
"data_file": "face_data.json",
"max_file_size": 10485760,
"default_threshold": 0.6,
"log_level": "info"
}
⚠️ 使用建议
样本采集
- 每人建议采集3-8个高质量样本
- 包含不同角度:正面、左侧、右侧
- 包含不同表情:微笑、严肃
- 确保良好的光线条件
- 避免模糊、遮挡的图片
阈值设置
- 默认阈值0.6适用于大多数场景
- 安全性要求高的场景可提高到0.7-0.8
- 便利性要求高的场景可降低到0.4-0.5
- 建议根据实际测试效果调整
方案二:基于 go-insightface(SCRFD + ArcFace)
go-insightface 用纯 Go 运行 SCRFD 人脸检测和 ArcFace 特征提取:通过 onnxruntime-purego 动态加载 ONNX Runtime,不需要 CGO 和 OpenCV,免去了 dlib 的编译地狱。SCRFD 是目前主流的高精度检测器(自带 5 点关键点),ArcFace 512 维特征比 dlib 的 128 维特征识别精度高一个量级;匹配器还自带 Top-1/Top-2 margin 检查,显著降低误识别。
依赖与模型准备
要求 Go 1.25+:
go mod init face-service
go get github.com/lib-x/go-insightface@v0.2.0
go get github.com/gorilla/mux
运行时需要三个文件:
- ONNX Runtime 1.23.x 动态库(
libonnxruntime.so.1.23.2) - SCRFD 检测模型(
det_10g.onnx) - ArcFace 识别模型(
w600k_r50.onnx)
# 1. ONNX Runtime 1.23.2(Linux x86_64)
curl -fL -o onnxruntime.tgz \
https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-linux-x64-1.23.2.tgz
tar -xzf onnxruntime.tgz
# 动态库在 onnxruntime-linux-x64-1.23.2/lib/libonnxruntime.so.1.23.2
# 2. 模型(SCRFD 检测 + ArcFace 识别)
mkdir -p models
curl -fL -o models/det_10g.onnx \
https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/det_10g.onnx
curl -fL -o models/w600k_r50.onnx \
https://github.com/yakhyo/face-reidentification/releases/download/v0.0.1/w600k_r50.onnx
⚠️ 许可与模型空间
- 上述 InsightFace 系模型为研究用途,生产/商用环境需使用有明确授权的模型或自行训练的权重
- 特征向量只在产生它的模型空间内有效(不同 ArcFace 模型的向量不可互相比较),因此本文实现把模型版本与每个样本一起持久化,更换模型后需重新登记样本
代码实现
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"log"
"math"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/gorilla/mux"
insightface "github.com/lib-x/go-insightface"
)
// Config 配置结构
type Config struct {
Port string `json:"port"`
RuntimeLibrary string `json:"runtime_library"` // libonnxruntime.so 动态库路径
DetectorModel string `json:"detector_model"` // SCRFD 检测模型
RecognizerModel string `json:"recognizer_model"` // ArcFace 特征提取模型
ModelVersion string `json:"model_version"` // 模型标识,特征向量只在同一模型空间内可比
UploadsDir string `json:"uploads_dir"`
DataFile string `json:"data_file"`
MaxFileSize int64 `json:"max_file_size"`
MinSimilarity float32 `json:"min_similarity"` // 余弦相似度阈值
MinMargin float32 `json:"min_margin"` // 与次优不同身份的相似度差距阈值
LogLevel string `json:"log_level"`
}
// Person 人员结构(支持多样本)
type Person struct {
ID int `json:"id"`
Name string `json:"name"`
Samples []FaceSample `json:"samples"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// FaceSample 人脸样本
type FaceSample struct {
ID int `json:"id"`
PersonID int `json:"person_id"`
Descriptor string `json:"descriptor"` // base64编码的512维特征向量
Model string `json:"model"` // 生成该向量的模型版本
ImagePath string `json:"image_path"`
ImageURL string `json:"image_url"`
Quality float32 `json:"quality"` // 人脸质量评分
Created time.Time `json:"created"`
}
// RecognitionResult 识别结果
type RecognitionResult struct {
PersonID int `json:"person_id"`
PersonName string `json:"person_name"`
Similarity float32 `json:"similarity"` // 余弦相似度
Confidence float32 `json:"confidence"` // 相似度百分比
Margin float32 `json:"margin"` // 与次优不同身份的差距
SampleID int `json:"sample_id"`
DetScore float32 `json:"det_score"` // 人脸检测置信度
}
// FaceDetection 多人识别中的单张人脸
type FaceDetection struct {
Index int `json:"index"`
Rectangle map[string]float32 `json:"rectangle"`
Score float32 `json:"score"`
Recognized bool `json:"recognized"`
Result *RecognitionResult `json:"result,omitempty"`
Message string `json:"message,omitempty"`
}
// Response 通用响应结构
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// Statistics 统计信息
type Statistics struct {
TotalPersons int `json:"total_persons"`
TotalSamples int `json:"total_samples"`
RecognitionCount int `json:"recognition_count"`
RegistrationCount int `json:"registration_count"`
}
// FaceService 人脸识别服务
type FaceService struct {
config *Config
engine *insightface.Engine
matcher *insightface.Matcher
persons map[int]*Person
samples map[int]*FaceSample
mu sync.RWMutex
nextPersonID int
nextSampleID int
stats Statistics
}
// NewFaceService 初始化人脸识别服务
func NewFaceService(config *Config) (*FaceService, error) {
// 初始化识别引擎(SCRFD 检测 + ArcFace 特征提取,无 CGO)
engine, err := insightface.New(insightface.Config{
RuntimeLibrary: config.RuntimeLibrary,
DetectorModel: config.DetectorModel,
RecognizerModel: config.RecognizerModel,
})
if err != nil {
return nil, fmt.Errorf("初始化识别引擎失败: %v", err)
}
if err := os.MkdirAll(config.UploadsDir, 0755); err != nil {
engine.Close()
return nil, fmt.Errorf("创建目录 %s 失败: %v", config.UploadsDir, err)
}
fs := &FaceService{
config: config,
engine: engine,
persons: make(map[int]*Person),
samples: make(map[int]*FaceSample),
nextPersonID: 1,
nextSampleID: 1,
}
if err := fs.loadData(); err != nil {
log.Printf("加载数据失败: %v", err)
}
return fs, nil
}
// Close 关闭引擎(幂等,等待进行中的推理完成)
func (fs *FaceService) Close() {
fs.engine.Close()
}
// ===== 特征向量序列化 =====
// embeddingToBytes 将 L2 归一化的 Embedding 序列化为字节
func embeddingToBytes(e insightface.Embedding) []byte {
values := e.Values() // 512 个 float32
buf := make([]byte, len(values)*4)
for i, v := range values {
binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(v))
}
return buf
}
// bytesToEmbedding 从字节还原 Embedding(内部会校验并再次 L2 归一化)
func bytesToEmbedding(b []byte) (insightface.Embedding, error) {
if len(b) == 0 || len(b)%4 != 0 {
return insightface.Embedding{}, fmt.Errorf("无效的特征向量长度: %d", len(b))
}
raw := make([]float32, len(b)/4)
for i := range raw {
raw[i] = math.Float32frombits(binary.LittleEndian.Uint32(b[i*4:]))
}
return insightface.NewEmbedding(raw)
}
func descriptorToString(e insightface.Embedding) string {
return base64.StdEncoding.EncodeToString(embeddingToBytes(e))
}
func stringToDescriptor(s string) (insightface.Embedding, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return insightface.Embedding{}, err
}
return bytesToEmbedding(b)
}
// ===== 数据持久化 =====
func (fs *FaceService) saveData() error {
fs.mu.RLock()
defer fs.mu.RUnlock()
data := struct {
Persons map[int]*Person `json:"persons"`
Samples map[int]*FaceSample `json:"samples"`
NextPersonID int `json:"next_person_id"`
NextSampleID int `json:"next_sample_id"`
Stats Statistics `json:"stats"`
}{
Persons: fs.persons,
Samples: fs.samples,
NextPersonID: fs.nextPersonID,
NextSampleID: fs.nextSampleID,
Stats: fs.stats,
}
file, err := os.Create(fs.config.DataFile)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}
func (fs *FaceService) loadData() error {
if _, err := os.Stat(fs.config.DataFile); os.IsNotExist(err) {
return nil
}
file, err := os.Open(fs.config.DataFile)
if err != nil {
return err
}
defer file.Close()
var data struct {
Persons map[int]*Person `json:"persons"`
Samples map[int]*FaceSample `json:"samples"`
NextPersonID int `json:"next_person_id"`
NextSampleID int `json:"next_sample_id"`
Stats Statistics `json:"stats"`
}
if err := json.NewDecoder(file).Decode(&data); err != nil {
return err
}
fs.mu.Lock()
fs.persons = data.Persons
fs.samples = data.Samples
fs.nextPersonID = data.NextPersonID
fs.nextSampleID = data.NextSampleID
fs.stats = data.Stats
fs.mu.Unlock()
// 检查模型版本一致性:特征向量只在产生它的模型空间内有效
if fs.config.ModelVersion != "" {
mismatch := 0
for _, sample := range fs.samples {
if sample.Model != "" && sample.Model != fs.config.ModelVersion {
mismatch++
}
}
if mismatch > 0 {
log.Printf("警告: %d 个样本由其他模型生成,其特征与当前模型不可比,建议重新登记", mismatch)
}
}
// 重建匹配器
fs.mu.Lock()
err = fs.rebuildMatcher()
fs.mu.Unlock()
if err != nil {
return fmt.Errorf("重建匹配器失败: %v", err)
}
log.Printf("加载数据成功: %d个人员, %d个样本", len(fs.persons), len(fs.samples))
return nil
}
// ===== 匹配器管理 =====
// rebuildMatcher 根据当前人员样本重建匹配器(每个人员一个身份,多样本取最佳)
func (fs *FaceService) rebuildMatcher() error {
gallery := make([]insightface.GalleryIdentity, 0, len(fs.persons))
for _, person := range fs.persons {
gi := insightface.GalleryIdentity{ID: strconv.Itoa(person.ID)}
for _, sample := range person.Samples {
emb, err := stringToDescriptor(sample.Descriptor)
if err != nil {
log.Printf("解析样本 %d 的特征向量失败: %v", sample.ID, err)
continue
}
gi.Samples = append(gi.Samples, emb)
}
if len(gi.Samples) > 0 {
gallery = append(gallery, gi)
}
}
if fs.matcher == nil {
m, err := insightface.NewMatcher(insightface.MatcherConfig{
MinSimilarity: fs.config.MinSimilarity,
MinMargin: fs.config.MinMargin,
}, gallery)
if err != nil {
return err
}
fs.matcher = m
return nil
}
return fs.matcher.ReplaceGallery(gallery)
}
// ===== 图片处理 =====
// decodeImage 从内存解码图片(jpeg/png/gif)
func decodeImage(data []byte) (image.Image, error) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("不支持的图片格式或图片已损坏")
}
return img, nil
}
// calculateFaceQuality 质量评分:检测置信度 * 人脸面积因子
func calculateFaceQuality(f insightface.Face) float32 {
box := f.Detection.Box
area := (box.Right - box.Left) * (box.Bottom - box.Top)
sizeFactor := area / 40000.0 // 200x200 视为满分
if sizeFactor > 1 {
sizeFactor = 1
}
quality := f.Detection.Score * sizeFactor
if quality > 1 {
quality = 1
}
return quality
}
// matchFace 在匹配器中识别单张人脸,返回 nil 表示未识别
func (fs *FaceService) matchFace(face insightface.Face, minSimilarity float32) *RecognitionResult {
fs.mu.RLock()
matcher := fs.matcher
fs.mu.RUnlock()
if matcher == nil {
return nil
}
result, err := matcher.Match(face.Embedding)
if err != nil || !result.Accepted {
return nil
}
// 请求级阈值覆盖(比配置阈值更严格时)
if minSimilarity > 0 && result.Best.Similarity < minSimilarity {
return nil
}
personID, err := strconv.Atoi(result.Best.Identity)
if err != nil {
return nil
}
fs.mu.RLock()
person, ok := fs.persons[personID]
if !ok {
fs.mu.RUnlock()
return nil
}
name := person.Name
sampleID := 0
if result.Best.SampleIndex >= 0 && result.Best.SampleIndex < len(person.Samples) {
sampleID = person.Samples[result.Best.SampleIndex].ID
}
fs.mu.RUnlock()
return &RecognitionResult{
PersonID: personID,
PersonName: name,
Similarity: result.Best.Similarity,
Confidence: result.Best.Similarity * 100,
Margin: result.Margin,
SampleID: sampleID,
DetScore: face.Detection.Score,
}
}
// ===== 登记 =====
// RegisterPerson 人员登记接口
func (fs *FaceService) RegisterPerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
if err := r.ParseMultipartForm(fs.config.MaxFileSize); err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
name := r.FormValue("name")
if name == "" {
fs.sendErrorResponse(w, "姓名不能为空", http.StatusBadRequest)
return
}
fs.mu.RLock()
for _, person := range fs.persons {
if person.Name == name {
fs.mu.RUnlock()
fs.sendErrorResponse(w, "该姓名已存在", http.StatusConflict)
return
}
}
fs.mu.RUnlock()
file, handler, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
data, err := io.ReadAll(file)
file.Close()
if err != nil {
fs.sendErrorResponse(w, "读取图片失败", http.StatusBadRequest)
return
}
sample, err := fs.createSample(data, handler.Filename, 0)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
fs.mu.Lock()
person := &Person{
ID: fs.nextPersonID,
Name: name,
Samples: []FaceSample{*sample},
Created: time.Now(),
Updated: time.Now(),
}
sample.PersonID = person.ID
fs.persons[person.ID] = person
fs.samples[sample.ID] = sample
fs.nextPersonID++
fs.stats.TotalPersons++
fs.stats.TotalSamples++
fs.stats.RegistrationCount++
if err := fs.rebuildMatcher(); err != nil {
fs.mu.Unlock()
fs.sendErrorResponse(w, "更新匹配器失败", http.StatusInternalServerError)
return
}
fs.mu.Unlock()
go fs.saveData()
fs.sendSuccessResponse(w, "人员登记成功", map[string]interface{}{
"person_id": person.ID,
"name": person.Name,
"sample_id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
})
}
// AddSample 为已存在人员添加样本
func (fs *FaceService) AddSample(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personID, err := strconv.Atoi(vars["person_id"])
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
fs.mu.RLock()
person, exists := fs.persons[personID]
fs.mu.RUnlock()
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
if err := r.ParseMultipartForm(fs.config.MaxFileSize); err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
file, handler, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
data, err := io.ReadAll(file)
file.Close()
if err != nil {
fs.sendErrorResponse(w, "读取图片失败", http.StatusBadRequest)
return
}
sample, err := fs.createSample(data, handler.Filename, personID)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
fs.mu.Lock()
person.Samples = append(person.Samples, *sample)
person.Updated = time.Now()
fs.samples[sample.ID] = sample
fs.stats.TotalSamples++
if err := fs.rebuildMatcher(); err != nil {
fs.mu.Unlock()
fs.sendErrorResponse(w, "更新匹配器失败", http.StatusInternalServerError)
return
}
fs.mu.Unlock()
go fs.saveData()
fs.sendSuccessResponse(w, "样本添加成功", map[string]interface{}{
"person_id": personID,
"sample_id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"total_samples": len(person.Samples),
})
}
// createSample 处理上传图片:保存文件、检测人脸、生成特征样本
func (fs *FaceService) createSample(data []byte, filename string, personID int) (*FaceSample, error) {
// 直接从内存解码图片,无需临时文件
img, err := decodeImage(data)
if err != nil {
return nil, err
}
// SCRFD 检测 + ArcFace 特征提取
faces, err := fs.engine.Analyze(context.Background(), img)
if err != nil {
return nil, fmt.Errorf("人脸分析失败: %v", err)
}
if len(faces) == 0 {
return nil, fmt.Errorf("未检测到人脸")
}
// 选置信度最高的人脸
best := faces[0]
for _, f := range faces[1:] {
if f.Detection.Score > best.Detection.Score {
best = f
}
}
fs.mu.Lock()
sampleID := fs.nextSampleID
fs.nextSampleID++
fs.mu.Unlock()
ext := filepath.Ext(filename)
if ext == "" {
ext = ".jpg"
}
savedPath := filepath.Join(fs.config.UploadsDir, fmt.Sprintf("sample_%d%s", sampleID, ext))
imageURL := fmt.Sprintf("/uploads/sample_%d%s", sampleID, ext)
if err := os.WriteFile(savedPath, data, 0644); err != nil {
return nil, fmt.Errorf("保存图片失败: %v", err)
}
return &FaceSample{
ID: sampleID,
PersonID: personID,
Descriptor: descriptorToString(best.Embedding),
Model: fs.config.ModelVersion,
ImagePath: savedPath,
ImageURL: imageURL,
Quality: calculateFaceQuality(best),
Created: time.Now(),
}, nil
}
// ===== 识别 =====
// RecognizeFace 单人脸识别接口
func (fs *FaceService) RecognizeFace(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
if err := r.ParseMultipartForm(fs.config.MaxFileSize); err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
// 可选:请求级相似度阈值覆盖
threshold := float32(0)
if thresholdStr := r.FormValue("threshold"); thresholdStr != "" {
if t, err := strconv.ParseFloat(thresholdStr, 32); err == nil {
threshold = float32(t)
}
}
file, _, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
data, err := io.ReadAll(file)
file.Close()
if err != nil {
fs.sendErrorResponse(w, "读取图片失败", http.StatusBadRequest)
return
}
img, err := decodeImage(data)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
faces, err := fs.engine.Analyze(r.Context(), img)
if err != nil {
fs.sendErrorResponse(w, "人脸分析失败", http.StatusInternalServerError)
return
}
if len(faces) == 0 {
fs.sendErrorResponse(w, "未检测到人脸", http.StatusBadRequest)
return
}
// 取置信度最高的人脸
best := faces[0]
for _, f := range faces[1:] {
if f.Detection.Score > best.Detection.Score {
best = f
}
}
result := fs.matchFace(best, threshold)
fs.mu.Lock()
fs.stats.RecognitionCount++
fs.mu.Unlock()
go fs.saveData()
if result == nil {
fs.sendSuccessResponse(w, "未找到匹配的人脸", map[string]interface{}{
"recognized": false,
"det_score": best.Detection.Score,
})
return
}
fs.sendSuccessResponse(w, "人脸识别成功", map[string]interface{}{
"recognized": true,
"result": result,
})
}
// RecognizeMultipleFaces 多人脸识别接口
func (fs *FaceService) RecognizeMultipleFaces(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fs.sendErrorResponse(w, "只支持POST方法", http.StatusMethodNotAllowed)
return
}
if err := r.ParseMultipartForm(fs.config.MaxFileSize); err != nil {
fs.sendErrorResponse(w, "解析表单失败", http.StatusBadRequest)
return
}
file, _, err := r.FormFile("image")
if err != nil {
fs.sendErrorResponse(w, "获取图片文件失败", http.StatusBadRequest)
return
}
data, err := io.ReadAll(file)
file.Close()
if err != nil {
fs.sendErrorResponse(w, "读取图片失败", http.StatusBadRequest)
return
}
img, err := decodeImage(data)
if err != nil {
fs.sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
faces, err := fs.engine.Analyze(r.Context(), img)
if err != nil {
fs.sendErrorResponse(w, "人脸分析失败", http.StatusInternalServerError)
return
}
if len(faces) == 0 {
fs.sendErrorResponse(w, "未检测到人脸", http.StatusBadRequest)
return
}
var detections []FaceDetection
for i, face := range faces {
detection := FaceDetection{
Index: i,
Rectangle: map[string]float32{
"left": face.Detection.Box.Left,
"top": face.Detection.Box.Top,
"right": face.Detection.Box.Right,
"bottom": face.Detection.Box.Bottom,
},
Score: face.Detection.Score,
}
if result := fs.matchFace(face, 0); result != nil {
detection.Recognized = true
detection.Result = result
} else {
detection.Message = "未找到匹配的人脸"
}
detections = append(detections, detection)
}
fs.mu.Lock()
fs.stats.RecognitionCount++
fs.mu.Unlock()
go fs.saveData()
fs.sendSuccessResponse(w, fmt.Sprintf("检测到%d张人脸", len(faces)), detections)
}
// ===== 人员与样本管理 =====
// GetPersonList 获取人员列表
func (fs *FaceService) GetPersonList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
defer fs.mu.RUnlock()
var personList []map[string]interface{}
for _, person := range fs.persons {
personInfo := map[string]interface{}{
"id": person.ID,
"name": person.Name,
"sample_count": len(person.Samples),
"created": person.Created,
"updated": person.Updated,
}
var samples []map[string]interface{}
for _, sample := range person.Samples {
samples = append(samples, map[string]interface{}{
"id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"model": sample.Model,
"created": sample.Created,
})
}
personInfo["samples"] = samples
personList = append(personList, personInfo)
}
fs.sendSuccessResponse(w, "获取成功", personList)
}
// DeletePerson 删除人员
func (fs *FaceService) DeletePerson(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
fs.sendErrorResponse(w, "只支持DELETE方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personID, err := strconv.Atoi(vars["person_id"])
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
person, exists := fs.persons[personID]
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
for _, sample := range person.Samples {
if sample.ImagePath != "" {
if err := os.Remove(sample.ImagePath); err != nil {
log.Printf("删除图片文件失败: %v", err)
}
}
delete(fs.samples, sample.ID)
fs.stats.TotalSamples--
}
delete(fs.persons, personID)
fs.stats.TotalPersons--
if err := fs.rebuildMatcher(); err != nil {
fs.sendErrorResponse(w, "更新匹配器失败", http.StatusInternalServerError)
return
}
go fs.saveData()
fs.sendSuccessResponse(w, "删除成功", nil)
}
// DeleteSample 删除样本
func (fs *FaceService) DeleteSample(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
fs.sendErrorResponse(w, "只支持DELETE方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
sampleID, err := strconv.Atoi(vars["sample_id"])
if err != nil {
fs.sendErrorResponse(w, "无效的样本ID", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
sample, exists := fs.samples[sampleID]
if !exists {
fs.sendErrorResponse(w, "样本不存在", http.StatusNotFound)
return
}
person, personExists := fs.persons[sample.PersonID]
if !personExists {
fs.sendErrorResponse(w, "关联的人员不存在", http.StatusNotFound)
return
}
if len(person.Samples) <= 1 {
fs.sendErrorResponse(w, "不能删除最后一个样本,请删除整个人员", http.StatusBadRequest)
return
}
if sample.ImagePath != "" {
if err := os.Remove(sample.ImagePath); err != nil {
log.Printf("删除图片文件失败: %v", err)
}
}
for i, s := range person.Samples {
if s.ID == sampleID {
person.Samples = append(person.Samples[:i], person.Samples[i+1:]...)
break
}
}
person.Updated = time.Now()
delete(fs.samples, sampleID)
fs.stats.TotalSamples--
if err := fs.rebuildMatcher(); err != nil {
fs.sendErrorResponse(w, "更新匹配器失败", http.StatusInternalServerError)
return
}
go fs.saveData()
fs.sendSuccessResponse(w, "样本删除成功", nil)
}
// GetStatistics 获取统计信息
func (fs *FaceService) GetStatistics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
stats := fs.stats
fs.mu.RUnlock()
fs.sendSuccessResponse(w, "获取统计信息成功", stats)
}
// GetPersonDetail 获取人员详情
func (fs *FaceService) GetPersonDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personID, err := strconv.Atoi(vars["person_id"])
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
fs.mu.RLock()
person, exists := fs.persons[personID]
fs.mu.RUnlock()
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
personDetail := map[string]interface{}{
"id": person.ID,
"name": person.Name,
"sample_count": len(person.Samples),
"created": person.Created,
"updated": person.Updated,
}
var samples []map[string]interface{}
for _, sample := range person.Samples {
samples = append(samples, map[string]interface{}{
"id": sample.ID,
"image_url": sample.ImageURL,
"quality": sample.Quality,
"model": sample.Model,
"created": sample.Created,
})
}
personDetail["samples"] = samples
fs.sendSuccessResponse(w, "获取人员详情成功", personDetail)
}
// UpdatePersonName 更新人员姓名
func (fs *FaceService) UpdatePersonName(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
fs.sendErrorResponse(w, "只支持PUT方法", http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(r)
personID, err := strconv.Atoi(vars["person_id"])
if err != nil {
fs.sendErrorResponse(w, "无效的人员ID", http.StatusBadRequest)
return
}
var requestData struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
fs.sendErrorResponse(w, "解析请求数据失败", http.StatusBadRequest)
return
}
if requestData.Name == "" {
fs.sendErrorResponse(w, "姓名不能为空", http.StatusBadRequest)
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
person, exists := fs.persons[personID]
if !exists {
fs.sendErrorResponse(w, "人员不存在", http.StatusNotFound)
return
}
for _, p := range fs.persons {
if p.ID != personID && p.Name == requestData.Name {
fs.sendErrorResponse(w, "该姓名已存在", http.StatusConflict)
return
}
}
person.Name = requestData.Name
person.Updated = time.Now()
go fs.saveData()
fs.sendSuccessResponse(w, "姓名更新成功", map[string]interface{}{
"id": person.ID,
"name": person.Name,
})
}
// HealthCheck 健康检查
func (fs *FaceService) HealthCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fs.sendErrorResponse(w, "只支持GET方法", http.StatusMethodNotAllowed)
return
}
fs.mu.RLock()
totalPersons := len(fs.persons)
totalSamples := len(fs.samples)
fs.mu.RUnlock()
fs.sendSuccessResponse(w, "服务正常", map[string]interface{}{
"status": "healthy",
"total_persons": totalPersons,
"total_samples": totalSamples,
"model": fs.config.ModelVersion,
"timestamp": time.Now(),
})
}
// ===== 响应辅助方法 =====
func (fs *FaceService) sendSuccessResponse(w http.ResponseWriter, message string, data interface{}) {
fs.sendResponse(w, true, message, data, http.StatusOK)
}
func (fs *FaceService) sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
fs.sendResponse(w, false, message, nil, statusCode)
}
func (fs *FaceService) sendResponse(w http.ResponseWriter, success bool, message string, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
response := Response{
Success: success,
Message: message,
Data: data,
Timestamp: time.Now(),
}
json.NewEncoder(w).Encode(response)
}
// ===== 配置与启动 =====
func loadConfig(configPath string) (*Config, error) {
config := &Config{
Port: ":8080",
RuntimeLibrary: "models/lib/libonnxruntime.so.1.23.2",
DetectorModel: "models/det_10g.onnx",
RecognizerModel: "models/w600k_r50.onnx",
ModelVersion: "scrfd-10g+w600k-r50",
UploadsDir: "uploads",
DataFile: "face_data.json",
MaxFileSize: 10 << 20, // 10MB
MinSimilarity: 0.6,
MinMargin: 0.05,
LogLevel: "info",
}
if _, err := os.Stat(configPath); err == nil {
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("打开配置文件失败: %v", err)
}
defer file.Close()
if err := json.NewDecoder(file).Decode(config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %v", err)
}
}
return config, nil
}
// startAutoSave 定期保存数据的后台任务
func (fs *FaceService) startAutoSave() {
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := fs.saveData(); err != nil {
log.Printf("自动保存数据失败: %v", err)
}
}
}()
}
func main() {
configPath := "config.json"
if len(os.Args) > 1 {
configPath = os.Args[1]
}
config, err := loadConfig(configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
faceService, err := NewFaceService(config)
if err != nil {
log.Fatal("初始化人脸识别服务失败:", err)
}
defer faceService.Close()
faceService.startAutoSave()
r := mux.NewRouter()
api := r.PathPrefix("/api/v1").Subrouter()
// 人员管理
api.HandleFunc("/person/register", faceService.RegisterPerson).Methods("POST")
api.HandleFunc("/person/list", faceService.GetPersonList).Methods("GET")
api.HandleFunc("/person/{person_id}", faceService.GetPersonDetail).Methods("GET")
api.HandleFunc("/person/{person_id}", faceService.UpdatePersonName).Methods("PUT")
api.HandleFunc("/person/{person_id}", faceService.DeletePerson).Methods("DELETE")
// 样本管理
api.HandleFunc("/person/{person_id}/sample", faceService.AddSample).Methods("POST")
api.HandleFunc("/sample/{sample_id}", faceService.DeleteSample).Methods("DELETE")
// 识别接口
api.HandleFunc("/face/recognize", faceService.RecognizeFace).Methods("POST")
api.HandleFunc("/face/recognize-multiple", faceService.RecognizeMultipleFaces).Methods("POST")
// 统计和健康检查
api.HandleFunc("/statistics", faceService.GetStatistics).Methods("GET")
api.HandleFunc("/health", faceService.HealthCheck).Methods("GET")
// 静态文件服务
r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir(config.UploadsDir))))
// CORS中间件
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
})
fmt.Printf("=== 人脸识别服务启动(go-insightface)===\n")
fmt.Printf("端口: %s\n", config.Port)
fmt.Printf("运行时: %s\n", config.RuntimeLibrary)
fmt.Printf("检测模型: %s\n", config.DetectorModel)
fmt.Printf("识别模型: %s\n", config.RecognizerModel)
fmt.Printf("模型版本: %s\n", config.ModelVersion)
fmt.Printf("相似度阈值: %.2f (margin %.2f)\n", config.MinSimilarity, config.MinMargin)
fmt.Printf("最大文件大小: %d MB\n", config.MaxFileSize/(1024*1024))
fmt.Println("\n=== API接口列表 ===")
fmt.Println("人员管理:")
fmt.Println(" POST /api/v1/person/register - 人员登记")
fmt.Println(" GET /api/v1/person/list - 获取人员列表")
fmt.Println(" GET /api/v1/person/{id} - 获取人员详情")
fmt.Println(" PUT /api/v1/person/{id} - 更新人员姓名")
fmt.Println(" DELETE /api/v1/person/{id} - 删除人员")
fmt.Println("\n样本管理:")
fmt.Println(" POST /api/v1/person/{id}/sample - 添加样本")
fmt.Println(" DELETE /api/v1/sample/{id} - 删除样本")
fmt.Println("\n识别接口:")
fmt.Println(" POST /api/v1/face/recognize - 单人脸识别")
fmt.Println(" POST /api/v1/face/recognize-multiple - 多人脸识别")
fmt.Println("\n系统接口:")
fmt.Println(" GET /api/v1/statistics - 获取统计信息")
fmt.Println(" GET /api/v1/health - 健康检查")
fmt.Println("\n静态文件:")
fmt.Println(" GET /uploads/* - 图片文件访问")
log.Fatal(http.ListenAndServe(config.Port, r))
}
配置文件 (config.json)
{
"port": ":8080",
"runtime_library": "models/lib/libonnxruntime.so.1.23.2",
"detector_model": "models/det_10g.onnx",
"recognizer_model": "models/w600k_r50.onnx",
"model_version": "scrfd-10g+w600k-r50",
"uploads_dir": "uploads",
"data_file": "face_data.json",
"max_file_size": 10485760,
"min_similarity": 0.6,
"min_margin": 0.05,
"log_level": "info"
}
使用建议
⚠️ 与方案一相同的样本采集建议(每人 3-8 个多角度高质量样本)同样适用,另外注意:
- 阈值校准:
min_similarity(余弦相似度)与方案一的欧氏距离阈值语义不同,0.6 只是起点,建议用部署环境的真实数据做 FPIR/FNIR 测试校准;相似度是匹配分数,不要当作概率/置信度- margin 的作用:
min_margin要求最佳身份与次优不同身份的相似度拉开差距,能有效防止"相似脸"误识别- 模型空间:更换识别模型后,旧样本的特征向量在新模型下不可比,必须重新登记;持久化时务必保存模型版本
- 并发安全:
Engine.Analyze和Matcher.Match本身并发安全,多人脸识别时可按需并发处理
两方案对比
| 维度 | 方案一:go-face (dlib) | 方案二:go-insightface (SCRFD + ArcFace) |
|---|---|---|
| 编译依赖 | CGO + 编译 dlib | 无 CGO/OpenCV,ONNX Runtime 动态加载 |
| 检测器 | dlib HOG/CNN | SCRFD(自带 5 点关键点) |
| 特征维度 | 128 维 | 512 维 |
| 相似度度量 | 欧氏距离(越小越相似) | 余弦相似度(越大越相似)+ Top-1/Top-2 margin |
| 识别精度 | 一般 | 更高(ArcFace 是业界主流方案) |
| 图像输入 | 必须落盘为文件路径 | 直接从内存解码(image.Image) |
| 平台 | 编译 dlib(Windows 下痛苦) | 首次发布支持 Linux/macOS CPU |
| 模型许可 | dlib 模型 BSD 许可 | InsightFace 预训练模型为研究用途 |
用到的数学方法
上面的代码用到了欧几里得距离计算公式解析,下面内容来自AI
基本概念
欧几里得距离(Euclidean Distance)是衡量两个点在多维空间中实际距离的最常用方法。在人脸识别领域,我们用它来计算两个人脸特征向量(通常是128维或更高维)之间的相似度。
公式表达
在n维空间中,两点$p$和$q$之间的欧几里得距离公式为:
$$ d(p,q) = \sqrt{\sum_{i=1}^{n}(p_i - q_i)^2} $$
其中:
- $p$ 和 $q$ 是两个特征向量
- $p_i$ 和 $q_i$ 分别是向量$p$和$q$在第$i$个维度的值
- $n$ 是特征向量的维度数
在人脸识别中的具体应用
在人脸识别领域,我们处理的是128维的特征向量(由dlib的ResNet模型生成),所以公式变为:
$$ distance = \sqrt{\sum_{i=0}^{127}(d1_i - d2_i)^2} $$
其中:
- $d1$ 和 $d2$ 是两个128维的人脸特征向量
- $d1_i$ 和 $d2_i$ 是向量在维度$i$上的值
相似度转换
在实现中,我们通常会将距离转换为更直观的相似度百分比:
$$ similarity = (1 - \frac{distance}{max_possible}) \times 100% $$
但在实践中,由于dlib模型的特性,我们更常使用:
$$ confidence = (1 - distance) \times 100% $$
这里需要注意:
- 当$distance > 1$时,$confidence$会变为负数
- 因此我们在实际应用中会将其限制为0:
if confidence < 0 {
confidence = 0
}
阈值设置
| 阈值水平 | 距离范围 | 置信度范围 | 识别结果 |
|---|---|---|---|
| 非常严格 | distance < 0.3 | confidence > 70% | 几乎可以确定是同一人 |
| 严格 | distance < 0.4 | confidence > 60% | 高度可能是同一人 |
| 正常(默认) | distance < 0.6 | confidence > 40% | 可能是同一人 |
| 宽松 | distance < 0.8 | confidence > 20% | 可能是同一人(但误差大) |
| 非常宽松 | distance < 1.0 | confidence > 0% | 不可靠的匹配 |
实际应用代码
// 计算两个人脸特征向量之间的欧几里得距离
func calculateDistance(desc1, desc2 face.Descriptor) float32 {
var sum float64
for i := 0; i < len(desc1); i++ {
diff := float64(desc1[i] - desc2[i])
sum += diff * diff
}
return float32(math.Sqrt(sum))
}
// 距离转换为相似度百分比
func distanceToConfidence(distance float32) float32 {
confidence := (1 - distance) * 100
if confidence < 0 {
return 0
}
return confidence
}
数学特性
- 非负性:$distance \geqslant 0$
- 同一性:$d(x,y) = 0$ 当且仅当 $x = y$
- 对称性:$d(x,y) = d(y,x)$
- 三角不等式:$d(x,z) \leqslant d(x,y) + d(y,z)$
性能优化考虑
-
平方距离替代:可以只计算平方和而不开方以提升性能: $$ squaredDistance = \sum_{i=0}^{127}(d1_i - d2_i)^2 $$
-
距离提前终止:在遍历计算过程中,如果部分和已超过阈值,可提前终止计算
-
向量化计算:使用SIMD指令并行处理多个维度计算(在Go中可使用
gorgonia等库)
与其他距离度量的对比
| 度量方式 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| 欧几里得距离 | $\sqrt{\sum(p_i-q_i)^2}$ | 直观性强,计算简单 | 人脸识别,图像检索 |
| 余弦相似度 | $\frac{p·q}{|p||q|}$ | 关注方向而非大小 | 文本分析,高维空间 |
| 曼哈顿距离 | $\sum|p_i-q_i|$ | 计算成本低 | 网格路径规划 |
| 马氏距离 | $\sqrt{(p-q)^TΣ^{-1}(p-q)}$ | 考虑特征相关性 | 统计分类 |
余弦相似度(方案二 go-insightface 使用)
方案二的 ArcFace 特征向量是 L2 归一化后的 512 维向量,此时点积(dot product)就等于余弦相似度,这正是 go-insightface 匹配器内部的计算方式。
公式表达
$ \cos(\theta) = \frac{p \cdot q}{|p| |q|} = \frac{\sum_{i=1}^{n} p_i q_i}{\sqrt{\sum_{i=1}^{n} p_i^2} \sqrt{\sum_{i=1}^{n} q_i^2}} $
由于向量已归一化($|p| = |q| = 1$),公式简化为:
$ similarity = \sum_{i=1}^{n} p_i q_i $
取值范围为 $[-1, 1]$,越接近 1 表示方向越一致(越相似),这与欧氏距离"越小越相似"的语义相反。
与欧氏距离的直观区别
- 欧氏距离关注绝对差距:向量 $p$ 和 $2p$ 距离很远,但它们方向相同
- 余弦相似度只关注方向:对特征向量的整体亮度/尺度变化不敏感,更适合高维特征匹配
阈值参考
| 阈值水平 | 余弦相似度 | 识别结果 |
|---|---|---|
| 非常严格 | > 0.75 | 几乎可以确定是同一人 |
| 严格 | > 0.65 | 高度可能是同一人 |
| 正常(默认) | > 0.55 ~ 0.60 | 可能是同一人 |
| 宽松 | > 0.45 | 可能是同一人(但误差大) |
注意:相似度是匹配分数而非概率。具体阈值应结合
min_margin(与次优不同身份的差距)一起,用部署环境数据做 FPIR/FNIR 校准。
实际应用代码
go-insightface 的 Embedding 自带比较方法:
// 直接计算两个已归一化特征向量的余弦相似度
sim, err := emb1.CosineSimilarity(emb2)
if err != nil {
// 维度不一致等
return
}
// 或使用 Matcher 进行身份级匹配(多样本取最佳 + margin 检查)
matcher, err := insightface.NewMatcher(insightface.MatcherConfig{
MinSimilarity: 0.6,
MinMargin: 0.05,
}, gallery)
result, err := matcher.Match(query)
if err == nil && result.Accepted {
fmt.Println(result.Best.Identity) // 最佳身份
fmt.Println(result.Best.Similarity) // 最佳相似度
fmt.Println(result.Margin) // 与次优不同身份的差距
}
数学特性
- 对称性:$\cos(p,q) = \cos(q,p)$
- 有界性:$-1 \leqslant \cos(\theta) \leqslant 1$(相比欧氏距离无上界,阈值更容易设定)
- 归一化不变性:对向量整体缩放不敏感,故先做 L2 归一化可统一比较尺度
- 高效计算:归一化后只需一次点积($n$ 次乘加),适合大规模 gallery 精确搜索
其他相关的仓库
- go-insightface (本文方案二使用的库:纯 Go SCRFD + ArcFace)
- onnxruntime-purego (go-insightface 底层的 ONNX Runtime 绑定)
- 基于gocv实现的人脸识别登录
- A simple and fast face detector using gocv and har cascade classifier.
- golang facial recognition project based on the go-face library
- Fast face detection, pupil/eyes localization and facial landmark points detection library in pure Go.
- Face detection and recognition using golang
- arcface-go