
数字体温计这玩意儿看着简单真做自动读数才发现处处是坑。去年我们团队接了个视觉需求在固定机位上拍一张体温计的屏幕系统自动识别出当前温度并写入记录全程不需要人去看。第一反应肯定是目标检测加OCR结果真正把方案跑通、把误读率压到能用的水平足足折腾了几周。这篇是这个系列的第二篇重点讲清楚检测和读数这条完整链路里哪些环节容易翻车、又是怎么修回来的适合正在做仪表读数、屏幕OCR、设备状态自动识别这类项目的朋友参考。1. 数字体温计读取任务里检测环节的真正难点1.1 目标小、特征杂为什么通用检测模型直接套用会崩体温计检测乍一听是个标准的目标检测任务画面里有体温计画个框输出类别完事。但实际跑一遍就会发现两个特别要命的问题。第一是目标尺寸太小。体温计的屏幕区域在整张1080P画面里通常只占几十乘一百多个像素数字区域更是只有几十像素宽。很多人习惯直接拿预训练权重去推理结果体温计本身倒是能框出来屏幕区域却经常漏检或者框偏。原因不复杂——预训练模型的分辨率基准是224或者640特征金字塔的浅层特征主要服务中等尺寸目标对这种几十像素的小区域默认配置下召回率根本不够看。第二是特征不够干净。体温计外壳是白色或浅色塑料和背景、手掌、衣物经常混在一起屏幕本身又是个小黑块稍微有点反光就变成亮块甚至把外壳的渐变当作屏幕边缘。再加上不同品牌的体温计屏幕区域比例、按键位置、字体粗细全都不一样同一个模型要把这些变体都扛下来光靠通用检测的默认策略是不够的。我们在项目里花了很大力气去调数据分布和标签策略后面会讲到。这里想强调的是不要一上来就追求模型多高级先把任务定义和标签划分想清楚收益大得多。1.2 类别的粒度划分检测体温计整体还是直接检测屏幕这是早期方案里争论最多的一个问题。团队里两种思路都有思路A只检测体温计这一个大类拿到整体框之后再用固定ROI感兴趣区域规则去切屏幕区域思路B直接检测屏幕区域这个类别体温计的整机位置通过屏幕框外扩推导出来。思路A的问题是固定ROI的假设太脆弱。摄像头机位、体温计握持角度、不同品牌的外形比例都会让屏幕在整机框内的相对位置漂移。一旦漂移超过设定阈值切出来的区域就不是屏幕后面的OCR全废。我们第一批测试集上固定ROI方法的屏幕定位成功率只有七成左右这个数字没法上线。思路B看起来更符合端到端的思路但它把小目标检测的难度直接拉满——屏幕区域既小又容易被反光干扰。后来我们还是采用了折中方案一个模型输出两个类别thermometer和display训练时两者独立标注推理时用display框作为OCR输入用thermometer框做区域校验和跟踪的去抖依据。如果display框的置信度太低就回退到thermometer框加显示区域回归分支。这个双类别加回退的策略让屏幕定位成功率从七成提到了九成五以上后面所有环节的稳定性都建立在这个基础上。2. YOLOv26带来的结构增益为什么这个项目需要新版模型2.1 小目标召回率提升背后的机制变化项目启动时正好赶上YOLOv26的官方仓库更新我们对比之后直接选了它而不是继续用上代版本。很多人觉得新版就是参数多点、精度高一点对这类小目标强相关的任务v26的收益其实体现在三个比较具体的点上。第一是主干网络里注意力模块的位置变了。老版本的注意力基本压在深层对小目标的浅层特征关注很弱v26在浅层也挂了轻量的通道注意力屏幕区域这种小但纹理明确的目标浅层语义增强之后召回率提升非常明显。第二是颈部特征融合的梯度通路做了简化多尺度特征在融合时信息损耗变小对边框回归精度有帮助我们测试集上屏幕框的IoU中位数比上代模型高了大概4个点。第三是训练策略里的动态分配机制它对正样本数量稀少的情况更友好——小目标在每张图里往往只有一两个正样本候选分配公式变化之后这些小目标更容易被学习到。上面这几点不是空谈我们做了一组对照实验来验证下面这个表格就是当时记录的一部分。2.2 实际对比老版本与v26在同一批数据上的表现我们在同一批标注数据大约8000张后面会细说上分别训练了上代nano模型和v26的s模型测试集固定为500张不同光照、角度、品牌的体温计画面指标如下模型输入分辨率display类mAP0.5display类mAP0.5:0.95单帧推理耗时(CPU)上代nano64086.254.728msv26 nano64089.158.927msv26 small64092.864.346msv26 small96094.668.278ms单看mAP提升幅度好像也就几个点但在下游OCR任务里这几个点直接决定了屏幕框切出来的画面到底干不干净。框稍微偏一点数字区域就带上了外壳边缘OCR错误率会成倍上升。所以最终线上我们用的是v26 small加960输入虽然推理耗时多了些但整条链路的稳定性才是这个项目的命门。这里也要提醒一句如果你的体温计在画面里占的比例本来就大用nano加640就够不必为了追精度牺牲速度。项目里有个兄弟场景是近距离拍摄我们试过四种组合最后用的反而是nano因为特征足够大模型容量没那么关键。3. 读数识别链路检测框之后的裁剪、增强与七段码解析3.1 屏幕区域的预处理三板斧检测模型输出display框之后不能直接把框里的图丢给OCR原始像素几乎必然有问题角度倾斜、曝光不均、对比度低。我们做了三步预处理每一步都是实测踩出来的。第一步是透视矫正。体温计屏幕本身是个矩形但握持姿势千奇百怪拍出来经常是平行四边形。我们拿到display框之后先用边缘检测找屏幕的四条边再做透视变换把屏幕拉正。如果你用的是固定机位、角度变化很小这步可以简化成固定仿射变换但我们的场景是人手拿着体温计角度随机性大透视矫正必不可少。第二步是自适应阈值二值化。LCD屏幕的背光不均匀是常态半边亮半边暗直接固定阈值分割数字笔画很容易断。我们用自适应阈值窗口大小设在21到31之间效果比全局阈值稳定得多。代码大概长这样def crop_display(frame, bbox): x1, y1, x2, y2 bbox # 外扩10%避免边缘上的数字被截断 h_pad int((x2 - x1) * 0.1) v_pad int((y2 - y1) * 0.1) region frame[max(0, y1-v_pad): y2v_pad, max(0, x1-h_pad): x2h_pad] return region def preprocess_display(region): gray cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 5 ) return binary第三步是形态学修补。二值化之后LCD数字常见的断笔、毛刺都还在。我们按先膨胀后腐蚀的顺序做了一次闭运算核选3x3能把细小的断点连上又不至于把相邻笔画粘在一起。这三步做完识别输入的质量基本就稳了。3.2 数字识别的两条路线通用OCR与七段码专用分类器预处理做完就该识别数字了。这里我们实际试过两条路线结论很明确体温计数字是七段码不是普通印刷体通用OCR并不占优势。第一条路线是通用OCR。我们试过几个开源框架在标准印刷体上确实猛但放到LCD七段码上就拉胯。原因在于七段码的笔画形态和印刷体差异太大尤其是数字1往往只有最右边两段通用OCR很容易把它和7混在一起。另外体温计的字体还有独特的小倾角和圆角普通训练数据里根本覆盖不到。第二条路线是七段码专用分类器。先利用七段码的结构特点把数字区域均匀切成七段上横、中横、下横、左上竖、右上竖、左下竖、右下竖对每一段判断亮还是不亮得到的7位0/1向量直接映射到0到9的数字。比如7段全亮是8只有上横加右上竖加右下竖是7。这种方法的鲁棒性极好只要你有办法把数字区域切准、把段亮不亮判断对识别准确率可以做到99%以上。段切分的难点在于数字宽度不一、间距不一。我们用一个轻量的语义分割网络先给每个数字做连通域定位再按固定模板切段。实际效果如下方案数字级准确率温度级准确率含小数点位置单次识别耗时通用OCR方案94.8%88.2%12ms七段码专用方案99.1%97.3%6ms七段码方案在速度和准确率上双赢。如果你读的不是体温计而是电表、水表、血糖仪这一类七段码设备思路完全可以复用。3.3 小数点和单位符号的处理细节数字读出来了小数点是个容易被忽视的大坑。体温计显示36.6和366差一个小数点含义完全不同。我们的做法是在二值化图上单独检测小数点区域。小数点通常位于个位和十分位之间的底部形状也是一个小方块用模板匹配加周边连通域分析就能定位。一旦检测到就在解析后的整数序列里从右往左插一位小数。注意有些体温计的温度显示在屏幕右侧还会带一个小一点的C字样要么它的位置固定且大小明显小于数字要么直接用区域规则排除掉不要让它混进数字检测里。4. 数据从哪里来真实样本采集与LCD数字仿真生成4.1 真实数据采集的注意事项训练数据是这类项目的地基。我们总共收集了大约8000张体温计图片其中真实拍摄样本接近3000张剩下的都是仿真生成。真实采集环节有几个心得尽量覆盖光照变化的极端。LCD显示在强光下会看不清在侧光下会反光变成一片亮白。我们专门挑了几类场景室内日光灯直射、窗边逆光、夜晚屏幕自发光、手电筒补光每种都拍一批。宁可多拍看似重复的极端光照也不要只拍光线均匀的理想图否则模型一到现场就瞎。握持姿势要多样。体温计被横握、竖握、斜45度握拍出来的屏幕形状完全不同。我们拍摄时故意让不同的人用不同姿势确保透视矫正的训练数据里有充分的形态差异。背景别太干净。实验室里白桌子上一放就拍模型学到的都是白色背景上的体温计真到了手里握着、背景是衣服纹理的时候就会懵。我们特意加了一批手持拍摄、桌面杂物背景、甚至隔着塑料袋拍的样本。4.2 仿真数据生成用渲染器批量造样本真实样本永远不够用尤其是不同品牌、不同数字组合的覆盖率。仿真生成是这类任务里性价比最高的做法。我们的思路很简单写一个脚本动态渲染LCD屏幕效果。七段码字体用开源的LCD字体库随机生成不同温度值、不同字宽、不同倾斜角再叠加噪声、模糊、亮度不均、塑料外壳高光的模拟。把渲染出来的屏幕随机拼到真实背景上贴图时加上仿射变换模拟角度最后再做一次整体光照扰动。这样一组下来几分钟就能生成上万张带精确标签的样本。仿真数据有个特别大的优势标注是免费的而且不会错。真实数据里人眼标注几十个像素宽的屏幕框容易偏仿真数据的框是渲染器直接输出的精度天生拉满。我们把仿真数据和真实数据按7比3混合训练最终测试集上的表现比纯真实数据训练高了约6个点mAP。4.3 标注时最容易犯的错误真实样本标注阶段最容易犯的错就是屏幕框不贴边。标注人员习惯把整个看起来像屏幕的区域都框进去包括周围一圈塑料边框。这会导致训练出来的display类别里混入大量非屏幕像素推理时框偏大OCR输入里全是干扰项。我们最后强行规定屏幕框必须贴着数字显示区域的可视边界宁可框小了也不要框大。重新标注一遍之后下游OCR的准确率肉眼可见地涨了一截。另外标签里最好单独区分被手指遮挡超过一半和清晰可见这两种情况。被遮挡严重的样本量不要太大但要留一批让模型学会在遮挡时输出低置信度而不是硬着头皮出一个错框。5. 帧级结果不可信连续帧融合与温度合理性校验5.1 为什么单帧识别结果不能直接用就算检测和OCR都做得不错单帧结果的稳定性依然不够看。我们之前跑过一个统计即便数字级准确率到了99%当画面出现轻微运动模糊或者一次反光闪过那一帧的读数可能就是错的。单帧直接上报偶尔冒出一个离谱温度在医疗相关场景里是不可接受的。解决方案是给系统加上时间维度的容错。思路很简单单帧结果只当作一次投票连续多帧的多数一致结果才被当作有效读数。这个思路在几乎所有仪表读数项目里都通用。5.2 滑窗投票与跟踪关联的具体实现我们用的是滑窗投票加简单的IoU跟踪。每一帧先跑检测和OCR得到一个候选温度值和置信度。把这个值与最近几帧的有效结果放在一个长度为5的窗口里取众数只有当众数的票数大于等于3时才更新最终输出。核心代码很简短from collections import Counter window [] # 最近5帧的有效结果 def update_output(candidate): if candidate[confidence] 0.7: return None window.append(candidate[temperature]) if len(window) 5: window.pop(0) if len(window) 5: counter Counter(window) temp, count counter.most_common(1)[0] if count 3: return temp return None这里要处理一个细节如果画面里的体温计位置在移动连续帧的框没有重叠窗口里的结果就不能直接混在一起投票。我们按屏幕框的IoU判断是不是同一个目标IoU小于阈值就清空窗口重新开始。否则一个镜头里先出现旧体温计、再出现新体温计两者温度不一样投票会一直失败。5.3 温度范围的硬约束与异常剔除模型的输出还要过一层领域常识约束。体温计的正常量程一般是32到43摄氏度当然不同设备会有差异。我们加了一个硬过滤器超过42度或者低于34度的结果直接标记异常不参与投票并触发重新识别状态。这层硬约束看起来简单实际价值极大。有一次现场测试屏幕反光让模型把37.0读成了37.9单帧结果连续三帧都是错的但由于浮动位置不同滑窗投票仍然输出了正确值。如果没有范围约束偶尔一次全窗口都被带偏的情况还是会发生。加上之后系统在数千次模拟测温中的异常读数基本被压到了零。6. 实测翻车场景与针对性修复6.1 反光和塑料外壳的镜面效应现场跑起来之后第一个大规模翻车的场景是反光。体温计外壳是圆弧形塑料稍微偏一个角度就会把顶灯的光斑映在屏幕上。光斑覆盖数字时二值化之后那一整块都是白色段判定的结果全错。我们针对这个问题的修复分三层数据层面仿真生成时加入了圆形高光斑和线性光斑的叠加让模型见过各种反光形态图像层面预处理时对高光区域做局部的直方图均衡把过曝压回来策略层面如果反光区域覆盖了超过三分之一的数字区域直接给这一帧打低置信度标签不参与投票。三层叠下来反光场景下的误读率从最初的两成降到了百分之一以下。6.2 运动中模糊与低帧率的影响另一个高频翻车是运动模糊。人手拿着体温计不可能纹丝不动微小的振动手机会让屏幕区域出现几像素的模糊。对检测来说几像素无所谓但对七段码的段判定来说模糊会导致笔画边缘粘连本来分得开的两段变成一段。我们试过在预处理里加去模糊效果不稳定最终选择了两个更务实的办法。一是推理时对连续几帧做小范围的对齐融合把多帧的屏幕区域按特征点配准后取平均等效于提高曝光时间但不会引入单帧的运动模糊二是调整投票窗口的容错让系统允许一两帧模糊结果存在只要最终窗口里的多数是清晰帧就行。整体效果是系统在轻微手抖条件下基本不受影响剧烈甩动时则果断进入低置信度状态。6.3 不同品牌字体的泛化问题最后是泛化问题。我们最初拿到的测试设备主要是两个品牌数字的字形接近模型在它们身上表现很好。重新买了另外三个品牌的体温计之后发现其中一家的字体笔画特别粗段间距明显小于训练分布段判定经常把相邻的段误连温度读数连续错。解决办法还是靠数据我们用仿真渲染器专门模拟了这家品牌的粗笔画字形并加入笔画粗细的正态随机扰动重新训练了一版模型。最终五个品牌都留一批验证样本模型在全部品牌上的数字级准确率都达到了98.5%以上。这个经历也说明做这类项目别偷懒多买几台不同品牌的设备回来拍比事后补数据省钱省时间。最后再分享一个实际用下来的小技巧上线之后一定要保留一个模型自检日志把每一帧的检测置信度、OCR置信度、投票结果都记录下来。这个日志平时没人看但一旦现场出现偶发的误读它就是定位问题最快的入口。我们后期有一次现场投诉读数偶尔跳一下就是靠翻日志定位到是特定角度的屏幕反光触发的针对性补了一组反光样本就解决了。这类系统的价值不在某个单点的模型精度而在整条链路的稳定性日志和兜底策略跟模型本身一样重要。