ASP.NET Core + FaceRecognitionDotNet 人脸识别实战:DI 单例、特征向量持久化与 1:N 比对
关键词:.NET 8 / FaceRecognitionDotNet / dlib / 128 维特征 / 反射 / 依赖注入
适用场景:中小型系统里的人脸录入与 1:N 身份识别(考勤、门禁、签到、人员核验)
一、需求背景
业务上需要一个很朴素的能力:
- 录入(Enroll):上传一张人脸照片 + 姓名,把人脸"存下来";
- 识别(Recognize):再传一张照片,告诉我是"库里的谁",顺便给个置信度。
听起来简单,但真做起来会遇到三个坑:
- 模型加载极慢,绝不能每次请求都
Create一次; - dlib 的人脸特征对象
FaceEncoding没有公开构造函数,存进数据库之后不知道怎么还原; - 缓存 / 数据库里存什么格式、阈值定多少、并发怎么控。
下面这套实现全部踩过一遍,直接给你能跑的版本。
二、技术选型与原理
FaceRecognitionDotNet 是 dlib 的 .NET 封装,识别流程和 Python 的 face_recognition 完全一致:
#mermaid-svg-39TTwy9FO8CCvkIh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-39TTwy9FO8CCvkIh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-39TTwy9FO8CCvkIh .error-icon{fill:#552222;}#mermaid-svg-39TTwy9FO8CCvkIh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-39TTwy9FO8CCvkIh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-39TTwy9FO8CCvkIh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-39TTwy9FO8CCvkIh .marker.cross{stroke:#333333;}#mermaid-svg-39TTwy9FO8CCvkIh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-39TTwy9FO8CCvkIh p{margin:0;}#mermaid-svg-39TTwy9FO8CCvkIh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-39TTwy9FO8CCvkIh .cluster-label text{fill:#333;}#mermaid-svg-39TTwy9FO8CCvkIh .cluster-label span{color:#333;}#mermaid-svg-39TTwy9FO8CCvkIh .cluster-label span p{background-color:transparent;}#mermaid-svg-39TTwy9FO8CCvkIh .label text,#mermaid-svg-39TTwy9FO8CCvkIh span{fill:#333;color:#333;}#mermaid-svg-39TTwy9FO8CCvkIh .node rect,#mermaid-svg-39TTwy9FO8CCvkIh .node circle,#mermaid-svg-39TTwy9FO8CCvkIh .node ellipse,#mermaid-svg-39TTwy9FO8CCvkIh .node polygon,#mermaid-svg-39TTwy9FO8CCvkIh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-39TTwy9FO8CCvkIh .rough-node .label text,#mermaid-svg-39TTwy9FO8CCvkIh .node .label text,#mermaid-svg-39TTwy9FO8CCvkIh .image-shape .label,#mermaid-svg-39TTwy9FO8CCvkIh .icon-shape .label{text-anchor:middle;}#mermaid-svg-39TTwy9FO8CCvkIh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-39TTwy9FO8CCvkIh .rough-node .label,#mermaid-svg-39TTwy9FO8CCvkIh .node .label,#mermaid-svg-39TTwy9FO8CCvkIh .image-shape .label,#mermaid-svg-39TTwy9FO8CCvkIh .icon-shape .label{text-align:center;}#mermaid-svg-39TTwy9FO8CCvkIh .node.clickable{cursor:pointer;}#mermaid-svg-39TTwy9FO8CCvkIh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-39TTwy9FO8CCvkIh .arrowheadPath{fill:#333333;}#mermaid-svg-39TTwy9FO8CCvkIh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-39TTwy9FO8CCvkIh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-39TTwy9FO8CCvkIh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-39TTwy9FO8CCvkIh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-39TTwy9FO8CCvkIh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-39TTwy9FO8CCvkIh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-39TTwy9FO8CCvkIh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-39TTwy9FO8CCvkIh .cluster text{fill:#333;}#mermaid-svg-39TTwy9FO8CCvkIh .cluster span{color:#333;}#mermaid-svg-39TTwy9FO8CCvkIh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-39TTwy9FO8CCvkIh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-39TTwy9FO8CCvkIh rect.text{fill:none;stroke-width:0;}#mermaid-svg-39TTwy9FO8CCvkIh .icon-shape,#mermaid-svg-39TTwy9FO8CCvkIh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-39TTwy9FO8CCvkIh .icon-shape p,#mermaid-svg-39TTwy9FO8CCvkIh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-39TTwy9FO8CCvkIh .icon-shape .label rect,#mermaid-svg-39TTwy9FO8CCvkIh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-39TTwy9FO8CCvkIh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-39TTwy9FO8CCvkIh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-39TTwy9FO8CCvkIh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
图片 Bitmap
FaceLocations
HOG/CNN 人脸检测
FaceEncodings
68 关键点对齐 + ResNet
128 维 double 向量
欧氏距离比对
距离 < 阈值?
命中该人员
未知人员
核心结论:一张人脸 = 一个 128 维向量 。识别不是"图像相似度",而是"向量空间里的欧氏距离"。距离越小越像,dlib 官方建议阈值 0.6。
所以持久化时我们根本不需要存图片,只存这 128 个 double 就够了------这也是本方案能做到毫秒级比对的原因。
三、环境准备
3.1 NuGet 包
xml
<PackageReference Include="FaceRecognitionDotNet" Version="1.0.7.7" />
<PackageReference Include="DlibDotNet" Version="19.21.0.2" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
DlibDotNet的 native 运行库(DlibDotNetNative.dll等)会随包一起还原,Windows x64 开箱可用。
3.2 下载 dlib 模型文件
去 http://dlib.net/files/ 下载并放到项目的 models 目录(4 个文件一个都不能少):
| 文件名 | 用途 |
|---|---|
shape_predictor_5_face_landmarks.dat |
5 点关键点(快速对齐) |
shape_predictor_68_face_landmarks.dat |
68 点关键点(主用) |
mmod_human_face_detector.dat |
CNN 人脸检测器 |
dlib_face_recognition_resnet_model_v1.dat |
ResNet 特征提取(输出 128 维) |
在 .csproj 里配置"复制到输出目录",否则运行时上班目录(bin/Debug/net8.0/models)是空的:
xml
<ItemGroup>
<None Update="models\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
四、第一步:注册为单例(关键)
csharp
// 注意:FaceRecognition 创建时很消耗资源,并且需要加载模型,必须注册为单例 (Singleton)
builder.Services.AddSingleton<FaceRecognition>(sp =>
{
// 确保 "models" 文件夹存在于程序的运行目录(bin/Debug/net8.0)下
// 如果路径不对,Create 方法会抛出异常,导致 DI 失败
var modelPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "models");
return FaceRecognition.Create(modelPath);
});
为什么必须是 Singleton?
FaceRecognition.Create() 内部会做两件事:
- 把
.dat模型文件反序列化成 dlib 的 C++ 对象(ShapePredictor、Anet、ResNet 权重); - 建立 native 内存句柄。
这个过程是 秒级 的,而且占用几十到上百 MB 内存。如果注册成 Scoped(ASP.NET Core 里最容易犯的错),每个 HTTP 请求都会重新加载一遍模型------轻则接口 3 秒起步,重则内存直接打满、OutOfMemoryException。
顺手再强调两个点:
FaceRecognition是线程安全的(内部无可变共享状态),单例可以并发调用;- 但它 CPU 密集 ,高并发下建议用
SemaphoreSlim限流(见第九节),别让线程池被拖死。
五、第二步:定义人员实体与存储格式
csharp
// 人员实体
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
// 存储 128 维特征向量的 JSON 序列化字符串
public string FaceEncodingJson { get; set; }
}
FaceEncodingJson 存的内容长这样,就是一段纯数组:
json
[0.123,-0.0456,0.0789, ... ,0.0012]
别存图片、别存 Base64、别存 byte\[\]。128 × 8 字节 = 1KB,一个人的特征占 1KB 左右,10000 人也才 10MB,随便放哪儿都行。
六、第三步:录入(Enroll)------把 FaceEncoding 变成可存储的 JSON
csharp
public async Task<APIResult> Enroll(RecognizeRequest request)
{
// 1. 处理图片(去掉 data:image/jpg;base64, 前缀后转 Bitmap)
var base64Data = ImgHelper.GetBase64String(request.ImageBase64);
byte[] imageBytes = Convert.FromBase64String(base64Data);
using var ms = new MemoryStream(imageBytes);
using var bitmap = new Bitmap(ms);
// 包一层 dlib 的 Image,注意 using(native 资源)
using var image = FaceRecognition.LoadImage(bitmap);
// 2. 找脸
var locations = _faceRecognition.FaceLocations(image).ToArray();
if (locations.Length == 0) return APIResult.Error("没找到脸");
// 3. 提取特征
var encodings = _faceRecognition.FaceEncodings(image, locations).ToArray();
using var targetEncoding = encodings[0];
// 4. ★★★ 关键所在:生成 FaceEncodingJson ★★★
// 先拿到 128 维数组
double[] rawData = targetEncoding.GetRawEncoding();
// 把数组序列化成 JSON 字符串,例如变成 "[0.123, 0.456, 0.789, ...]"
string jsonString = JsonSerializer.Serialize(rawData);
// 5. 存入缓存 / 数据库(此时 FaceEncodingJson 字段就有值了)
var person = new Person
{
Id = Guid.NewGuid().ToString("N"), // ← 注意:一定要生成 Id,否则识别结果里 personId 永远是 null
Name = request.Name, // 比如 "张三"
FaceEncodingJson = jsonString // 这里存的就是上面生成的字符串
};
var list = await App.Cache.GetAsync<List<Person>>("rlsb") ?? new List<Person>();
list.Add(person);
await App.Cache.SetAsync("rlsb", list, TimeSpan.FromDays(1));
// _dbContext.Persons.Add(person);
// _dbContext.SaveChanges();
return APIResult.Success("录入成功");
}
这里唯一的"知识点"就是 GetRawEncoding():它是 FaceEncoding 暴露出来的唯一一个导出接口 ,把内部 Matrix<double> 拉平成 double[128]。
如果你的
FaceRecognitionDotNet版本比较老、没有这个方法,可以反射读私有字段(字段名在不同版本里可能是_Matrix或Matrix):
csharpvar field = typeof(FaceEncoding).GetField("_Matrix", BindingFlags.NonPublic | BindingFlags.Instance) ?? typeof(FaceEncoding).GetField("Matrix", BindingFlags.NonPublic | BindingFlags.Instance); var matrix = (Matrix<double>)field.GetValue(targetEncoding); double[] rawData = matrix.ToArray();
七、第四步:识别(Recognize)------用反射重建 FaceEncoding
这才是整篇文章最"脏"也最有价值的部分。
7.1 为什么必须反射?
FaceEncoding 这个类型的设计是只出不进:
- 公开 API 里只有
FaceEncodings(image, locations)能从图片造出它; - 没有任何
new FaceEncoding(double[])或FromArray()之类的入口; - 它的构造函数是 internal 的:
internal FaceEncoding(Matrix<double> matrix)。
也就是说,你想把落库的 128 维数组还原成 FaceEncoding 对象,再用官方的 FaceDistance() 比对,反射是唯一的路。
7.2 反射重建代码
csharp
/// <summary>
/// 通过反射,从 DlibDotNet 的 Matrix 重建 FaceEncoding
/// </summary>
public FaceEncoding CreateFaceEncodingByReflection(Matrix<double> matrix)
{
// 1. 获取 FaceEncoding 的类型
var faceEncodingType = typeof(FaceEncoding);
// 2. 查找那个接受 Matrix<double> 的私有构造函数
var constructor = faceEncodingType.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null,
new[] { typeof(Matrix<double>) },
null);
if (constructor == null)
{
throw new MissingMethodException("未找到 FaceEncoding 的私有构造函数。");
}
// 3. 调用构造函数,创建实例
return (FaceEncoding)constructor.Invoke(new object[] { matrix });
}
7.3 1:N 比对主流程
csharp
public async Task<APIResult> Recognize(RecognizeRequest request)
{
// 1. 图片预处理
var base64Data = ImgHelper.GetBase64String(request.ImageBase64);
byte[] imageBytes = Convert.FromBase64String(base64Data);
using var ms = new MemoryStream(imageBytes);
using var bitmap = new Bitmap(ms);
using var image = FaceRecognition.LoadImage(bitmap);
// 2. 检测人脸并获取特征编码(128 维向量)
var faceLocations = _faceRecognition.FaceLocations(image).ToArray();
if (faceLocations.Length == 0)
return APIResult.Error("照片中未检测到人脸");
if (faceLocations.Length > 1)
return APIResult.Error("照片中检测到多个人脸,请确保只传一人");
// 3. 提取传进来的人脸特征
var targetEncodings = _faceRecognition.FaceEncodings(image, faceLocations).ToArray();
var targetEncoding = targetEncodings.FirstOrDefault();
if (targetEncoding == null)
return APIResult.Error("无法提取人脸特征");
// 4. 从数据库 / 缓存中获取系统中所有人脸特征
var allPersons = await App.Cache.GetAsync<List<Person>>("rlsb") ?? new List<Person>();
// 5. 遍历比对,计算相似度
double bestDistance = double.MaxValue;
Person bestMatchPerson = null;
// 常用阈值:0.6(dlib 官方);0.45 更严格,误识率更低
const double Tolerance = 0.45;
foreach (var person in allPersons)
{
if (string.IsNullOrWhiteSpace(person.FaceEncodingJson)) continue;
// 反序列化并比对
double[] rawData = JsonSerializer.Deserialize<double[]>(person.FaceEncodingJson);
if (rawData is null || rawData.Length != 128) continue;
// 包装成 Matrix,注意:不要把 matrix 用 using 包起来(所有权已转移给 FaceEncoding)
var matrix = new Matrix<double>(rawData, rawData.Length, 1);
using var knownEncoding = CreateFaceEncodingByReflection(matrix);
// 计算两张人脸的欧氏距离
var distance = FaceRecognition.FaceDistance(targetEncoding, knownEncoding);
if (distance < Tolerance && distance < bestDistance)
{
bestDistance = distance;
bestMatchPerson = person;
}
}
// 6. 返回结果
if (bestMatchPerson != null)
{
// 距离越小,置信度越高(简单线性映射,仅用于展示)
double confidence = Math.Round(1.0 - (bestDistance / Tolerance), 2);
return new APIResult
{
data = new
{
isSuccess = true,
personId = bestMatchPerson.Id,
personName = bestMatchPerson.Name,
confidence,
message = "识别成功"
}
};
}
return APIResult.Error("未匹配到系统中的人员");
}
八、踩坑清单(这一节最值钱)
坑 1:Matrix<double> 不能用 using
csharp
// ❌ 错误:matrix 会被提前释放,FaceEncoding 内部持有的是野指针
using var matrix = new Matrix<double>(rawData, rawData.Length, 1);
using var encoding = CreateFaceEncodingByReflection(matrix);
// ✅ 正确:所有权转移给 FaceEncoding,由它统一释放
var matrix = new Matrix<double>(rawData, rawData.Length, 1);
using var encoding = CreateFaceEncodingByReflection(matrix);
FaceEncoding 的构造函数会接管 Matrix 的所有权,FaceEncoding.Dispose() 内部会连带释放 Matrix。如果你再 using 一次 Matrix,就是 double free ,表现为随机崩溃或 AccessViolationException------这种 bug 极难排查。
坑 2:Enroll 忘了给 Id 赋值
原版代码里 Person 只赋了 Name 和 FaceEncodingJson,Id 是 null。结果就是识别明明成功了,前端拿到的 personId 却是 null,业务层直接炸。录入时必须 Id = Guid.NewGuid().ToString("N")。
坑 3:阈值 0.45 和 0.6 的取舍
| 阈值 | 误识率 FAR | 拒识率 FRR | 适用场景 |
|---|---|---|---|
| 0.6 | 偏高 | 低 | 相册聚类、宽松查找 |
| 0.5 | 中 | 中 | 大多数业务(推荐起点) |
| 0.45 | 低 | 偏高 | 门禁、考勤、身份核验(宁拒不错) |
不要迷信单一阈值。正确做法是拿你自己业务的照片(不同光照、角度、眼镜、口罩)跑一批样本,画 FAR/FRR 曲线再定。1:N 场景下 N 越大,越要往严了调。
坑 4:System.Drawing.Bitmap 的跨平台限制
System.Drawing.Common 在 .NET 6+ 已 仅支持 Windows 。部署到 Linux 容器会抛 PlatformNotSupportedException。Linux 上的替代方案:
csharp
// 用 ImageSharp 解码,再交给 dlib
using var image = SixLabors.ImageSharp.Image.Load<Rgb24>(imageBytes);
using var bmp = new Bitmap(image.Width, image.Height, PixelFormat.Format24bppRgb);
// ... 逐像素拷贝(或用 FaceRecognition.LoadImage(byte[]) 重载)
坑 5:缓存版"数据库"的三个隐患
示例里用 App.Cache 存人员列表,上线前请务必替换:
| 隐患 | 后果 | 解法 |
|---|---|---|
TimeSpan.FromDays(1) 过期 |
第二天所有人脸数据凭空消失 | 落库(SQLite / MySQL / PostgreSQL) |
| 读-改-写无锁 | 并发录入互相覆盖,丢数据 | 分布式锁 / 数据库事务 |
| 多实例部署 | 各节点缓存不一致 | 集中存储 + 本地只读缓存 |
坑 6:FaceLocations 的第二个参数
csharp
_faceRecognition.FaceLocations(image) // 默认 upsample = 1
_faceRecognition.FaceLocations(image, 2) // 远处的小脸能检测到,但慢 3~4 倍
_faceRecognition.FaceLocations(image, 0) // 大脸场景,最快
默认 HOG 检测器对 小于 80×80 像素 的人脸基本无能为力。如果业务是"手机自拍/证件照",默认值够用;如果是"监控画面远端人脸",就得加大 upsample 或改用 FaceDetectorModels.Cnn。
九、生产化改造建议
9.1 更优雅的方案:根本不需要反射
说到底,FaceDistance 干的事情就是两向量欧氏距离:
csharp
private static double EuclideanDistance(double[] a, double[] b)
{
if (a.Length != b.Length) throw new ArgumentException("特征维度不一致");
double sum = 0;
for (int i = 0; i < a.Length; i++)
{
double d = a[i] - b[i];
sum += d * d;
}
return Math.Sqrt(sum);
}
那么整个识别流程可以简化成:只保留 query 一张图的 FaceEncoding,库里所有人都直接拿 double[] 算距离。
收益非常明显:
- 彻底摆脱反射(不再受内部构造函数改名、字段改名影响);
- 省掉每次循环 new 一个
Matrix+FaceEncoding的 native 分配与释放,1:N 场景下性能提升 5~10 倍; - 代码可测试、可移植,甚至能把比对丢到 SIMD / GPU 或向量数据库里。
csharp
var queryRaw = targetEncoding.GetRawEncoding(); // 只算一次
foreach (var person in allPersons)
{
var raw = JsonSerializer.Deserialize<double[]>(person.FaceEncodingJson);
var distance = EuclideanDistance(queryRaw, raw);
// ...
}
结论:反射方案适合"我想用官方 API 保证行为一致",纯数组方案适合"我要性能和稳定"。两者结果数值上完全一致(同一套浮点运算),可以放心替换。
9.2 并发限流
dlib 是纯 CPU 计算,10 个并发请求就能把 8 核机器打满,然后整站接口一起变慢。用一个全局信号量兜住:
csharp
private static readonly SemaphoreSlim Gate = new(Environment.ProcessorCount);
public async Task<APIResult> Recognize(RecognizeRequest request)
{
await Gate.WaitAsync();
try
{
// 上面的识别逻辑
}
finally
{
Gate.Release();
}
}
9.3 特征缓存到内存
如果人员库不大(< 5000)且变更不频繁,可以把 double[] 在启动时一次性加载进 ConcurrentDictionary<string, double[]>,比对时零反序列化开销。
9.4 数据量上来之后
线性扫全库是 O(N)。N 超过 1 万以后,单次识别要几百毫秒。此时应该上 向量索引:
pgvector(PostgreSQL 扩展,最省事)- Milvus / Qdrant(专业向量库)
- Faiss(本地索引,适合单机)
9.5 别忘了安全和合规
- 人脸是生物识别信息 ,属于《个人信息保护法》里的敏感个人信息,必须单独告知 + 单独同意;
- 建议只存特征向量、不存原图;特征不可逆,泄露风险远低于照片;
- 线上一定要加 活体检测(眨眼/张嘴/动作指令),否则一张打印照片就能骗过整套系统。
十、完整接口定义
csharp
public interface IAgentAIService
{
Task<APIResult> Enroll(RecognizeRequest request);
Task<APIResult> Recognize(RecognizeRequest request);
}
public class RecognizeRequest
{
/// <summary>带前缀的 Base64,如 data:image/jpeg;base64,/9j/4AAQ...</summary>
public string ImageBase64 { get; set; }
/// <summary>录入时使用</summary>
public string Name { get; set; }
}
/// <summary>
/// 获取真正的图片 base64 数据
/// 即去掉 data:image/jpg;base64, 这样的格式
/// </summary>
public static class ImgHelper
{
public static string GetBase64String(string base64UrlStr)
{
const string pattern = "^(data:image/.*?;base64,).*?$";
var match = Regex.Match(base64UrlStr, pattern);
if (match.Groups.Count > 1)
base64UrlStr = base64UrlStr.Replace(match.Groups[1].ToString(), "");
return base64UrlStr;
}
}
关于 GetBase64String 有个小建议:match.Groups.Count > 1 这个判断在没有匹配 时也是 1(只有 Group 0),所以逻辑是对的;但更稳的写法是判断 match.Success:
csharp
if (match.Success) base64UrlStr = base64UrlStr[match.Groups[1].Length..];
Substring 比 Replace 安全------万一 Base64 正文里恰好出现同样的前缀字符串,Replace 会把正文也削掉。
十一、总结



| 要点 | 一句话 |
|---|---|
| DI 注册 | 必须 Singleton,模型加载是秒级 + 上百 MB |
| 持久化 | 只存 128 维 double[] 的 JSON,1KB/人 |
| 还原 | FaceEncoding 构造函数是 internal,只能反射 |
| 更优解 | 自己写欧氏距离,绕过反射,性能提升数倍 |
| 阈值 | 门禁类业务 0.45,检索类 0.6,别拍脑袋定 |
| 释放 | Matrix 交给 FaceEncoding 管,千万别重复 using |
| 合规 | 人脸是敏感个人信息,存特征不存图,上线配活体 |
整套逻辑不到 200 行,真正难的不是"调用库",而是上面这七个细节。希望对正在做同类需求的同学少踩两个坑。
有问题欢迎评论区交流,尤其是阈值调参 和跨平台部署这两块,踩过的坑最多。