
前两年我在现场给一家工厂装过一套红外体温快速筛查设备硬件很简单一个32x24的红外温度阵列传感器、一个USB摄像头、一台工控机核心逻辑全在C#上位机里。后面所有跟C#、OpenCV、机器视觉相关的温度判定、人脸定位、数据记录、报警联动都是在这套东西上跑的。这篇文章我就把这个项目的完整链路拆开讲一遍从传感器选型、数据解析、热图生成到用Emgu CVC#下的OpenCV封装做人脸检测和测温区域提取再到温度补偿标定和上位机联动每一步怎么设计、为什么这么做、实际踩过什么坑统统写清楚。这套方案比较适合两类人看一类是做上位机开发、想往机器视觉方向扩展的C#程序员另一类是正在选型或设计红外测温相关系统的嵌入式/工控工程师。它解决的核心问题其实就一个怎么在低分辨率的红外温度图上准确定位目标人的额头区域并读到稳定可信的温度值。如果只是拿着传感器直接读数那误差能大到没法用这里面OpenCV要做的事情比想象中多得多。1. 项目整体设计与方案选型1.1 系统到底由哪几部分组成这个系统的物理组成一句话就是两个眼睛一个大脑。一个眼睛负责“看人长什么样”——就是普通的可见光摄像头输出给OpenCV做人脸检测另一个眼睛负责“看人有多热”——就是红外温度阵列传感器输出一组离散的温度矩阵。大脑则是那台工控机跑着C#写出来的上位机程序负责把所有数据拉进来、算清楚、显示出来并触发报警和记录。数据流大概是这么走的红外传感器通过I2C或串口把32x24个像素点的温度值传给下位机STM32或直接转USB串口上位机通过SerialPort读取原始字节并解析成float温度矩阵。可见光摄像头通过DirectShow或者USB摄像头SDK把一帧一帧的RGB图像送到上位机OpenCV在这张图上做Haar人脸检测得到人脸框坐标。上位机把人脸框中心从可见光图坐标映射到32x24的红外温度矩阵坐标在该坐标附近取额头ROI提取最高温作为体温参考值。经过温度补偿和滤波后把结果推到界面上超过阈值就触发声光报警、写日志、记录CSV。这个架构最大的好处是每个环节都可以独立替换。今天用MLX90640明天想换更高分辨率的传感器只需要改解析那一段今天用Haar级联检测人脸后天想换成深度学习模型也只需要改检测模块的输出接口。系统的耦合被压到了最低这也是后来迭代维护时最省心的地方。1.2 红外传感器选型8x8和32x24到底差多少市面上常见的红外阵列传感器有两档8x8的AMG8833和32x24的MLX90640两者价格差了好几倍但测温项目的效果差距非常明显。8x8意味着整个视场只有64个温度点别说多人同时检测了就算单人站在画面中央额头区域可能就覆盖了四五个像素传感器离人一远额头和环境的像素混在一起读出来的根本不是你想要的额头温度。我用一个简单例子说明这个问题MLX90640的水平视场角是55度如果人站在2米外整个画面覆盖的宽度大约是2 * tan(27.5°) * 2 ≈ 2米左右32个像素横向排列平均每个像素覆盖大约6到7厘米的物理宽度。人的额头宽度一般在12到15厘米大约能占到2到3个像素这个分辨率下取温才有意义。如果换成8x8每个像素覆盖接近25到30厘米额头和头发、背景完全混在一起温度值基本就是废物。所以我的建议是别省这个钱测温项目起步就是32x24。传感器型号上MLX90640有两个FOV版本55°x35°和110°x75°我推荐55°版本。大FOV看着能覆盖更多人但单个像素对应的物理空间更大测温精度反而更差。现场部署时把传感器架在离被测人0.8到1.2米的高度和距离固定一个位置让人从镜头前经过效果最稳。1.3 为什么必须用OpenCV而不是固定框选区域早期方案我也偷懒过不搞人脸检测直接把红外画面的中心区域当成测温区人只要走到中间就算测到温度。结果就是误报率特别高一块金属板、一杯热水、甚至太阳光反射都能触发报警。因为你不确认画面里站着的到底是不是一个人、额头在哪个位置单靠温度阈值做判断一定会翻车。OpenCV在这里解决的就是“目标确认”和“目标定位”两个问题。通过Cascaded Haar分类器在可见光图上框出人脸然后把这个框映射到红外温度图上明确告诉温度提取逻辑测温点应该在这个位置而不是画面中央。这样一来就算人站在画面偏左或偏右的位置系统依然能准确找到额头区域。从产品角度看这已经把测温逻辑从“盲人摸象”变成了“拿着地图找路”可靠度完全不是一个量级。1.4 C#上位机在整个链路中的定位很多人问为什么非要用C#Python写OpenCV不是更顺吗确实Python在算法调试阶段有优势但到了真正的工控现场C#的优势就出来了窗体应用开发效率高、串口和网络通信库齐全、WinForm/WPF做实时监控界面非常顺手而且部署到工控机上几乎零依赖不像Python还得装解释器、配一堆包。Emgu CV作为OpenCV的官方.NET封装API跟OpenCV基本都是对位的C#调用起来不算别扭。我实际开发中用到的C#技能点其实非常集中SerialPort串口通信、多线程和UI跨线程更新、Bitmap与Mat的图像转换、简单的队列做数据缓冲。如果这些基础你都已经掌握这个项目上手会非常快。还没掌握也没关系下面每一步我都会写清楚实现方法和踩坑点。2. 温度数据采集与热图像生成2.1 从串口字节流到温度矩阵的解析过程MLX90640这类传感器本身输出的是I2C信号直接接电脑不太现实一般通过一个STM32或者USB转I2C模块转成串口或USB协议再往上位机送。我这次用的是“传感器定制转接板USB串口”的组合上位机通过COM口收数据。不同厂家的模块帧格式不一样所以第一步一定先拿到你手里模块的协议说明搞清楚这几件事一帧数据包含几个字节、包头和CRC怎么校验温度值是int16还是uint16放大倍数是多少常见的是直接除以100得到摄氏度数据是按行扫描还是按列扫描32x24矩阵的行列顺序是哪个方向我这边模块的协议是每帧1538字节左右去掉帧头帧尾后32x24个像素点按行优先排列每个温度点用2字节小端序表示放大100倍。解析核心代码大概是这个思路private float[,] _temperature new float[24, 32]; // buffer 是已经去掉帧头的一帧数据offset 是温度数据起始位置 private void ParseTemperatureFrame(byte[] buffer, int offset) { for (int row 0; row 24; row) { for (int col 0; col 32; col) { int idx offset (row * 32 col) * 2; // 小端序低字节在前高字节在后 short raw (short)(buffer[idx] | (buffer[idx 1] 8)); float temp raw / 100.0f; _temperature[row, col] temp; } } }这里有个细节容易踩坑int16在C#里是有符号数如果模块把负温补码当无符号数输出直接用short转就会出现负数比如一帧温度本来是-20.5°C按无符号读出来会变成一个大正数。解决方法是先读成ushort再强制转shortushort uRaw (ushort)(buffer[idx] | (buffer[idx 1] 8)); short raw (short)uRaw;解析完的温度矩阵是一块24行32列的float数据这一版先别急着画图直接在界面上写一个表格控件把数值刷出来确认数据在动、温度数值合理再进入下一步。我见过太多人跳过这一步直接去搞图像显示结果后面所有逻辑都在错误的温度数据上跑白折腾。2.2 把32x24温度矩阵变成可视热图的完整步骤拿到float矩阵以后要把它变成人能看懂的伪彩色图。为什么是伪彩色而不是直接显示灰度图因为人眼对灰度梯度的分辨能力非常有限但对红蓝黄这些颜色的差别非常敏感在热图上我们通常用蓝色表示低温、红白色表示高温这样额头区域一眼就能扫出来。步骤分三步我贴一下核心代码第一步把float矩阵转成OpenCV的Mat这一步要用Marshal.Copy把托管数组拷贝到Mat的DataPointer原理是把托管内存的float数据直接铺到Mat内部不需要逐像素Setfloat[] flat new float[24 * 32]; for (int r 0; r 24; r) for (int c 0; c 32; c) flat[r * 32 c] _temperature[r, c]; Mat tempMat new Mat(24, 32, DepthType.Cv32F, 1); Marshal.Copy(flat, 0, tempMat.DataPointer, flat.Length);第二步把32x32浮点数据归一化到0到255的范围并转成8位灰度图。归一化用NormType.MinMax意思是把矩阵里最小温度映射到0、最大温度映射到255。这里有个小问题如果一帧里某个坏像素出现50°C的尖峰整个画面的灰度会被拉偏所以建议在归一化之前先做一次温度范围的截断把低于20°C和高于45°C的像素排除让颜色映射集中在人体温度区间CvInvoke.Threshold(tempMat, tempMat, 45.0, 45.0, ThresholdType.Truncate); CvInvoke.Threshold(tempMat, tempMat, 20.0, 20.0, ThresholdType.Tozero);第三步把归一化后的灰度图放大到适合观看的分辨率并套用Jet色标Mat gray new Mat(); CvInvoke.Normalize(tempMat, gray, 0, 255, NormType.MinMax, DepthType.Cv8U); Mat resized new Mat(); CvInvoke.Resize(gray, resized, new Size(640, 480), 0, 0, Inter.Linear); Mat color new Mat(); CvInvoke.ApplyColorMap(resized, color, ColorMapType.Jet);这里Resize的插值方式我选的是Inter.Linear因为红外原始分辨率太低用Inter.Cubic反而会把像素边缘的锯齿放大产生假的细节容易误导判断。线性插值带来的平滑效果更适合温度分布这种缓变数据。ColorMapType.Jet就是经典的“蓝→青→绿→黄→红”渐变属于OpenCV内置色标直接调用即可。如果你觉得Jet在低温区和高温区颜色太接近不好区分也可以用ColorMapType.Inferno或ColorMapType.Turbo这个完全是审美问题不影响取温逻辑。2.3 原始热图到底哪里不够用如果你把上面生成的640x480热图直接当界面主画面会很快发现一个问题热图上根本分不清哪里是额头、哪里是衣服、哪里是背景。因为32x24的原始分辨率放大到640x480以后整个画面都是模糊的色块人眼只能大概看出一个“人形轮廓”完全没法精确定位到额头。这就是为什么系统必须要有一套“可见光图像人脸检测坐标映射”的机制而不是光靠热图人工找位置。可见光图像负责提供结构信息告诉系统“人脸在哪里”热图负责提供温度信息告诉系统“这个位置多少度”。两者叠加起来才是完整可用的测温界面。实际界面上我通常是把可见光图作为主背景在上面叠加一个半透明的热图区域再把检测到的人脸框和当前最高温度标出来。这样操作员一眼就能看到画面里每个人的温度而不是盯着一张模糊的热图猜。3. 人脸定位与额头取温区域提取3.1 人脸检测为什么放在可见光图上做这个决定看起来简单其实背后是有原因的。红外图像分辨率只有32x24人脸特征在这么多像素里几乎不可辨认不要说Haar分类器就算用深度学习模型也一样困难。与其在低分辨率图上挣扎不如让可见光摄像头做擅长的事情在1280x720的高清图里用OpenCV自带的Haar级联分类器就能非常稳定地检测到正脸。C#里用Emgu CV做人脸检测相当直接加载一个训练好的xml文件就行CascadeClassifier faceCascade new CascadeClassifier(haarcascade_frontalface_default.xml); // grayFrame 是从摄像头拿到的灰度图 Rectangle[] faces faceCascade.DetectMultiScale( grayFrame, scaleFactor: 1.1, minNeighbors: 5, minSize: new Size(80, 80), maxSize: Size.Empty); foreach (Rectangle face in faces) { // 每个 face 就是可见光图上的人脸框 }scaleFactor1.1表示每层缩放窗口大小缩小10%这个值越小检测越慢但召回率越高minNeighbors5表示一个候选区域至少被周边5个窗口确认才算人脸用来消掉误检。实际上我们把它固定在这里因为场景是固定机位的出入口通道人脸大小变化范围有限不需要太大搜索空间。注意Haar模型对人脸角度比较敏感正脸效果最好侧脸容易丢所以现场我会贴一张提示让人通过时面向设备不用正对也没关系只要不是完全侧脸就能检测到。3.2 可见光坐标到红外坐标的映射计算人脸检测框是在可见光图上得到的是一个矩形区域坐标单位是像素。但要取温必须先把这个矩形映射到32x24的红外温度矩阵上。映射的原理是分辨率比例换算公式很简单// 可见光图像宽高 int visW 1280, visH 720; // 红外矩阵行列数 int irW 32, irH 24; // 人脸框中心在可见光图中的像素坐标 int faceCenterX face.X face.Width / 2; int faceCenterY face.Y face.Height / 2; // 按分辨率比例映射到红外坐标 int irX (int)((double)faceCenterX * irW / visW); int irY (int)((double)faceCenterY * irH / visH);但实测下来直接用这个比例映射会出现偏差因为可见光摄像头和红外传感器的视场角不一定一样安装位置也有物理错位导致同一个点在两幅图里的位置并不严格按同一个比例缩放。解决办法是做一个“单点标定”让一个人站在测温位置额头贴一张小反光贴或用手电筒照一下在可见光图上记下标记点的坐标同时在红外热图上找到对应的高温点坐标算出两组坐标的偏移量把这个偏移量存进配置项里irX offsetX; // offsetX 就是标定出来的偏移 irY offsetY;这个Offset标定最好在设备安装完成后做一次之后只要摄像头和红外传感器没有被碰歪偏差基本能保持在1到2个像素以内对取温来说完全够用。3.3 额头ROI怎么取才能避开眼睛和背景很多第一次写测温逻辑的人直接拿整个人脸框在红外图上取最高温结果发现读到的温度比真实体温高好几度。原因很简单人的头发、衣服、眼镜框都可能比额头温度高在低分辨率红外图上人脸框里混入了太多非皮肤区域最高温很容易被这些干扰源抢走。正确做法是把测温区域收窄到额头的核心区域。以人脸框为基础我做的是这样一个策略在映射到红外坐标后不是取整个人脸框而是以人脸中心为锚点框出一个更小的矩形覆盖额头位置。经验值是这样算的水平方向从人脸框左边界往右35%到右边界往左35%取中间30%的宽度垂直方向从人脸框顶部往下10%开始到顶部往下40%结束这段基本就是额头和发际线下方区域对应到代码里就是// 先算出人脸框映射到红外图上的矩形 Rectangle irFace new Rectangle( (int)((double)face.X * irW / visW) offsetX, (int)((double)face.Y * irH / visH) offsetY, Math.Max(1, (int)((double)face.Width * irW / visW)), Math.Max(1, (int)((double)face.Height * irH / visH))); // 取额头区域中间30%宽度顶部10%-40%高度 int cropX irFace.X (int)(irFace.Width * 0.35); int cropY irFace.Y (int)(irFace.Height * 0.10); int cropW Math.Max(1, (int)(irFace.Width * 0.30)); int cropH Math.Max(1, (int)(irFace.Height * 0.30));这个比例不需要太精确因为32x24的分辨率下额头总共也就覆盖两三个像素框太大反而容易混进干扰框太小又可能偏移漏掉真正的额头点。宁可稍微偏大一点然后用下面要讲的取温策略去兜底。3.4 取温策略最高温、平均温与坏点剔除ROI框出来以后怎么从这个区域里读一个温度值出来最朴素的方案是取平均温度但平均温度有个问题只要ROI里混进了一个背景像素点平均值就会被拉低导致测出来的体温偏低。我最终采用的是“区域最高温为主、剔除孤立坏点为辅”的策略这也是很多红外测温枪的实际原理——非接触式红外测温本身就是取传感器视场内的最高辐射温度。但是红外阵列数据里经常会出现一种“坏点”表现为某个单独像素的温度比周围高十几度这是传感器本身或电路干扰引起的。直接取最高温会被坏点带偏所以要先做一步孤立点检测当前像素温度比周围8个像素的平均值高出一定阈值比如5°C时认为它是坏点忽略它继续取剩下像素里的最高值。实现逻辑大概是private float ExtractMaxTemperature(float[,] data, int cropX, int cropY, int cropW, int cropH) { float maxTemp float.MinValue; for (int r cropY; r Math.Min(24, cropY cropH); r) { for (int c cropX; c Math.Min(32, cropX cropW); c) { if (IsIsolatedHotPixel(data, r, c, 5.0f)) continue; if (data[r, c] maxTemp) maxTemp data[r, c]; } } return maxTemp; }IsIsolatedHotPixel大致是取当前像素周围8邻域的平均温度如果当前温度减平均温度超过5度就判为坏点。这个阈值在低温环境下要适当调低否则会把真实高温点误判成坏点。我后来还加了一个限制坏点连续出现的数量如果超过ROI总像素的1/3说明不是坏点而是真的存在一个高温暖源这时候宁可把整帧数据标为“疑似异常”让上位机提示操作员人工确认也不要让系统贸然报警。4. 温度校正、判定逻辑与上位机联动4.1 实测温度不准的三个元凶传感器读出来的原始温度数据直接当成体温上报误差会大到让人怀疑人生。最常见的三个问题发射率、测量距离和环境温度。发射率指的是物体表面辐射红外线的能力。人体皮肤的发射率大约在0.95到0.98之间而MLX90640在出厂时通常把发射率默认设成1.0。如果你没有在传感器配置里把发射率改成0.95到0.98读出来的温度会明显偏低因为皮肤反射回来的环境辐射被当成物体自身辐射计算了。测量距离的影响来自空气对红外辐射的吸收衰减。距离越远红外能量损失越多读到的温度也越低。这个不是线性的跟空气中的水汽含量都有关系。所以我前面强调传感器要尽可能离被测人近一点固定安装距离目的就是让这个系统误差变成一个常数后续标定可以一次性补偿掉。环境温度的影响最隐蔽。传感器内部的Ta环境温度探头测的是环境温度温度输出本身会做一定程度的环境补偿但现场空调风、阳光直射、设备旁边电路发热都会干扰Ta的读数。这就是为什么部署时传感器要放在通风、不被直射、远离大功率发热设备的位置。4.2 现场标定到底怎么做标定是整个系统里最核心、也最容易被跳过的一步。我的做法很简单找一个温度稳定的热源黑体炉最好没有的话用医用电子体温计加温水袋也能凑合设定几个不同的温度点分别记录传感器原始读数和标准温度计的真值然后拟合一条校正曲线。最常用的是一阶线性校正公式是T_cal a * T_raw b其中a是比例系数b是偏移量。两个标定点就够解出a和b// 标定点1传感器读数 r1真值 t1 // 标定点2传感器读数 r2真值 t2 float a (t2 - t1) / (r2 - r1); float b t1 - a * r1; float calibratedTemp a * rawTemp b;如果现场有条件我建议标三个点然后做二项式拟合效果会更好因为红外传感器在低温段和高温段的响应非线性还是比较明显的特别是在30°C到40°C这个人体测温区段最好让标定点密集一些。标定的时候有一个细节标定源的位置要尽量靠近实际被测人的位置距离和角度都要一致。如果你在1米处标定的曲线拿到2米处去用那个距离带来的衰减误差会重新冒出来。4.3 报警判定不能只看单帧数据温度值跳变在红外阵列传感器上是常态可能上一帧37.2°C下一帧就跳到38.6°C。如果单帧超阈值就报警系统会变成“惊弓之鸟”天天误报。我的做法是连续N帧超过阈值才触发报警低于阈值的帧数达到一定数量就自动复位。代码如下private int _overTempCount 0; private const int OVER_TEMP_FRAME_THRESHOLD 3; private void CheckAlarm(float temp, float threshold) { if (temp threshold) { _overTempCount; if (_overTempCount OVER_TEMP_FRAME_THRESHOLD) { TriggerAlarm(); // 触发声光报警、写日志、联动门禁 _overTempCount 0; // 触发后复位防止同一人连续触发多次 } } else { _overTempCount 0; } }连续3帧这个值是实际调出来的经验值。帧率如果跑在5到10帧左右3帧相当于0.3到0.6秒既不会漏报正常通过的人也能有效滤掉单帧跳变。报警联动我用了两个通道一个是直接在界面上弹红框和声音报警给现场操作员看另一个是通过Modbus TCP协议往PLC或继电器模块发指令控制三色警灯和道闸。C#里用NModbus4库做Modbus客户端非常成熟几行代码就能把保持寄存器写进去这里不多展开。4.4 上位机界面与数据记录界面这一块我最后做出来大概是这样一个布局左侧是可见光视频窗口上面叠加半透明热图和温度标签右侧是实时温度曲线用折线图把当前帧的最高温画出来方便观察温度变化趋势顶部是状态栏显示当前环境温度、系统运行状态、阈值配置项。温度曲线在调试阶段帮了大忙标定的时候你一眼就能看出传感器原始温度和标定温度的偏差趋势比盯着数字猜快得多。数据记录方面每天按日期生成一个CSV文件记录这条记录的时间、最大温度、是否报警、人脸框坐标方便事后追溯string logPath $logs/{DateTime.Now:yyyyMMdd}.csv; string line ${DateTime.Now:yyyy-MM-dd HH:mm:ss},{temp:F2},{(isAlarm ? 1 : 0)}; File.AppendAllText(logPath, line Environment.NewLine);其实CSV已经够用了除非你有复杂的查询需求再考虑SQLite否则不要为一个小功能引一个大依赖。5. 实战中的常见问题与避坑技巧5.1 Haar检测在逆光和暗光环境下失灵最开始在工厂车间测试白天窗户方向来的强光把整个画面打成逆光人脸区域几乎是黑的Haar检测器直接漏检。这个不是算法问题是图像质量太差。解决办法有两个一是给摄像头加一个带背光补偿的模式在DirectShow属性里把Backlight Compensation打开二是从硬件侧加一盏补光灯让现场面部照度稳定在300到500勒克斯左右。我最后选了补光灯效果最直接检测率从60%提到了95%以上。5.2 可见光和红外两个画面的视场总是对不齐不管你怎么安装两台设备的视场几乎不可能做到完全重合这就会导致人脸框映射到红外图上后取温点偏到额头旁边。解决办法就是我前面提到的“单点标定Offset”但要注意这个偏移量在画面不同位置并非常数畸变会导致边缘偏差变大。所以我建议被测人尽量保持在画面中央区域活动并把这个提示贴在现场画面边缘的数据即使读到上报也不作为最终结果。5.3 温度老是跳来跳去温度跳变的来源很多最典型的是传感器供电不稳。MLX90640内部有一个控温加热器如果供电电流不够整个温度数据都会出现周期性的漂移。我遇到过一帧数据整体比上一帧高1.2°C的诡异问题排查到最后是USB串口模块和传感器共用一个劣质5V电源导致的。最后换了单独一路稳压电源跳变立刻消失。如果你的数据也出现整体漂移先怀疑供电再去调滤波。5.4 上位机界面卡顿界面卡顿几乎都是因为把耗时操作放在了UI线程里。摄像头取流、人脸检测、温度矩阵绘制任何一个落到UI线程都会卡成PPT。C#里正确做法是主线程只负责界面刷新取流和检测放到后台线程处理完一帧后用BeginInvoke把结果交给UI线程展示。如果你用的是WinForm记得在Form_Load里开一个BackgroundWorker或者Task.Run循环而不是直接在Timer事件里做重活。顺便说一个很多人忽略的点如果你每帧都在主线程创建新的Bitmap并加载到PictureBox垃圾回收会让界面周期性卡顿。更好的做法是预先创建好Bitmap拿到新帧后用Graphics.DrawImage覆盖绘制或者直接操作Bitmap的像素内存避免每帧都new大对象。5.5 常见问题速查表问题现象可能原因解决办法人脸框经常消失逆光、暗光、侧脸加补光、调整摄像头位置、降低Haar检测minNeighbors温度持续偏高0.5-1°C发射率设置偏大检查传感器发射率配置设成0.95-0.98温度整体漂移供电不足/电源劣质换独立稳压电源检查电源纹波偶发单帧超高温度坏点或电路干扰增加孤立坏点剔除逻辑做中值滤波红外图和可见光画面错位安装偏差/视场不一致做单点标定Offset让被测人处于画面中央远距离测温偏低空气吸收衰减固定安装距离按实际距离重新标定UI操作卡顿耗时操作放在UI线程改为后台线程处理数据UI只做展示最后分享一点个人的体会这个项目做下来我最深的感受是一套测温系统真正的技术难点不在于把温度数据读出来也不在于把界面做得好看而在于“把多个环节的误差一点点抠掉”。传感器本身有误差安装位置引入误差环境温度影响误差距离带来误差标定不精确又产生新误差。每一个误差单独看都不大但叠在一起温度数据就会完全失真。所以我的建议是如果你要复现这套方案务必给自己留出充足的调试和标定时间。硬件买回来三天就可以把代码跑通但要把温度测准、让误报率降到可接受范围至少需要一周到两周的现场磨合。先拿固定热源反复验证再上真人测试最后才交给现场长期运行。如果一开始就图快直接部署后面返工的代价会翻好几倍。如果你想在这个项目基础上继续扩展两个方向我觉得很值得试一是把Haar人脸检测换成ONNX的深度学习检测模型侧面、遮挡、口罩场景下鲁棒性会好很多二是把单点测温扩展成多点同时检测结合跟踪算法实现多人在画面中并排行进时的连续测温那基本就是一套完整的人体测温筛查系统了。希望这篇拆解能让你少走些弯路。