本文提供两种实现方案,接口设计完全一致(人员登记、多样本、单人/多人识别、统计等):

  • 方案一:基于 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

运行时需要三个文件:

  1. ONNX Runtime 1.23.x 动态库(libonnxruntime.so.1.23.2)
  2. SCRFD 检测模型(det_10g.onnx)
  3. 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% $$

这里需要注意:

  1. 当$distance > 1$时,$confidence$会变为负数
  2. 因此我们在实际应用中会将其限制为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
}

数学特性

  1. 非负性:$distance \geqslant 0$
  2. 同一性:$d(x,y) = 0$ 当且仅当 $x = y$
  3. 对称性:$d(x,y) = d(y,x)$
  4. 三角不等式:$d(x,z) \leqslant d(x,y) + d(y,z)$

性能优化考虑

  1. 平方距离替代:可以只计算平方和而不开方以提升性能: $$ squaredDistance = \sum_{i=0}^{127}(d1_i - d2_i)^2 $$

  2. 距离提前终止:在遍历计算过程中,如果部分和已超过阈值,可提前终止计算

  3. 向量化计算:使用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)          // 与次优不同身份的差距
}

数学特性

  1. 对称性:$\cos(p,q) = \cos(q,p)$
  2. 有界性:$-1 \leqslant \cos(\theta) \leqslant 1$(相比欧氏距离无上界,阈值更容易设定)
  3. 归一化不变性:对向量整体缩放不敏感,故先做 L2 归一化可统一比较尺度
  4. 高效计算:归一化后只需一次点积($n$ 次乘加),适合大规模 gallery 精确搜索

其他相关的仓库