资讯动态

OpenCvSharp4人脸识别C#完整代码:LBPH工控机摄像头

发布时间:2026/10/25 12:26:30 来源:尧图企业网站定制
简介面向具备 C# 基础、希望用 OpenCV 完成摄像头实时人脸检测与识别的开发者这份完整工程以 WinForms 应用组织界面、检测与识别模块借助 Haar 级联分类器定位人脸、LBPH 识别器完成比对并支持启动摄像头、采集人脸与录入姓名。压缩包共 476 个文件约 69.28MB除 7 个 cs 源码和 sln、csproj 工程文件外还包含 137 个 dll、106 个 xml、44 个 txt 及 17 个 nupkg 等依赖库、模型配置与说明材料便于还原运行环境。已有 647 人学习读者可据此理解实时视频逐帧处理流程、界面与算法模块的衔接方式并参考现有结构补充训练样本、调整识别阈值以改进准确率也可作为课程设计或桌面端视觉应用的原型基础。1. 工控机摄像头直连OpenCV 人脸识别 C# 完整代码能解决哪类活我见过最常见的场景是工控机上直接读 USB 摄像头做人脸识别不想额外起 Python 服务也不想把视频帧传来传去。这类需求拿 OpenCV 人脸识别 C# 完整代码做底子很合适OpenCvSharp4 负责采集、检测和 LBPH 训练C# 负责业务、配置和界面一套解决方案就能编译出 x64 桌面程序。它适合想快速验证门禁、考勤、访客登记原型的开发者也适合要把 Python Demo 迁到 .NET 桌面端的团队。代码量不大但路径、摄像头后端、阈值这几个点很容易翻车。下面按项目骨架、核心链路、避坑和二次开发四段拆尽量让新手能跑通熟手能直接改。2. 项目骨架与运行链路OpenCvSharp4 选型、NuGet 依赖和目录结构拿到一份 C# 人脸识别完整代码先别急着看识别算法先看解决方案怎么分层。OpenCV 原生是 CC# 只能通过封装层调用封装层选错后面打包、拷贝 DLL、跨平台都会成倍麻烦。我一般把项目拆成 Core 和 App 两个工程Core 只做检测、训练、识别App 负责摄像头、配置和 UI。这样后面把识别搬到服务端或工控机算法层不用重写。运行链路也要先画清楚读取 appsettings.json加载 Haar 模型打开摄像头逐帧转灰度并检测人脸裁剪出人脸区域做归一化送进 LBPH 识别器最后按阈值决定返回标签还是“未知”。这条链路里任何一个路径参数写错表现都不是报错而是检测结果为空排查时很容易抓瞎。2.1 技术选型OpenCvSharp4 和 Emgu.CV 怎么选C# 调 OpenCV 常见的有两个封装OpenCvSharp4 和 Emgu.CV。不是谁绝对更好而是交付形态不同。下面这张表是我选型时实际会看的几个点维度OpenCvSharp4Emgu.CVNuGet 依赖拆得细按平台装 runtime 包包体较大常带 native 依赖API 风格接近原生 OpenCV函数名好对应.NET 封装更多老项目资料多部署需要带 OpenCvSharpExtern、ffmpeg 等 DLL同样要处理 native DLL许可证Apache-2.0GPL 或商业授权适合场景轻量桌面、服务端、跨平台老项目、已有商业授权我选 OpenCvSharp4 的主要原因是 API 贴近原生 OpenCV网上 C 的例子稍作替换就能用另一个原因是包体相对可控作为 opencv c# 软件/插件 交付给客户时输出目录里多几个 DLL 还能接受但如果封装层自带一大堆用不上的模块打包和升级都会变重。这里有个选型细节如果项目只在 Windows 上跑装OpenCvSharp4.runtime.win就够如果要上 Linux 工控机换成对应的runtime.ubuntu包。不要把 Windows 的 runtime 包带到 Linux 发布目录里否则运行时会报DllNotFoundException而且报错信息只告诉你缺某个 so不会告诉你装错了包。2.2 解决方案分层与目录结构我一般把代码分成 Core、App、Tests 三层。Core 不引用摄像头和 UI只接收Mat返回检测框和识别结果App 负责把摄像头帧喂给 CoreTests 用来跑阈值扫描和样本统计。目录大概是这样# 目录结构示意 FaceRecognition/ ├─ FaceRecognition.sln ├─ src/ │ ├─ FaceRecognition.Core/ │ │ ├─ FaceRecognition.Core.csproj │ │ ├─ Detection/ │ │ │ └─ HaarFaceDetector.cs │ │ ├─ Recognition/ │ │ │ ├─ LbphFaceRecognizer.cs │ │ │ └─ FaceResult.cs │ │ └─ Preprocessing/ │ │ └─ FacePreprocessor.cs │ └─ FaceRecognition.App/ │ ├─ FaceRecognition.App.csproj │ ├─ Program.cs │ ├─ appsettings.json │ └─ Camera/ │ └─ CameraFrameSource.cs ├─ models/ │ └─ haarcascade_frontalface_default.xml ├─ data/ │ ├─ faces/ │ │ ├─ 1/ │ │ │ ├─ 001.png │ │ │ └─ 002.png │ │ └─ 2/ │ └─ labels.csv ├─ output/ └─ tests/ └─ FaceRecognition.Tests/ └─ ThresholdScanTests.cs模型文件放models/训练样本放data/faces/{labelId}/训练产物写output/lbph_model.yml标签映射单独放data/labels.csv。不要把模型和样本塞进bin/Debug因为清理解决方案时很容易被一起删掉。样本目录用数字 ID不要用姓名做目录名中文路径和重名会让后面批量读取变得很啰嗦。labels.csv和样本目录要同步维护。我的习惯是采集样本时同时写 CSV训练时只读 CSV 做显示名映射不靠目录名猜。这样后面加人、删人、改显示名都不用重新整理目录。2.3 环境准备与依赖安装新建解决方案和工程的命令不复杂关键是包别装错工程。Core 只需要 OpenCvSharp4App 需要 OpenCvSharp4 加平台 runtime配置读取还要补两个包dotnet new sln -n FaceRecognition dotnet new classlib -n FaceRecognition.Core -o src/FaceRecognition.Core dotnet new console -n FaceRecognition.App -o src/FaceRecognition.App dotnet new xunit -n FaceRecognition.Tests -o tests/FaceRecognition.Tests dotnet sln add src/FaceRecognition.Core/FaceRecognition.Core.csproj dotnet sln add src/FaceRecognition.App/FaceRecognition.App.csproj dotnet sln add tests/FaceRecognition.Tests/FaceRecognition.Tests.csproj dotnet add src/FaceRecognition.Core package OpenCvSharp4 dotnet add src/FaceRecognition.App package OpenCvSharp4 dotnet add src/FaceRecognition.App package OpenCvSharp4.runtime.win dotnet add src/FaceRecognition.App package OpenCvSharp4.Extensions dotnet add src/FaceRecognition.App package Microsoft.Extensions.Configuration.Json dotnet add src/FaceRecognition.App package Microsoft.Extensions.Configuration.BinderOpenCvSharp4.runtime.win只给 Windows 用Linux 换成对应的runtime.ubuntu包。OpenCvSharp4.Extensions主要用来做 Mat 和 Bitmap 的转换如果只用控制台输出可以先不装。Microsoft.Extensions.Configuration.Json和Binder是用来读appsettings.json的不装的话编译期就会报找不到GetT。App 的 csproj 里还要固定 x64。AnyCPU 在 64 位系统上可能加载 32 位 native DLL表现就是BadImageFormatException。我一般显式写PropertyGroup OutputTypeExe/OutputType TargetFrameworknet8.0/TargetFramework PlatformTargetx64/PlatformTarget RuntimeIdentifierwin-x64/RuntimeIdentifier CopyLocalLockFileAssembliestrue/CopyLocalLockFileAssemblies /PropertyGroup ItemGroup Content Include..\..\models\haarcascade_frontalface_default.xml Linkmodels\haarcascade_frontalface_default.xml CopyToOutputDirectoryPreserveNewest / /ItemGroupPlatformTarget固定 x64RuntimeIdentifier按发布平台改Linux 就写linux-x64。CopyToOutputDirectory让模型文件跟着输出目录走避免调试时当前工作目录在bin/Debug/net8.0而模型还在解决方案根目录。这个坑后面避坑章节还会展开。2.4 配置文件与启动参数配置我一般集中放appsettings.json不把路径硬编码在代码里。一个够用的配置长这样{ Face: { CascadePath: models/haarcascade_frontalface_default.xml, TrainDataDir: data/faces, LabelFile: data/labels.csv, ModelPath: output/lbph_model.yml, ConfidenceThreshold: 70.0, MinFaceSize: 80 }, Camera: { Index: 0, Width: 1280, Height: 720 } }CascadePath是 Haar 模型路径TrainDataDir是样本根目录LabelFile是标签映射ModelPath是训练产物。ConfidenceThreshold是 LBPH 的距离阈值越小越严格MinFaceSize控制最小人脸太小会增加误检太大会漏掉远处的人。Camera.Index是摄像头索引笔记本内置摄像头一般是 0外接 USB 摄像头可能是 1。启动时先打印绝对路径这是血泪经验var config new ConfigurationBuilder() .AddJsonFile(appsettings.json, optional: false) .Build() .GetSection(Face) .GetFaceOptions() ?? throw new InvalidOperationException(Face 配置缺失); Console.WriteLine($Cascade: {Path.GetFullPath(config.CascadePath)}); Console.WriteLine($Model: {Path.GetFullPath(config.ModelPath)});Path.GetFullPath会把相对路径按当前工作目录展开一眼就能看出模型到底找的是哪里。如果打印出来的路径不对先改工作目录或复制文件不要急着怀疑算法。启动命令我一般写成dotnet run --project src/FaceRecognition.App -- --config appsettings.json--config是自定义参数App 里可以读也可以不读。关键是运行目录要固定不要今天在解决方案根目录跑明天在bin/Debug里跑否则相对路径会变。后面部署时把models/、data/、output/一起带到发布目录启动脚本里cd到程序目录再执行。3. 核心模块拆解人脸检测、预处理、训练与识别的一条完整链路这一章拆核心代码。顺序按一帧图像的真实流向走摄像头采集灰度化Haar 检测裁剪人脸归一化训练或识别。代码不需要写得花关键是资源释放、参数含义和训练/识别尺寸一致。C# 里Mat是非托管资源循环里反复 new 不 Dispose跑十几分钟内存就会涨上去。3.1 摄像头采集与 Mat 生命周期循环里只复用不反复 new采集层我习惯封一个CameraFrameSource内部持有VideoCapture和一个复用的Mat。每帧Read到同一个 Mat 里调用方用完不要长期持有using OpenCvSharp; public sealed class CameraFrameSource : IDisposable { private readonly VideoCapture _capture; private readonly Mat _frame new(); public CameraFrameSource(int index, int width, int height) { // Windows 用 DSHOWLinux 一般用 V4L2 _capture new VideoCapture(index, VideoCaptureAPIs.DSHOW); _capture.Set(VideoCaptureProperties.FrameWidth, width); _capture.Set(VideoCaptureProperties.FrameHeight, height); if (!_capture.IsOpened()) throw new InvalidOperationException($摄像头 {index} 打开失败); } public Mat? Read() { // Read 返回 false 或空帧都说明这一帧不可用 if (!_capture.Read(_frame) || _frame.Empty()) return null; return _frame; } public void Dispose() { _frame.Dispose(); _capture.Dispose(); } }VideoCaptureAPIs.DSHOW是 Windows 上的 DirectShow 后端对很多 USB 摄像头兼容性比默认后端好Linux 改成VideoCaptureAPIs.V4L2。Set设置分辨率不一定被摄像头接受设置完可以读回FrameWidth确认。Read返回的是内部复用的 Mat调用方如果要在后台线程处理必须Clone()一份否则下一帧会覆盖。Dispose顺序先释放 Mat 再释放 VideoCapture反了也不会立刻报错但摄像头释放不干净时下一次打开会失败。这个点在第 4 章会展开。3.2 Haar 级联检测DetectMultiScale 的参数别照抄Haar 级联检测的代码很短参数却最容易被复制粘贴。下面是我常用的检测封装using OpenCvSharp; public sealed class HaarFaceDetector : IDisposable { private readonly CascadeClassifier _cascade; private readonly int _minFaceSize; public HaarFaceDetector(string cascadePath, int minFaceSize) { _cascade new CascadeClassifier(cascadePath); if (_cascade.Empty()) throw new FileNotFoundException($Haar 模型加载失败: {cascadePath}); _minFaceSize minFaceSize; } public Rect[] Detect(Mat frame) { using var gray new Mat(); Cv2.CvtColor(frame, gray, ColorConversionCodes.BGR2GRAY); Cv2.EqualizeHist(gray, gray); return _cascade.DetectMultiScale( gray, scaleFactor: 1.1, minNeighbors: 5, flags: HaarDetectionTypes.ScaleImage, minSize: new Size(_minFaceSize, _minFaceSize)); } public void Dispose() _cascade.Dispose(); }scaleFactor控制每次缩小的比例1.1 是常用起点调到 1.05 检测更细但更慢调到 1.3 快但容易漏小脸。minNeighbors控制候选框合并3 到 6 之间比较常见越大误检越少、漏检越多。minSize要跟摄像头分辨率匹配1280x720 下 80x80 是合理起点太小会把背景纹理当人脸。CascadeClassifier不是线程安全的。如果多个线程共用同一个实例检测结果会随机异常表现是偶尔返回空数组。我的做法是每个线程一个 detector或者在外面加锁。Haar 对正脸有效侧脸、遮挡、逆光效果会明显下降这不是参数能完全救回来的。3.3 预处理灰度、直方图均衡与尺寸归一化检测到人脸框后不要直接把整帧送进识别器。先裁剪 ROI转灰度做直方图均衡再缩放到固定尺寸。训练和识别必须用同一套预处理否则距离值会飘using OpenCvSharp; public sealed class FacePreprocessor { private readonly Size _size new(100, 100); public Mat Normalize(Mat frame, Rect box) { // 防止检测框越界 var safe box.Intersect(new Rect(0, 0, frame.Width, frame.Height)); if (safe.Width 0 || safe.Height 0) throw new ArgumentException(人脸框超出图像范围); using var roi new Mat(frame, safe); using var gray new Mat(); Cv2.CvtColor(roi, gray, ColorConversionCodes.BGR2GRAY); using var equalized new Mat(); Cv2.EqualizeHist(gray, equalized); var resized new Mat(); Cv2.Resize(equalized, resized, _size, interpolation: InterpolationFlags.Area); return resized; } }box.Intersect很关键Haar 检测框偶尔会贴边直接new Mat(frame, box)会抛异常。Cv2.Resize的目标尺寸要和训练时一致示例用 100x100改尺寸可以但训练模型和识别代码必须同时改。InterpolationFlags.Area适合缩小放大一般用Linear。直方图均衡对光照不均有帮助但别过度依赖。如果现场灯光很硬均衡化会把噪声也放大这时候应该先调摄像头曝光而不是继续加预处理。3.4 样本入库与标签映射labels.csv 比内存字典可靠样本目录按标签 ID 分文件夹标签映射单独存 CSV。格式简单方便排查label_id,name,created_at 1,user_01,2025-01-01 2,user_02,2025-01-02读取时跳过表头做一次基础校验public static Dictionaryint, string LoadLabelMap(string path) { return File.ReadLines(path) .Skip(1) .Select(line line.Split(,)) .Where(parts parts.Length 2 int.TryParse(parts[0], out _)) .ToDictionary(parts int.Parse(parts[0]), parts parts[1]); }label_id必须和样本目录名一致name只用于显示不参与训练。这样后面改显示名不用重训模型。训练前我会统计每个类的样本数少于 10 张就打印警告var counts Directory.GetDirectories(trainDataDir) .ToDictionary( dir Path.GetFileName(dir), dir Directory.GetFiles(dir, *.png).Length); foreach (var item in counts.Where(x x.Value 10)) Console.WriteLine($样本不足: label{item.Key}, count{item.Value});样本不足时 LBPH 也能训练但识别会偏向样本多的类。类别不平衡比算法选择更常见后面避坑章节会专门讲。3.5 训练与识别LBPH 的 threshold 不是概率训练代码把每个标签目录下的 PNG 读成灰度图统一尺寸后塞进LBPHFaceRecognizer。识别时Predict返回标签和距离距离越小越像using OpenCvSharp; using OpenCvSharp.Face; public sealed class LbphFaceRecognizer : IDisposable { private readonly LBPHFaceRecognizer _recognizer; private readonly FacePreprocessor _preprocessor; private readonly Dictionaryint, string _labelMap; public LbphFaceRecognizer(string modelPath, Dictionaryint, string labelMap) { _recognizer LBPHFaceRecognizer.Create( radius: 1, neighbors: 8, gridX: 8, gridY: 8, threshold: 70); _recognizer.Read(modelPath); _preprocessor new FacePreprocessor(); _labelMap labelMap; } public FaceResult? Recognize(Mat frame, Rect box, double threshold) { using var face _preprocessor.Normalize(frame, box); _recognizer.Predict(face, out int label, out double distance); if (label 0 || distance threshold) return null; var name _labelMap.TryGetValue(label, out var n) ? n : $label_{label}; return new FaceResult(label, name, distance, box); } public void Dispose() _recognizer.Dispose(); } public record FaceResult(int LabelId, string Name, double Distance, Rect Box);LBPHFaceRecognizer.Create里radius、neighbors、gridX、gridY是 LBPH 的纹理参数默认值就能用不要一开始就调。threshold在 Create 里可以设也可以在识别时用外部阈值覆盖我习惯外部传方便做阈值扫描。Predict的distance不是概率70 只是经验起点换摄像头、换灯光都要重新扫。训练代码要把所有样本读进来再Train训练完Save到 yml。样本 Mat 用完要释放不然训练几千张图时内存会顶上去。模型加载用Read路径不存在时也会抛异常启动阶段就能发现。4. 避坑与常见问题排查模型加载失败、摄像头黑屏和误识别人脸识别项目翻车大多不在算法而在路径、后端、阈值和部署。下面 5 条是我实际踩过的按“现象 → 原因 → 解决”写每条都给可执行的检查动作。4.1 模型加载失败CascadeClassifier 的路径是相对工作目录现象new CascadeClassifier(models/haarcascade_frontalface_default.xml)不抛异常但DetectMultiScale一直返回空数组摄像头画面能看到人脸就是没有框。原因CascadeClassifier加载不到文件时不一定立刻抛异常Empty()才是黑匣子。相对路径按当前工作目录解析调试时工作目录可能是bin/Debug/net8.0不是解决方案根目录。解决启动时检查文件存在和Empty()打印绝对路径csproj 里把模型复制到输出目录。检查代码var cascadePath Path.GetFullPath(config.CascadePath); Console.WriteLine($Cascade: {cascadePath}); if (!File.Exists(cascadePath)) throw new FileNotFoundException(cascadePath); using var cascade new CascadeClassifier(cascadePath); if (cascade.Empty()) throw new InvalidOperationException(模型文件存在但加载失败检查文件是否完整);csproj 里用Content加CopyToOutputDirectory让模型跟程序走。发布时把models/一起拷过去启动脚本先cd到程序目录。这个坑不报大错只让检测结果为空最容易浪费一下午。4.2 摄像头打开成功但黑屏后端选择和释放顺序现象IsOpened()返回 true但Read()一直返回空或者画面全黑重启程序偶尔又能打开。原因Windows 上默认后端和某些 USB 摄像头兼容性一般换成 DSHOW 往往能解决另一个原因是上一次进程没释放摄像头设备被占着。VideoCapture和Mat的释放顺序不对也会让下一次打开失败。解决打开时指定 DSHOW设置分辨率后读一帧确认释放时先_frame.Dispose()再_capture.Dispose()。如果还是黑屏换一个 USB 口确认设备索引是 0 还是 1。Linux 上用 V4L2检查/dev/video*权限。不要在循环里反复 newVideoCapture打开和释放摄像头是重操作。4.3 误识别与阈值LBPH 的置信度是距离不是概率现象同一个人有时识别成别人阈值调到 80 误识多调到 50 又总是拒绝。原因LBPH 的distance是特征距离不是概率受光照、姿态、样本量影响很大。一个固定阈值不可能适配所有摄像头。解决固定摄像头和灯光采集多角度样本用测试集扫阈值记录误识和拒识识别时加多帧投票。投票代码可以这样写var votes new Dictionaryint, int(); for (int i 0; i 5; i) { var frame camera.Read(); if (frame is null) continue; var faces detector.Detect(frame); if (faces.Length 0) continue; var result recognizer.Recognize(frame, faces[0], threshold); if (result is null) continue; votes[result.LabelId] votes.GetValueOrDefault(result.LabelId) 1; } var best votes.OrderByDescending(x x.Value).FirstOrDefault();多帧投票能压住单帧抖动但会引入延迟。门禁场景可以接受 300 毫秒内的投票实时预览就不要每帧都投票。4.4 样本不足与类别不平衡每个类至少 10 张现象新加一个人后模型只认他旧人全错或者某个人总是被识别成样本最多的那个人。原因LBPH 对类别分布敏感样本少的类在特征空间里覆盖不足增量训练如果只拿新样本训练旧类会直接丢失。解决每个类至少 10 到 20 张覆盖正脸、轻微侧脸、不同光照训练前统计样本数加人后全量重训不要只训新样本。样本目录和labels.csv要同步更新训练前跑一遍统计少于 10 张就补采。这个检查不花时间但能省掉很多“玄学”误识别。4.5 部署到无界面环境DLL、x64 和 OpenCL现象开发机正常拷到工控机报DllNotFoundException或BadImageFormatException或者程序启动很慢日志里出现 OpenCL 初始化失败。原因native DLL 没复制到输出目录平台目标 AnyCPU 加载了错误位数工控机显卡驱动不支持 OpenCLOpenCV 初始化时反复尝试。解决csproj 固定x64发布时用dotnet publish -c Release -r win-x64 --self-contained false检查输出目录里有没有OpenCvSharpExtern.dll和opencv_videoio_ffmpeg*.dll。如果不需要 OpenCL启动时关掉Cv2.SetUseOpenCL(false);工控机上先把发布目录整个拷过去不要只拷 exe。缺少 DLL 的报错有时候只给一个文件名不给路径提前检查输出目录比到现场猜快得多。5. 二次开发改造把单机 Demo 拆成可维护的人脸识别服务单机 Demo 跑通后下一步通常不是换更复杂的模型而是把识别器抽成接口。摄像头采集、检测、识别各自有状态混在一个Program.cs里后面加日志、加缓存、换模型都会互相牵制。我一般先抽IFaceRecognizer让 App 只依赖接口public interface IFaceRecognizer { FaceResult? Recognize(Mat frame, Rect box, double threshold); void Reload(string modelPath); }注册时把识别器作为单例但内部要加锁因为LBPHFaceRecognizer不是线程安全的。摄像头采集单独开后台线程识别结果按人脸框做 300 毫秒缓存避免同一张脸每帧都跑一次训练距离。活体检测可以先做简单的多帧一致性连续 3 帧都识别到同一标签才放行不宣称安全级只挡照片翻拍的低级场景。验证不要只看“能识别”要准备一张表跑回归场景输入预期正常正脸测试集 50 张准确率不低于 90%遮挡口罩、手挡半边脸拒绝或低置信换摄像头新分辨率 640x480重新扫阈值后再上线并发4 线程同时识别不崩溃记录 QPS阈值扫描我一般从 50 到 90每 5 一步统计误识和拒识选一个业务能接受的平衡点。换摄像头后必须重扫不要直接复用旧阈值。从那以后我每次接人脸识别项目都先强制跑一遍三条命令打印模型绝对路径、抓一帧保存 PNG、用 50 张图扫阈值。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑