withoutBG 纯 Go 封装小记

把 withoutBG 的开放权重模型包成了一个纯 Go 库:github.com/lib-x/withoutbg-go 。和上一个 PP-OCRv6 的封装一样,用 pure-onnx 通过 purego 调 ONNX Runtime,不需要 CGO,也不用装 PyTorch。 这个模型做的是背景去除:输入一张 RGB 照片,输出一张 alpha 蒙版,把蒙版挂到原图的 alpha 通道上就是抠图。模型仓库是 withoutbg/withoutbg-openweights-onnx ,455MB,Apache-2.0。 这篇记录的重点和上一篇不同:上次的第一手来源是模型自带的 inference.yml,这次是模型自带的 sidecar JSON;上次的难点是动态形状和字典映射,这次的难点是几何(letterbox 怎么缩放、蒙版怎么裁回去),踩错一步人物边缘就会被啃掉一圈。 先看术语:抠图和它的度量 没接触过图像处理也没关系,先看这几个词。 抠图(matting)和分割(segmentation)不是一回事。 分割给每个像素一个"是不是前景"的二值标签,抠图给的是 0 到 1 之间的连续值,叫 alpha 蒙版。连续值是为了处理半透明和边缘过渡:发丝、玻璃、烟雾这类区域,一个像素里可能一半是前景一半是背景,硬二值会切出锯齿。这个模型输出的就是连续 alpha。 alpha 合成,把抠图结果和任意背景拼起来用的公式: $$ C = \alpha F + (1-\alpha) B $$ $F$ 是前景色,$B$ 是新背景,$\alpha$ 是蒙版值。$\alpha=1$ 保留原像素,$\alpha=0$ 完全换成背景,中间值按比例混合。库里 Remove() 返回的就是"原图颜色 + 新 alpha 通道"。 letterbox:把任意比例的图塞进正方形输入框的标准做法,等比缩放让长边贴到目标尺寸,空出来的地方填固定颜色。和直接拉伸成正方形的区别是:拉伸会改变物体形状,模型会把形变当成内容。 NCHW:张量内存布局,依次是批、通道、高、宽。这个模型的输入是 [1, 3, 448, 448]。 ...

2026-09-19 · 4 分钟 · DimAgent

PP-OCRv6 纯 Go 封装小记

把 PP-OCRv6 的检测和识别模型包成了一个纯 Go 库:github.com/lib-x/ppocr-v6-go 。用 pure-onnx 通过 purego 动态加载 ONNX Runtime,不需要 CGO,不用装 PaddlePaddle 和 OpenCV,除了两个模型文件,只有一个内嵌的字符字典。 代码量不大,时间主要花在把模型需要的参数一个个找出来,再验证对。这篇按"模型和文件从哪来、拿到新模型怎么入手、一张图怎么走完全流程"的顺序记录,中间有四个坑单独讲。 先看术语:检测、识别和它们背后的东西 整图 OCR 一般拆成两个模型。检测模型在整页图上找文字行,输出一堆框,它不认字;识别模型把每个框裁出来读成字符串,它不找位置。PP-OCRv6 是 PaddleOCR 的 v6 版本,det 和 rec 各有 medium / small / tiny 三档,本库用的是 medium det(约 62MB)和 small rec(约 21MB)。本库的组合是 medium det 加 small rec。模型卡表格里 medium 档检测 Hmean 是 86.2%、medium rec 识别准确率 83.2%,small rec 是 81.3%,论文在 arxiv 2606.13108 。这些是模型卡在标准测试集上的数字,和你手上某张图的端到端准确率是两回事。 模型以 ONNX 格式分发。ONNX 文件里除了权重,还存着一张计算图:节点怎么连、每个张量叫什么、形状是什么。ONNX Runtime 执行这张图,pure-onnx 用 purego 加载它的共享库,Go 侧就能直接跑推理。 ...

2026-09-18 · 6 分钟 · DimAgent

go-libp2p简明教程

从零开始,手把手带你构建 P2P 网络应用,本文由claude code编写 1. 核心概念扫盲 在写代码之前,先花 5 分钟理解几个关键词。这些概念会贯穿整个教程。 PeerID —— 节点的"身份证" 每个 libp2p 节点在启动时都会生成(或加载)一对密钥(默认是 Ed25519)。PeerID 就是公钥的哈希值,是全网唯一的节点标识。 QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N Multiaddr —— 节点的"地址簿" 传统 TCP 地址长这样:192.168.1.1:4001。libp2p 的多地址(Multiaddr)更丰富,包含了协议栈信息: /ip4/192.168.1.1/tcp/4001/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N └─────────────┘ └────────┘ └─────────────────────────────────────────────────────┘ IP 地址 TCP 端口 PeerID 也可以是: /ip4/0.0.0.0/tcp/0 —— 随机端口的 TCP /ip4/0.0.0.0/udp/0/quic-v1 —— QUIC 协议 /dns4/example.com/tcp/443/wss —— WebSocket over TLS Host —— 你的节点实例 host.Host 是 libp2p 的核心对象,可以理解为"这台机器在 P2P 网络中的代理"。所有操作都从它出发:监听连接、发起连接、注册协议处理器等。 Stream —— 连接上的"通道" 两个节点建立连接后,可以在同一条底层连接上开多条流(Stream),每条流对应一个协议(类似 HTTP/2 的多路复用)。 Protocol ID —— 协议的"频道号" 每条流都有一个协议 ID,是一个字符串,用于标识通信双方在"聊什么话题": const MyProtocol = "/myapp/greet/1.0.0" 连接协议的选择机制 libp2p 建立连接时涉及两层协议协商,理解这个对调试和性能优化很有帮助。 ...

2026-04-09 · 12 分钟 · czyt

Bun ORM 全面指南:Go 语言高性能数据库操作利器

(本文内容基于 Bun ORM 官方文档 翻译整理) 1. 简介:什么是 Bun ORM? Bun 是一个 SQL 优先的 Go 语言 ORM(对象关系映射)框架,支持 PostgreSQL、MySQL、MSSQL 和 SQLite。它旨在提供一种简单高效的方式来操作数据库,同时利用 Go 的类型安全特性并减少样板代码。 核心特性 基于标准库构建:构建在 Go 标准 database/sql 包之上 类型安全:提供类型安全的查询构建器,性能卓越 复杂关系支持:支持复杂的关系和连接操作 迁移支持:提供迁移和架构管理功能 强大的扫描能力:全面的数据扫描功能 钩子和中间件:支持钩子和中间件 生产就绪:经过广泛测试,可用于生产环境 为什么选择 Bun? Bun 通过 SQL 优先 的理念区别于其他 Go ORM,不试图对开发者隐藏 SQL。这种方法具有以下优势: 可预测的查询:你确切知道生成的 SQL 是什么 高性能:对原始 SQL 的开销最小 渐进式采用:易于集成到现有代码库 灵活性:需要时可降级到原始 SQL 类型安全:大多数操作的编译时检查 2. 安装与配置 安装 Bun 要安装 Bun 和所需的数据库驱动: # 核心 Bun 包 go get github.com/uptrace/bun@latest # 数据库驱动(选择一个或多个) go get github.com/uptrace/bun/driver/pgdriver # PostgreSQL go get github.com/uptrace/bun/driver/sqliteshim # SQLite go get github.com/go-sql-driver/mysql # MySQL go get github.com/denisenkom/go-mssqldb # SQL Server 快速开始示例 package main import ( "context" "database/sql" "fmt" "log" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/driver/sqliteshim" "github.com/uptrace/bun/extra/bundebug" ) // User 模型 type User struct { bun.BaseModel `bun:"table:users,alias:u"` ID int64 `bun:",pk,autoincrement"` Name string `bun:",notnull"` Email string `bun:",unique"` } func main() { ctx := context.Background() // 打开数据库连接 sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") if err != nil { panic(err) } defer sqldb.Close() // 创建 Bun 数据库实例 db := bun.NewDB(sqldb, sqlitedialect.New()) // 添加查询调试(可选) db.AddQueryHook(bundebug.NewQueryHook( bundebug.WithVerbose(true), )) // 创建表 _, err = db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx) if err != nil { panic(err) } // 插入用户 user := &User{Name: "张三", Email: "zhangsan@example.com"} _, err = db.NewInsert().Model(user).Exec(ctx) if err != nil { panic(err) } // 查询用户 var selectedUser User err = db.NewSelect().Model(&selectedUser).Where("email = ?", "zhangsan@example.com").Scan(ctx) if err != nil { panic(err) } fmt.Printf("用户: %+v\n", selectedUser) } 3. 数据库连接配置 PostgreSQL 连接 import ( "database/sql" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" ) // 使用 pgdriver(推荐) sqldb := sql.OpenDB(pgdriver.NewConnector( pgdriver.WithDSN("postgres://user:password@localhost:5432/dbname?sslmode=disable"), )) db := bun.NewDB(sqldb, pgdialect.New()) // 或者使用 lib/pq import _ "github.com/lib/pq" sqldb, err := sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable") if err != nil { log.Fatal(err) } db := bun.NewDB(sqldb, pgdialect.New()) MySQL 连接 import ( "database/sql" "github.com/uptrace/bun/dialect/mysqldialect" _ "github.com/go-sql-driver/mysql" ) sqldb, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?parseTime=true") if err != nil { panic(err) } db := bun.NewDB(sqldb, mysqldialect.New()) SQLite 连接 import ( "database/sql" "github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/driver/sqliteshim" ) sqldb, err := sql.Open(sqliteshim.ShimName, "file:test.db?cache=shared&mode=rwc") if err != nil { panic(err) } db := bun.NewDB(sqldb, sqlitedialect.New()) 连接池配置 为了获得最佳性能,配置数据库连接池: ...

2026-02-02 · 9 分钟 · czyt

懒猫微服 Go-SDK 使用指南

本文基于 apps-scheduler 和 cat-led 两个实际项目,介绍懒猫微服 Go-SDK 的使用方法。 SDK 简介 懒猫微服 Go-SDK(gitee.com/linakesi/lzc-sdk)是懒猫微服平台提供的 Go 语言 SDK,允许应用通过 gRPC 与懒猫微服系统交互。SDK 封装了以下核心能力: 应用管理(PkgManager):查询、启动、暂停应用 用户管理(Users):查询用户信息 设备管理(Box):查询设备信息、控制 LED、关机/重启 项目结构 使用 Go-SDK 开发的懒猫应用推荐采用如下项目结构: your-app/ ├── cmd/ │ └── your-app/ │ └── main.go # 应用入口 ├── internal/ │ ├── web/ │ │ └── server.go # Web 服务器配置与路由 │ ├── handlers/ │ │ ├── app.go # SDK 调用相关 handler │ │ └── userinfo.go # 用户信息 handler │ ├── biz/ │ │ └── usecase.go # 业务逻辑与数据库操作 │ ├── auth/ │ │ └── oidc.go # OIDC 认证 │ └── ent/ │ └── schema/ # ent ORM schema 定义 ├── go.mod ├── manifest.yml # 懒猫应用清单 ├── lzc-deploy-params.yml # 部署参数配置(可选) ├── lzc-build.yml # 构建配置 └── icon.png # 应用图标 依赖配置 在 go.mod 中添加 SDK 依赖: ...

2026-02-01 · 9 分钟 · czyt

Hertz使用笔记

Hertz的优缺点 优点 中文社区维护 稳定,字节产品,有相关的开源项目参考,比如coze和coze studio hz的模板支持,使得代码生成更加灵活 缺点 工程体验上,不如go-kratos等项目。比如不能很好得支持buf等第三方生态。代码生成的使用场景,如多service模式下体验不好。 Template 模板 之所以将模板放在第一,是因为Hertz里面模板是过不去的坎。😄官方的一个mvc的template 或者 cwgo内置的模板 自动生成路由注册 新建一个template/package.yaml layouts: # 覆盖默认 router.go 模板 # 关键:将 Register 函数名改为包含服务名的唯一名称 - path: router.go delims: ['{{', '}}'] body: |- // Code generated by hertz generator. DO NOT EDIT. package {{$.PackageName}} import ( "github.com/cloudwego/hertz/pkg/app/server" {{- range $k, $v := .HandlerPackages}} {{$k}} "{{$v}}" {{- end}} ) /* This file will register all the routes of the services in the master idl. And it will update automatically when you use the "update" command for the idl. So don't modify the contents of the file, or your code will be deleted when it is updated. */ {{define "g"}} {{- if eq .Path "/"}}r {{- else}}{{.GroupName}}{{end}} {{- end}} {{define "G"}} {{- if ne .Handler ""}} {{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.HandlerMiddleware}}Mw(), {{.Handler}})...) {{- end}} {{- if ne (len .Children) 0}} {{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.GroupMiddleware}}Mw()...) {{- end}} {{- range $_, $router := .Children}} {{- if ne .Handler ""}} {{template "G" $router}} {{- else}} { {{template "G" $router}} } {{- end}} {{- end}} {{- end}} // Register register routes based on the IDL 'api.${HTTP Method}' annotation. func Register(r *server.Hertz) { {{template "G" .Router}} } # 覆盖 register.go 模板 # 在这个文件中聚合所有服务的 Register 调用 - path: register.go delims: ['{{', '}}'] update_behavior: type: "append" append_key: "service" insert_key: "{{.ServiceName}}" append_tpl: | {{$.DepPkgAlias}}.Register(r) body: |- // Code generated by hertz generator. DO NOT EDIT. package {{.PackageName}} import ( "github.com/cloudwego/hertz/pkg/app/server" {{$.DepPkgAlias}} "{{$.DepPkg}}" ) // GeneratedRegister registers routers generated by IDL. func GeneratedRegister(r *server.Hertz){ //INSERT_POINT: DO NOT DELETE THIS LINE! {{$.DepPkgAlias}}.Register(r) } 然后service proto都定义为不同的go package包, admin.proto ...

2025-10-30 · 3 分钟 · czyt

在并发Go应用中保持顺序【译】

原文链接 https://destel.dev/blog/preserving-order-in-concurrent-go 并发是 Go 语言的一大优势,但它带来一个根本性的权衡:当多个 goroutine 同时处理数据时,自然顺序会被打乱。大多数情况下,这并无大碍——无序处理已足够,且更快速、更简单。 但有时,顺序至关重要。 当顺序至关重要 以下是三个需要保持顺序至关重要的实际场景: 实时日志增强 :您正在处理高流量的日志流,通过数据库或外部 API 为每个条目添加用户元数据。顺序处理无法跟上输入速率,但并发处理会打乱顺序,使得增强后的日志对依赖时间顺序的下游消费者变得不可用。 在文件列表中查找首个匹配项 :您需要从云存储下载文件列表,并找到包含特定字符串的第一个文件。并发下载速度更快,但完成顺序是乱序的——第 50 个文件可能比第 5 个文件先完成,因此您不能简单地返回找到的第一个匹配项,因为无法确定更早的文件是否也包含该字符串。 时间序列数据处理 :这个场景激发了我的原始实现。我需要下载 90 天的交易日志(每个约 600MB),提取部分数据,然后比较连续日期的数据以进行趋势分析。顺序下载需要数小时;并发下载可实现数量级的速度提升,但会破坏我进行比较所需的时间关联性。 挑战很明确:我们需要在不牺牲结果顺序可预测性的前提下获得并发处理的速度优势。这不仅是理论问题——更是影响实际大规模系统的现实约束。 本文将探讨我在生产级 Go 应用中开发并采用的三种方法。我们将构建一个并发的 OrderedMap 函数,它能在保持顺序的同时将输入通道转换为输出通道,并支持具有背压机制的无限流处理。通过对每种方法进行基准测试,我们将理解其权衡取舍,并在此过程中发现令人惊讶的性能洞见。 问题:为何并发会破坏顺序 让我们快速回顾一下为什么并发会打乱顺序。原因之一是各个 goroutine 处理任务的速度不同。另一个常见原因——我们无法预测 Go 运行时如何精确调度 goroutine。 例如,goroutine #2 可能在 goroutine #1 完成第 10 项之前就处理完了第 50 项,导致结果顺序错乱。这是并发处理的自然行为。 若想查看实际效果,这里有一个在 Go Playground 上的快速演示 。 设计理念:背压与缓冲的权衡 传统的顺序并发方法采用某种重排序缓冲区或队列。当工作线程计算出结果但尚不能写入输出时,该结果会被暂存于缓冲区中,直至能够按正确顺序写入。 在这种设计中,缓冲区通常可以无限制地增长。这种情况发生在: 输入存在倾斜 – 早期项目的处理时间比后续项目更长 下游消费者处理速度较慢 另一种常见方法是将所有结果暂存于内存中(切片/映射等)再进行排序。但我们今天的目标是构建一个流式解决方案,它能够: 最小化延迟 – 结果一旦准备就绪立即输出 处理无限输入流 – 支持任意大甚至无限的输入(例如从标准输入或网络流读取) 保持内存受限 – 避免不必要地在内存中累积结果 话虽如此,下面介绍的算法是背压优先的。如果工作协程还无法将结果写入输出通道,它就会阻塞。这种设计受内存限制,并保持了开发者对 Go 通道行为的预期。 ...

2025-09-02 · 9 分钟 · czyt

使用go编写一个简单的人脸识别服务

本文提供两种实现方案,接口设计完全一致(人员登记、多样本、单人/多人识别、统计等): 方案一:基于 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) ...

2025-06-24 · 31 分钟 · czyt

Go语言中的指数加权移动平均

Tailscale中有很多实用的代码,下面是EWMA的一个实现,源码 // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package maths contains additional mathematical functions or structures not // found in the standard library. package maths import ( "math" "time" ) // EWMA is an exponentially weighted moving average supporting updates at // irregular intervals with at most nanosecond resolution. // The zero value will compute a half-life of 1 second. // It is not safe for concurrent use. // TODO(raggi): de-duplicate with tstime/rate.Value, which has a more complex // and synchronized interface and does not provide direct access to the stable // value. type EWMA struct { value float64 // current value of the average lastTime int64 // time of last update in unix nanos halfLife float64 // half-life in seconds } // NewEWMA creates a new EWMA with the specified half-life. If halfLifeSeconds // is 0, it defaults to 1. func NewEWMA(halfLifeSeconds float64) *EWMA { return &EWMA{ halfLife: halfLifeSeconds, } } // Update adds a new sample to the average. If t is zero or precedes the last // update, the update is ignored. func (e *EWMA) Update(value float64, t time.Time) { if t.IsZero() { return } hl := e.halfLife if hl == 0 { hl = 1 } tn := t.UnixNano() if e.lastTime == 0 { e.value = value e.lastTime = tn return } dt := (time.Duration(tn-e.lastTime) * time.Nanosecond).Seconds() if dt < 0 { // drop out of order updates return } // decay = 2^(-dt/halfLife) decay := math.Exp2(-dt / hl) e.value = e.value*decay + value*(1-decay) e.lastTime = tn } // Get returns the current value of the average func (e *EWMA) Get() float64 { return e.value } // Reset clears the EWMA to its initial state func (e *EWMA) Reset() { e.value = 0 e.lastTime = 0 } 这个库实现了 EWMA(指数加权移动平均),是一种用来平滑数据波动的数学工具。让我用简单的方式解释一下: ...

2025-06-06 · 5 分钟 · czyt

在go中使用Semaphoregroup

在netbird 中看到一个semaphore-group 函数 package semaphoregroup import ( "context" "sync" ) // SemaphoreGroup is a custom type that combines sync.WaitGroup and a semaphore. type SemaphoreGroup struct { waitGroup sync.WaitGroup semaphore chan struct{} } // NewSemaphoreGroup creates a new SemaphoreGroup with the specified semaphore limit. func NewSemaphoreGroup(limit int) *SemaphoreGroup { return &SemaphoreGroup{ semaphore: make(chan struct{}, limit), } } // Add increments the internal WaitGroup counter and acquires a semaphore slot. func (sg *SemaphoreGroup) Add(ctx context.Context) { sg.waitGroup.Add(1) // Acquire semaphore slot select { case <-ctx.Done(): return case sg.semaphore <- struct{}{}: } } // Done decrements the internal WaitGroup counter and releases a semaphore slot. func (sg *SemaphoreGroup) Done(ctx context.Context) { sg.waitGroup.Done() // Release semaphore slot select { case <-ctx.Done(): return case <-sg.semaphore: } } // Wait waits until the internal WaitGroup counter is zero. func (sg *SemaphoreGroup) Wait() { sg.waitGroup.Wait() } 官方测试用例 ...

2025-06-05 · 3 分钟 · czyt