1. 疼痛检测这件事为什么值得用YOLO来做先说结论疼痛是临床里最难量化的指标之一而计算机视觉恰好能提供一个相对客观、可持续监测的观察维度。我在做医疗健康相关项目时经常遇到一个尴尬场景——护士需要定时评估患者的疼痛程度传统做法是让患者自己打分NRS数字评分法或者医护人员根据行为量表如FLACC量表手工判断。但问题很明显ICU里插管镇静的患者不会说话婴幼儿无法配合表达术后苏醒期的病人神志不清老年痴呆患者更是难以自述。这种时候观察者的经验和主观倾向直接影响评估结果不同护士打分可能差异很大。于是就有了一个很自然的技术诉求能不能让摄像头自动、持续、一致地捕捉疼痛的视觉线索把评估结果作为辅助参考这正是这个2200张YOLO医疗健康数据集的出发点。它的核心不是去做诊断而是做“疼痛相关行为线索的目标检测”——把疼痛表情区域、手部紧握动作、肢体屈曲姿势等有代表性的视觉特征用检测框标出来再用YOLO系列模型训练出一个可以实时推理的检测器。为什么选YOLO而不是随便用一个图像分类模型我在实际项目里对比过几个方案差异非常明显。分类模型如ResNet只能告诉整张图里“有没有疼痛表现”一旦画面里有患者同时又有护士、家属、监护仪模型就会被无关区域干扰而且你永远不知道它到底看到了哪里。目标检测模型能精确定位到“哪个区域出现了疼痛相关线索”比如眉间紧缩、面颊抬高、手握紧这些空间位置信息对后续的疼痛评估算法非常关键。YOLO系模型在医疗边缘设备上部署友好一个YOLOv8s模型量化后只有不到20MB在Jetson或RK3588这类设备上跑实时推理毫无压力这一点对病房场景极其重要。这里还要多说一句疼痛检测这条路线并非凭空创造它有很扎实的医学基础。新生儿疼痛领域有一个著名量表叫NIPS新生儿疼痛评分里面明确把“面部表情”“哭闹”“腿部活动”等行为作为评分维度。成人常用的FLACC量表也包含“表情”“腿部动作”“身体活动”“哭闹”“可安慰性”五个维度。所以从医学逻辑上讲疼痛确实存在可以被视觉识别到的外部表现。这个数据集的本质就是把医学量表里描述性的行为线索转化为目标检测任务中的标注框。对于想入门医疗AI或者想做行为识别相关项目的人来说这个数据集的价值在于它不要求你具备多深的临床知识只需要理解“疼痛行为线索”的定义就能训练出一个有实际应用场景的检测模型。门槛不高但延伸空间很大。2. 2200张图像的数据构成与采集思路2.1 数据源分析与取舍我整理数据集时没有只依赖单一来源而是做了公开数据和补充采集的混合搭配。公开部分主要参考了两个方向的研究成果一个是新生儿疼痛面部数据库另一个是多模态婴儿疼痛数据集MiFI。这类数据库在学术圈是公开可用的里面包含了大量标注好的疼痛/非疼痛表情帧对面部相关部分帮助非常大。但公开数据有一个可见的问题场景过于单一。学术数据库大多在固定光照、固定摄像头角度下拍摄和真实病房环境差距不小。所以我补充了约30%的自采数据——当然不是真让人痛而是模拟疼痛表情。具体做法是邀请志愿者按标准化的面部动作指令类似FACS动作单元的引导方式做出“眉间紧缩”“眼睑紧闭”“鼻唇沟加深”“张嘴嘴角拉伸”等动作同时用手部握紧、腿部屈曲作为肢体线索补充。这里我特别想提醒一句做医疗AI数据集千万别碰真实患者的隐私影像除非你有完整的伦理审批和授权流程。用标准化的模拟动作采集数据既安全又能覆盖主要视觉特征是一个稳妥可行的方法。我这次就这样做的整个过程拍下来大概两周。2.2 类别分布与图像规格整个数据集共2200张图像。我最初的标注方案包含6个类别但在清洗过程中合并了一些出现频次过低、模型根本学不出来的类别最终锁定为以下4个类别类别标签对应行为线索样本框数量原始依据brow_lower眉间紧缩、眉毛下垂约3400FACS动作单元AU4eye_close眼睑紧闭、挤眼动作约2900FACS动作单元AU6/AU7nasolabial鼻唇沟加深、面颊上提约1800上唇提肌相关动作hand_clench手部紧握、抓握动作约1500FLACC/NIPS量表肢体条目有人可能会问为什么不做“pain / no_pain”二分类框这正是我在初版踩过的坑。疼痛表情是渐变的不同人对疼痛的耐受度和表现差异极大直接贴“疼痛”标签会引入大量标注者主观偏差。改为检测具体的FACS动作单元之后标注一致性显著提升模型也更通用——未来无论接什么量表只需要把检测结果映射到对应评分规则里就行不用重新训练。图像规格方面公开源帧多为接近720p分辨率的视频帧自采部分统一以1080p拍摄在送入模型前按短边缩放到640到1280不等。数据集实例标注采用常见的COCO格式存储同时导出了YOLO所需的txt标签文件方便直接用YOLOv8、YOLO11系列训练。2.3 标注一致性是如何保证的标注环节是整个数据集最耗时也最容易翻车的部分。我有两点体会比较深标注规范必须具体到“什么算、什么不算”。光写“眉间紧缩”四个字肯定不够标注员会问轻微皱眉算不算有抬头纹算不算戴眼镜挡住眉间怎么办我最终写了约两页的标注细则配上正例和反例截图把边界卡死才把多人标注的一致性问题压下来。用双人标注第三人仲裁的方式处理全部数据。每个框至少经过两个人确认不一致的样本单独拎出来复审。实测下来大约12%的框在第一轮存在争议主要集中在nasolabial这个类别上——因为鼻唇沟在成年人和婴儿面部的显著性差异很大。经过仲裁和尺度统一后最终标注质量才稳定下来。3. 从标注框到YOLO训练格式转换与数据划分3.1 COCO格式与YOLO格式的转换拿到COCO格式的标注文件之后第一件事就是转换成YOLO训练用的txt标签格式。这个流程我基本每次都得写一遍脚本干脆这次直接贴出来方便需要的人直接参考。import json import os def coco_to_yolo(coco_json_path, output_dir, img_dir): with open(coco_json_path, r, encodingutf-8) as f: coco_data json.load(f) # 建立 image_id 到 文件名 的映射 id2name {} for img in coco_data[images]: id2name[img[id]] img[file_name] # 建立 category_id 到 类别索引 的映射 cat_id_map {} for idx, cat in enumerate(coco_data[categories]): cat_id_map[cat[id]] idx # 把每个标注转换到对应图片的 txt 文件 annotations_by_img {} for ann in coco_data[annotations]: img_id ann[image_id] annotations_by_img.setdefault(img_id, []).append(ann) os.makedirs(output_dir, exist_okTrue) for img_id, anns in annotations_by_img.items(): img_name id2name[img_id] img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): continue from PIL import Image w, h Image.open(img_path).size txt_name os.path.splitext(img_name)[0] .txt out_txt os.path.join(output_dir, txt_name) lines [] for ann in anns: cat_idx cat_id_map[ann[category_id]] bbox ann[bbox] # x, y, w, h (COCO 格式) x, y, bw, bh bbox # 归一化到 0~1 cx (x bw / 2) / w cy (y bh / 2) / h bw_n bw / w bh_n bh / h # YOLO 格式: class cx cy w h lines.append(f{cat_idx} {cx:.6f} {cy:.6f} {bw_n:.6f} {bh_n:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: coco_to_yolo( coco_json_pathannotations/instances_train.json, output_dirlabels/train, img_dirimages/train )这段脚本不长但有一个细节值得注意我在读取图片尺寸时直接用了PIL打开图片获取宽高而不是从COCO的width和height字段读取。原因是我遇到过标注文件中图片元信息与实际图片尺寸不一致的情况如果按错误的尺寸归一化所有标注框都会偏移模型训练损失曲线会一直降不下来排查起来非常痛苦。3.2 数据划分不要随手random一下数据划分看起来简单实际有讲究。医学序列数据集最大的坑在于同一段视频里前后几帧高度相似如果随机划分训练集和验证集里很可能出现来自同一原始视频的帧导致验证分数虚高模型真实泛化能力被严重高估。我的做法是以“原始视频片段”为分组单位划分而不是以图片为单位。先把图像按来源视频分组再保证整个视频组要么进训练集要么进验证集。最终按约8:1:1拆成训练集、验证集、测试集其中测试集完全不参与任何调参过程只在最后做一次评估。在划分的时候我还特意检查了每个类别的框数量在三个集合中的比例是否接近原始分布避免某个小类别在验证集里直接消失。3.3 数据增强策略医疗场景数据少增强就显得格外重要。我用的增强组合包括随机水平翻转pain表情虽然有左右对称性但部分自采数据中面部侧转角度不均匀翻转能平衡这种偏差HSV色域扰动病房光照色温差异大适当扰动可以提升模型的照明鲁棒性随机尺度缩放与平移马赛克增强这个放在训练前中期用实验发现它在医疗目标检测上仍然有效但不宜全程使用后期反而会导致小目标收敛不稳小角度随机旋转面部在真实画面中不一定完全正对镜头5到15度的旋转增强很有帮助有一项增强我做了减法强透视变换。医疗场景下摄像头位置比较固定强透视会导致脸部结构失真反而伤害模型对真实场景的适配所以只在特殊需求时才考虑。4. YOLOv8训练配置与实测收敛过程4.1 模型选型与训练参数我选的是YOLOv8s作为基座。为什么不用nano虽然nano更快但在小目标检测比如隔着一定距离观察到的眉间区域上s模型的召回率明显更好。为什么不用m或更大因为我的训练数据只有2200张模型容量再大就明显出现过拟合迹象验证集指标反而下降。数据量的天花板决定模型容量的上限。训练参数如下可以直接参考# pain_data.yaml path: /path/to/pain_dataset train: images/train val: images/val test: images/test nc: 4 names: 0: brow_lower 1: eye_close 2: nasolabial 3: hand_clench训练命令yolo train \ modelyolov8s.pt \ datapain_data.yaml \ imgsz640 \ epochs200 \ batch16 \ optimizerAdamW \ lr00.001 \ lrf0.01 \ augmentTrue \ patience30 \ projectruns/pain_detect \ nameexp_pain_v1几个参数我单独解释一下imgsz640初始测试时我试过imgsz320训练速度快但眼睑紧闭这种细节区域在320分辨率下特征非常弱mAP50掉到0.5以下没法用。640是比较均衡的选择。optimizerAdamW医疗小数据集上AdamW比SGD收敛稳定。SGD在数据量充足时上限更高但要花更多时间调学习率小数据集不值得。patience30如果验证集损失连续30个epoch不下降就早停。我在第120到160轮之间就触发了早停说明数据量确实偏小训练后期没有更多增益。4.2 损失曲线与常见问题整个训练过程我盯了三张图box_loss、cls_loss、df_loss。有一个明显的现象是brow_lower和eye_close这两个类别的loss下降速度远快于nasolabial和hand_clench。原因也很直白——后两个类别的框数量少而且自采的模拟姿态和公开数据的真实姿态在视觉上有差异模型学起来吃力。训练期间还出现过一次BN层相关问题。具体表现是训练到第70轮左右cls_loss突然出现尖峰然后val指标一度回退。排查后确定不是网络结构问题而是某个batch里混入了几张极端暗光图像导致梯度异常。处理方式简单粗暴把那几张暗光样本从训练集抽出放到验证集的尾部做鲁棒性测试问题随即消失。4.3 最终评估结果在完全没参与训练的测试集上最终评估结果大致如下类别PrecisionRecallmAP50mAP50-95brow_lower0.810.740.820.53eye_close0.850.790.860.56nasolabial0.720.610.680.41hand_clench0.760.650.720.45整体0.790.700.770.49这个结果放在通用检测任务里不算亮眼但考虑到疼痛评估本身的主观性以及数据量的限制作为辅助检测器已经能干活了。实际部署之前我还跑了一轮“旋转分辨率”的回归测试把测试集按短边放大到960再推理mAP50整体提升约4个百分点但推理耗时增加了近一倍。边缘设备上怎么取舍要结合场景来定。5. 用评估结果反推数据集短板以及三类典型误检拆解模型拿到手之后不能只看数字。我花了大量时间逐帧看推理结果总结出了三类典型误检每一类都能追溯到数据的某个结构性问题。第一类眼睑紧闭与正常人眨眼/眯眼混淆。模型会把光线强烈时人物下意识眯眼的画面检测为eye_close。根因是在采集模拟动作时大家做“眼睛紧闭”动作都做得特别用力缺少“轻度眯眼”这种中间状态样本。解决办法是扩充数据集中“non-pain眯眼”的负样本帧。对检测模型来说负样本缺席会让边界学不宽这是很多自建数据集的通病。第二类老年人的静态皱纹被检出为高置信度的brow_lower。老年人额头和眉间本来就有深纹路模型对纹理响应极敏感直接当成动作单元输出。这个误检对疼痛评估的影响很致命——如果不加处理老年患者一入镜就可能持续输出“疑似疼痛”的检测结果整个系统直接报废。我在后处理里做了一层修正结合相邻帧的时序信息要求同一个检测框在连续至少5帧中持续存在且位置稳定才判定为有效事件瞬时出现或闪烁的框一律丢弃。实测这个后处理可以把静态皱纹导致的误报降低六成以上。第三类手部握紧与抓握床栏/被角混淆。在真实病房场景中患者手部通常离身体较远而且有大量遮挡物模型容易把“手握住物体”误判为hand_clench。这个问题的根治需要引入手部姿态估计或深度信息单纯用框检测做不了。我在数据层面做了一些几何约束将手部区域与躯干区域的位置关系写进后处理逻辑手部离躯干过远时降低其置信度权重。虽然不算彻底解决但已经能把误报压到可接受范围。还有一个容易被忽略的实际问题摄像头视角的变化。公开数据集的疼痛表情图像大多是正面或轻微偏侧视角但病房顶挂式摄像头拍摄角度通常接近45度俯视。我拿45度俯视角度拍了几十张样本做测试模型整体mAP50掉到0.63掉幅很大——验证集里的正面/轻微侧视角数据无法完全代表真实部署视角。这个问题的正确处理方式是在病房布点设计阶段提前确定摄像头角度按实际视角采集补充数据而不是事后找算法弥补。6. 落地部署时的隐私合规与评估边界6.1 隐私合规是“能不能跑起来”的生死线如果这个模型只是放在实验室里测试那没什么问题。但一旦要接到病房系统里隐私合规就是第一个要过的问题。这里分享一点我实际踩过的经验摄像头采集端与推理端必须做数据脱敏。技术上最简单的做法是把推理设备部署在院内内网视频流不出病房网关只将检测框坐标和置信度这类元数据传给服务端把图像数据“留在本地”。涉及患者面部视频必须走完整的伦理审查流程。我在筹备真实环境试点时和医院伦理委员会来回沟通了三次花了一个多月才把流程走完。数据集本身不含真实患者数据但部署环境涉及真实场景这条绕不过去。清晰的告知义务不能省略。病房内的采集区域、采集目的、数据保留期限都需要写清楚获得患者或家属授权后方可实施。6.2 模型的辅助定位再强调一次这个检测模型是辅助评估工具不是诊断设备。它输出的不是“疼痛分数”而是“疼痛相关行为线索的检测结果”。最终能否将检测结果映射到具体的评分量表中需要由临床团队基于本院的评估规范做二次开发绝不能直接把模型的输出等同于疼痛诊断。我在项目总结里给团队定的原则是模型永远只做“发现线索”不做“给出结论”。比如检测到眉间紧缩和高置信度的手部握紧系统提示护士“患者可能有不适表现请人工复核”而不是自动在病历系统里写入“患者疼痛”。这个边界守住了项目才真正具有可落地的价值。6.3 部署到边缘设备的流程从训练服务器到病房边缘盒子我走的部署链路是YOLOv8 pt模型导出为ONNX再用TensorRT FP16量化。Jetson Orin Nano上进行转换和推理测速耗时约40毫秒每帧满足实时需求。如果目标设备是算力更弱的IPC建议直接改用YOLOv8n或做INT8量化但INT8量化之后mAP50会有大约3-5个点的下降需要在精度和速度之间做取舍。部署时还有一个隐藏问题摄像头动态画面的拖影。病房摄像头一般是25到30帧每秒患者移动时画面会出现运动模糊。我在测试视频流推理时发现运动模糊会让模型置信度整体下降10到15个百分点。这个问题我在数据增强阶段没有顾及后续要么提高采集端帧率要么在推理前加一步去模糊预处理这算是目前项目遗留的最大短板之一。7. 基于这次项目实践我对疼痛视觉检测的几点真实判断整个项目从数据整理到部署验证前后用了将近四个月。过程中最深的体会是数据集的价值远大于模型结构本身。YOLO的调用门槛现在已经被压得很低任何人都能用官方预训练权重跑通一条训练链路真正决定一条医疗AI辅助检测路线能不能走通的是数据的质量、标注的严谨度和场景适配的完整度。2200张图听起来不多但如果每张图的标注都经得起逐框审查它能发挥的作用比两万张粗糙标注的数据还大。如果你也想复现类似项目我的建议是不要一上来就奔着“做个完美的疼痛检测系统”去先从单一行为线索做起把一个类别的检测精度打磨到90%以上再逐步扩展。另外有必要向你强调在实际落地前一定要找到愿意配合的临床团队做需求对齐和场景验证——很多你认为理所当然的假设比如摄像头角度、光照环境、患者正常状态下的表情基线只有在拿到真实场景反馈后才会暴露出来。技术路线本身不复杂复杂的是对场景的理解和尊重。