简介:C# 人脸比对服务离线部署源码包,专为需要在局域网或无网环境下集成人脸识别能力的.NET开发者准备。项目自带模型和必要依赖,无需云端接口即可完成人脸检测、特征提取与1:1比对,可快速落地到门禁、考勤、身份核验等业务。RAR压缩包共176个文件,约594MB,包含52个DLL运行库、C#源文件与工程文件、配置及资源文件,并带有测试工程,示例代码覆盖从初始化、加载模型到人脸检测比对的完整调用链路,目录结构清晰,便于定位核心算法与接口。目前已有275人学习下载,源码支持直接编译和离线部署。对希望掌握ViewFaceService实现机制、避免外部接口依赖或需要定制人脸比对服务的开发人员,这份源码提供了可运行的参照,能有效缩短从调研到落地的周期。
1. C#人脸比对服务就该这么落地:离线、自带模型、不依赖外网
如果你和我一样,被"人脸识别必须联网""算法是黑匣子,只给个SDK"这类问题卡过项目工期,那这篇就正好对上需求。我做上位机和人脸识别集成有些年头,这两年最常被问的就是:C#项目里能不能有一套人脸比对服务,离线跑、自带模型、不依赖云端API,最好连GPU都不要。ViewFaceService这类源码服务就是干这个的——它把模型、推理、比对逻辑整体封装成一个可独立部署的服务,C#客户端只需发HTTP请求,就能拿到检测框、特征值和相似度分数。适合门禁考勤、VIP识别、工位摄像头值守这类场景,也适合设备本地方案。
我拆这套源码时最大的感受是:它把"算法"这件事藏得很深,把"工程"这件事放得很重。不需要你懂深度网络怎么训练,但要你对模型目录、特征阈值、并发调用有基本认知。后面的内容我不讲玄学,直接按部署顺序写,参数和踩坑都会给。
2. 模型体系与比对原理:自带模型到底值在哪
2.1 人脸比对不是比像素,是比特征向量
人脸比对看着玄,原理拆开其实就三步:先从图片里找出来人脸在哪,再从人脸上找关键点把脸摆正,最后把正脸图喂给特征提取网络得到一个固定长度的特征向量。比对的时候不再看原图,只算两个向量之间的距离。距离小就是同一个人,距离大就是不同人。
ViewFaceService的客户端接口里常见的做法是:检测接口返回人脸框和关键点坐标,特征提取接口返回一个浮点数组。比对任务放到调用方来做——你拿一张底图注册时算一次特征存起来,后续每次抓拍算一次特征,然后算两个特征的余弦距离或欧氏距离。
我一般会先把"检测"和"特征提取"分开调,分别计时。这样如果现场识别慢,你能立刻定位到是检测那块耗时,还是特征比对那块出问题。整个链路最消耗CPU的就是特征提取,检测相对快很多。
2.2 自带模型的典型构成与目录安排
这类源码工程里,模型文件一般是以独立目录或嵌入式资源形式存在的。ViewFace系列一贯选用的是SeetaFace6的模型体系,包含检测模型、关键点模型、特征提取模型三个独立文件。
我拆包时重点关注的是它的模型加载方式。有两类典型做法,第一类是启动时全量加载进内存,响应快,但内存占用高;第二类是懒加载,第一次调用时才加载,启动快但首次请求会卡顿。ViewFaceService的启动方式需要看源码里的启动日志,如果是全量加载,启动后会看到模型初始化完成的提示。
模型文件的放置位置建议保持服务默认的查找路径,不要随便挪。很多离线部署翻车,都是因为模型路径没配对。
2.3 相似度阈值怎么定
特征比对拿到的是一个分数。常见的约定是分数越高越相似,但不同模型打的分数分布不一样。有的模型输出天然在0到1之间,有的输出范围很宽,需要归一化。
对ViewFaceService这种基于SeetaFace6的模型,我给的默认经验值是0.6左右作为"同一个人的最低线"。但注意,这个值只适用于同模型同特征类型。你换模型文件甚至换图像预处理方式,分数分布都会漂。正确做法是先拿20到30张真实现场照片做交叉比对,画出"同人分数"和"不同人分数"两条分布的区间,取中间值做阈值。
2.4 1:1比对与1:N检索的区别
人脸比对业务实际分两种。1:1就是"这张脸是不是张三",适合闸机核验和支付确认。1:N是"这张脸是谁",在注册库里检索,适合考勤打卡和VIP识别。
ViewFaceService提供的接口如果只做了1:1,你自己在C#端循环遍历注册特征库就能实现1:N,但要注意性能。我见过有人直接foreach几千个特征做余弦计算,单次比对规模不大还好,如果注册库上万,每次请求都全量遍历,响应时延就会很难看。后续第六章我会给一个缩小检索范围的方案。
3. 离线部署ViewFaceService:从配置文件到服务跑起来
部署这套服务的关键词是"离线"——目标机器可能不联网,甚至在内网隔离区。这意味着所有运行时依赖、模型文件、配置文件必须在部署前一次性备齐。
3.1 运行时依赖清单
先列依赖再动手,是我部署这类服务的第一步。C#服务最常见的依赖是.NET运行时,注意目标框架版本。如果源码是net6.0写的,目标机器就要装.NET 6 Desktop Runtime;net8.0同理。拿到的部署包里如果自带runtime目录,反而要注意架构位数的匹配。
# 检查目标机器已安装的.NET运行时版本 dotnet --list-runtimes执行后能看到类似Microsoft.NETCore.App 6.0.x的输出。确认版本号不低于源码工程的目标框架。如果输出为空,说明运行时没装,需要把发布时自包含的文件夹整个拷过去。
提示:自包含发布和框架依赖发布,两者区别就是目标机器要不要装运行时。离线环境我通常选自包含,虽然体积大几百MB,但省去装运行时的麻烦。
3.2 修改配置文件中的模型路径与监听端口
配置文件是部署的核心。ViewFaceService的配置一般集中在appsettings.json或独立的配置类里。重点检查三处:模型目录路径、服务监听端口、日志级别。
{ "Face": { "ModelPath": "models", "DetectThreshold": 0.7, "SimilarityThreshold": 0.6 }, "Server": { "Port": 9100, "UseHttps": false }, "Logging": { "LogLevel": "Information" } }ModelPath建议用绝对路径,尤其是在Windows服务方式运行时,相对路径的工作目录会变。Port默认用9000以上的高位端口,避开常见冲突。LogLevel调试时改成Debug,正常跑改成Information。
3.3 启动服务并验证健康状态
启动方式取决于源码给的宿主类型。如果是控制台宿主,直接双击exe或命令行启动;如果是Windows服务宿主,用sc命令注册。
# 注册Windows服务方式启动(假设发布目录为 D:\ViewFaceService) sc create ViewFaceService binPath= "D:\ViewFaceService\ViewFaceService.exe" start= auto sc start ViewFaceService启动后验证要看两块:一是进程有没有起来,二是HTTP接口能不能通。如果源码自带健康检查接口就调用它,没有就调用人脸检测接口做冒烟测试。
curl http://127.0.0.1:9100/api/health返回JSON包含服务名和模型加载状态,就说明服务底层没毛病。返回超时或连接拒绝,就去翻日志,重点看模型文件是否加载成功。
3.4 Windows防火墙放行规则
这一步容易被忽略。服务在本地跑得好好的,换到局域网其他机器调用就超时,十有八九是防火墙拦了。
netsh advfirewall firewall add rule name="ViewFaceService" dir=in action=allow protocol=TCP localport=9100这条命令给TCP 9100端口加一条入站放行规则。企业内网如果有安全策略,还需要提工单让网管在机房防火墙放行该端口。电力或制造业现场经常有隔离网段,服务所在机器和客户端所在机器必须能互相ping通。
4. 调用与集成:从HTTP接口到C#客户端封装
服务部署起来只是第一步,真正干活的是接口调用。这一章给你一套可抄的客户端代码,以及注册特征库的工程做法。
4.1 接口协议与请求格式
ViewFaceService对外提供的接口一般遵循REST风格。最核心的两个:人脸检测与特征提取。请求方式通常是POST,图片以Base64字符串形式放JSON里传输。
{ "imageBase64": "/9j/4AAQSkZJRgABAQAAAQABAAD...", "maxFaceCount": 5 }maxFaceCount表示一张图最多检测多少人脸,超过会被丢弃。实际项目中我会设3,因为闸机或考勤机前一般不会同时站超过三个人。imageBase64是原始图片的Base64编码,注意不要压成缩略图,分辨率太小会丢人脸细节。
4.2 C#客户端请求封装
既然标题叫C#人脸比对服务,客户端自然要用C#写。用HttpClient封装接口,别每次新建实例,这是性能底线。
public class FaceServiceClient { private readonly HttpClient _http; private readonly string _baseUrl; public FaceServiceClient(string baseUrl) { _baseUrl = baseUrl.TrimEnd('/'); _http = new HttpClient(); _http.Timeout = TimeSpan.FromSeconds(15); } public async Task<float[]> ExtractFeatureAsync(string imageBase64, CancellationToken ct = default) { var payload = new { imageBase64 }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var resp = await _http.PostAsync($"{_baseUrl}/api/face/extract", content, ct); resp.EnsureSuccessStatusCode(); var responseJson = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(responseJson); var root = doc.RootElement; if (root.TryGetProperty("feature", out var feature)) { return feature.EnumerateArray().Select(x => x.GetSingle()).ToArray(); } throw new Exception($"提取特征失败: {root}"); } }HttpClient在 .NET 6 之后建议用IHttpClientFactory管理生命周期,但独立工具类里保持单例就能用。超时设置15秒,离线模型服务正常一次特征提取不会超过2秒,超过就得查服务端日志。返回的feature是一个浮点数组,长度取决于模型输出维度,ViewFaceService这个体系的特征通常是512维。
4.3 注册底图:建本地特征库
比对不能每次拿着两张原图去服务端算,那样又慢又费流量。正确做法是注册阶段算好特征,存本地。常见的存储介质是SQLite或内存字典。结合C#生态,SQLite是顺手的选择。
public class FaceRegistry { private readonly FaceServiceClient _client; private readonly Dictionary<string, float[]> _store = new(); public async Task RegisterAsync(string personId, string imageBase64) { var feature = await _client.ExtractFeatureAsync(imageBase64); _store[personId] = feature; } public float Compare(string personId, float[] targetFeature) { if (!_store.TryGetValue(personId, out var baseFeature)) throw new KeyNotFoundException($"未注册: {personId}"); return CosineSimilarity(baseFeature, targetFeature); } private static float CosineSimilarity(float[] a, float[] b) { float dot = 0, normA = 0, normB = 0; for (int i = 0; i < a.Length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dot / (float)(Math.Sqrt(normA) * Math.Sqrt(normB)); } }相似度计算用余弦相似度,值越接近1越相似。这个类把注册库和比对逻辑收在了一起,后续换存储或加缓存都有地方改。RegisterAsync只在人员录入时调用,运行时比对只走Compare方法,设计上就把重操作挡在了高频路径之外。
4.4 图像采集与Base64编码
图片怎么来取决于你的场景。USB摄像头采集用DirectShow或AForge,网络摄像机走RTSP拉流。人脸识别对图片质量的要求是:人脸区域不要小于64x64像素,光线均匀,不要大角度侧脸。
using var ms = new MemoryStream(); bitmap.Save(ms, ImageFormat.Jpeg); var base64 = Convert.ToBase64String(ms.ToArray());编码后的Base64字符串会很长,一兆左右的JPEG编码出来约1.4MB字符。HTTP传输没问题,但局域网里注意不要阻塞带宽。你把这串交到服务端时,服务端会解码再送进模型。
5. 离线部署常见问题排查:三个必踩的坑
离线部署的坑不在代码逻辑,而在环境差异。下面集中写我拆这个资源时遇到的高频问题,按现象到原因再到解决来展开。
5.1 提示找不到模型文件
现象是服务启动后访问接口,返回模型加载失败,日志里有个FileNotFound或DirectoryNotFoundException。原因大概率是模型路径写错了,或者当前工作目录不是发布目录。Windows服务方式启动的进程,工作目录是C:\Windows\System32,不是exe所在目录。
解决方式:把配置里的ModelPath改成绝对地址,或者读取exe所在目录再拼接:
var modelPath = Path.Combine(AppContext.BaseDirectory, "models");5.2 调用时抛DllNotFoundException
这个错在C#调用原生库时尤其常见。原因有两个方向,一是目标机器缺VC++运行库,二是原生库的位数和进程位数不匹配,常见于32位进程加载64位DLL。
解决方式是先确认进程位数,再确认原生DLL位数。用命令查看:
# 查看进程位数 tasklist /fi "imagename eq ViewFaceService.exe" /v输出里能看到进程是32位还是64位。如果进程是x86,就去发布目录找x64子目录里的原生DLL,确认它是64位还是32位。这个坑在离线部署时很隐蔽,因为开发机可能同时装了两种运行时,而目标机只装了一种。
5.3 首次请求特别慢
现象是服务启动后第一次调用花了十几秒,后续调用恢复正常。原因是模型懒加载。服务启动时只加载了网络框架,模型文件到首次请求时才从磁盘读入内存并初始化。
解决方式是启动后主动做一次预热请求,或者在服务启动流程里强制调用一次初始化。我一般会在部署脚本的末尾加一行curl指令,让服务刚启动就收到一次假请求,把模型预热好。
5.4 USB摄像头取流失败导致服务频繁启动
这个问题的现场是:摄像头偶发断流,客户端拿不到帧就反复重连,重连时没释放旧连接,资源越积越多。这不是ViewFaceService自身的问题,但集成时很容易误判为服务不稳定。
解决方式是在客户端加断线重连策略,采集不到帧时等待2秒再重连,不要写成死循环。摄像头驱动选厂商提供的WDM驱动,别用UVC通用驱动,兼容性会好很多。
6. 最后一公里的性能调优:把CPU占用和响应时延压下来
服务能跑和跑得好是两码事。这一章给几个我在现场常用的调优手段,全部围绕CPU和内存展开,因为离线部署的机器往往配置不高。
6.1 并发模型:线程池参数调整
默认的ThreadPool适合IO密集型任务,但人脸特征提取是CPU密集型。高并发时线程切换反而拉低整体吞吐。我就吃过亏——上线第二天发现CPU占用90%,但每秒处理量不到5次。
调优方向是限制并发度。在服务端给特征提取接口的信号量设上限,让请求排队而不是抢CPU:
private static readonly SemaphoreSlim _gate = new(2); public async Task<IActionResult> Extract() { await _gate.WaitAsync(); try { // 实际推理逻辑 } finally { _gate.Release(); } }SemaphoreSlim初始值设为2或4,对应机器的物理核心数减一。机器是4核8线程时设3,8核设5。这个值需要压测微调,但方向是排队比抢占稳定得多。
6.2 特征库检索加速:先聚类再精确比对
1:N检索在注册库过千后,全量遍历开始吃力。C#端的Parallel.For能缩短耗时,但治标不治本。
我后来的做法是给注册库分桶:先对全部特征做一次聚类,桶中心存下来,检索时先比桶中心,选出最近的3个桶,再进桶内逐条精比对。注册库5000人的时候,检索范围从5000缩减到300左右,效果很明显。
// 分桶后的检索逻辑 var candidates = _buckets .OrderBy(b => CosineSimilarity(b.Center, target)) .Take(3) .SelectMany(b => b.Members); var best = candidates .OrderByDescending(c => CosineSimilarity(c.Feature, target)) .FirstOrDefault();这段代码适合注册人员长期稳定、不频繁变动的场景。人员经常加减的现场,维护桶中心的成本反而高于全量遍历。
6.3 模型和图像分辨率的选择
SeetaFace6体系里有不同精度的模型,代价是尺寸和耗时的差异。默认模型在CPU上单次特征提取大约200毫秒到400毫秒,升级大模型后可能到800毫秒以上。很多场景下,默认模型配合好一点的光线控制,比换大模型带来的收益更大。
图像输入分辨率不要无脑大。人脸区域超过200x200像素后,识别率提升非常有限,但预处理时间成倍上涨。服务端可以对输入图先做一次人脸检测,只把检测到的人脸区域缩放后送进特征提取,比整图送进去快得多。
6.4 我曾经踩过的生产事故
这套方案的另一个坑是内存泄漏。最开始我在RegisterAsync里每次调用都 new 一个HttpClient,跑了一周后,程序内存从300MB涨到1.2GB。后来排查发现HttpClient底层占用socket不释放。从那以后我每次做C#人脸服务集成,都会强制检查一遍所有HttpClient的使用方式,确保整个服务生命周期里只有一个实例。
处理器:从2核心4线程工控机到8核心16线程服务器,都能跑。低于这个配置就不要开并发调优,老老实实串行处理。希望这一套拆解思路能帮你把ViewFaceService用稳,少走我踩过的弯路。
本文还有配套的精品资源,点击获取