简介基于YOLOv8的人员溺水检测告警监控系统是一套面向深度学习毕业设计及计算机视觉初学者的完整实践项目可用于泳池、海滨、水上乐园等场景对溺水、离水及正常游泳人员进行实时识别检测类别覆盖Drowning、Person out of water、Swimming。压缩包内共681个文件整体约78.23MB其中293个Python源码负责逻辑与界面交互151个YAML配置用于模型参数设置3个pt文件为训练好的YOLOv8权重另含XML/JSON标注文件、JPG/PNG预测结果图、Shell部署脚本、UI设计文件、CSV指标记录及Dockerfile等目录层次明确。运行main.py即可打开GUI并自动加载模型支持本地图片、视频和摄像头实时检测附带的评估指标曲线能够直观反映模型训练过程中的精确度、召回率变化便于分析性能、优化参数。目前已有765人浏览学习适合需要快速搭建溺水检测系统、开展毕业设计或深入理解YOLOv8工程部署流程的读者。1. 溺水检测为什么选YOLOv8而不是分类模型先定一个反直觉的结论提到人员溺水检测很多从分类任务转过来的人第一反应是训练一个“溺水/正常”二分类模型拿一帧画面丢进去输出一个概率。这个思路在固定机位、单一泳池、光照稳定的实验室环境里能跑通一换到真实水域监控就崩水花、倒影、泳姿变化、救生圈和漂浮物都会让分类器给出毫无道理的置信度。真正能落地的方案是把任务拆成两步——先用目标检测把画面里的人找出来再用时序逻辑判断这个人的行为是不是符合溺水特征。YOLOv8在这个方案里恰好是最稳的一环检测精度高、推理快、权重生态成熟社区里关于yolov8训练自己的数据集的资料也最全。这套搭配适合两类人一类是做安防和水域监控集成的工程师需要一套能交付的告警系统另一类是拿这个方向做课设和毕设的学生源码、模型、GUI界面俱全照着改参数就能跑起来。2. YOLOv8模型选型检测到人和判断溺水是两件事别混在一起做2.1 为什么单靠分类模型做溺水识别会翻车溺水检测最容易犯的错误是试图让一个分类网络同时承担“找目标”和“判行为”两件事。分类模型给的是整张图的全局概率它不知道人具体在哪也没法对多个游泳者做独立判断。泳池里同时有三个人一个人在正常游、一个人静止漂浮、一个人在水底挣扎分类模型只能给一个整体结论这在实际监控里没有任何可用性。换成检测模型之后输出变成一组边界框每个框带类别和置信度。系统对每个框单独做行为判断——这个人是长时间静止在原地还是在快速挥手还是检测框长时间不出现在水面区域。每一种判断对应不同的告警策略而不是一个笼统的“溺水概率”。这也是YOLOv8在这个项目里的核心分工它负责稳定地把人从复杂的水面背景里挖出来行为归类交给后端的时序逻辑。2.2 YOLOv8相比YOLOv5和旧版检测器改了什么对溺水场景有什么实际好处YOLOv8在结构上把anchor-based换成了anchor-free检测头不再需要预设一组先验框尺寸。对溺水检测这种目标尺度跨度大的任务anchor-free省去了聚类调anchor的步骤小目标——比如远距离泳池尽头的人头——和大目标用同一套回归机制处理边界框的定位精度整体更稳。另一个实用改动是C2f模块替换了C3。它在保留梯度分流的同时增加了更多的分支连接参数量略涨但特征表达更强。在泳池这种强反光、纹理重复的背景里更强的特征提取意味着人形轮廓和波纹背景的区分度更高。直观表现就是同样的置信度阈值下水面反光造成的误检框数量明显低于YOLOv5。选择具体型号时按部署硬件来定我一般参照这个标准型号参数量级推理硬件建议场景判断yolov8n最小CPU/树莓派/低端盒子要跑多路视频才选精度损失明显yolov8s中等GTX 1660 Ti及以上显卡单路1080p检测告警的稳妥起点yolov8m较大RTX 3060及以上双路视频或需要更高小目标召回率yolov8l/x很大RTX 3080以上追求极致精度且不差钱不建议实时监控用标题里的方案主打精确度高我建议直接选yolov8s起步。GTX 1660 Ti这类卡跑s模型640分辨率输入单帧推理时间在15到25毫秒之间留给后续GUI渲染和告警判断的余量很充足。2.3 拿到源码包之后第一时间该看哪几个文件很多人的习惯是把压缩包解压后直接双击GUI脚本黑匣子跑起来再说。但既然是准备做二次开发和调参我建议先花十分钟把模型文件和配置文件的脉络理清楚。一个标准的YOLOv8检测项目里最需要关注三个东西权重文件常见命名是best.pt或last.pt、训练用的数据配置文件data.yaml、以及检测脚本里关于类别名和置信度阈值的常量。先跑一段最小测试确认模型能正确加载并输出结果from ultralytics import YOLO import cv2 model YOLO(best.pt) # 换成自己训练完的权重路径 frame cv2.imread(test_frame.jpg) results model.predict(frame, conf0.4, verboseFalse) for r in results: boxes r.boxes print(检测框数:, len(boxes)) print(坐标:, boxes.xyxy.cpu().numpy()) print(置信度:, boxes.conf.cpu().numpy()) print(类别索引:, boxes.cls.cpu().numpy())这段代码逻辑很简单但能一次性暴露出三类问题第一模型能不能正常读取权重报错通常在torch和ultralytics库版本不匹配第二显存是否够用如果推理时爆显存需要把batch调成1并降低输入尺寸第三输出类别索引是否和你的预期一致比如你只训练了person一个类那cls应该永远是0如果出现了其他数值说明训练数据和当前权重的类别定义对不上。2.4 模型结构上的两个细节检测头输出和输入分辨率的影响YOLOv8的检测头输出的是一个张量形状是(1, 84, 8400)或者(1, 84, 2100)取决于输入尺寸。84的构成是4个边界框坐标加80个COCO类别概率。如果你用预训练的yolov8s.pt做迁移学习前80个类别概率会被你自定义的类别数替换所以训练自己数据集时nc改成几这个维度就变成4加几。搞清楚这个数字才能在推理脚本里调试输出解析逻辑而不是拿着黑匣子瞎试。输入分辨率对溺水检测的影响比很多人想的更大。泳池区域的监控画面里人的像素高度往往只有几十个像素640分辨率下的人脸几乎不可辨但人形轮廓仍然保留了足够特征。如果漏检严重可以考虑把imgsz从640提高到768甚至960。代价是推理时间大概按分辨率平方上涨GTX 1660 Ti跑768分辨率还能撑住再往上建议换卡。3. 数据集与标注精确度高不是模型训出来的是标注质量喂出来的3.1 溺水检测数据集从哪来三种常见来源和各自的坑溺水检测没有现成的大型公开数据集可以直接用这和COCO、VOC那种成熟领域不一样。常见的做法是三种来源拼凑第一从网络公开的泳池监控和游泳比赛视频里截帧优势是场景真实劣势是跳水、转身这类动作和溺水姿态容易混淆第二自己到泳池边用手机或相机拍摄覆盖不同距离、不同光照和不同泳姿第三用公开的行人检测数据集做预训练再用前两类数据微调让模型先学会“人长什么样”再学“人在水里长什么样”。数据标注的时候不要贪多宁缺毋滥。一千张质量高、边界框贴合、覆盖多种姿态的图片比三千张随手截帧、标注粗糙的图片更容易训出高精度模型。有一个经验值单类别检测任务八百到一千两百张标注图就能把mAP0.5拉到0.9以上前提是每张图里的人和背景要有足够的差异化。3.2 标注类别怎么定一个class和两个class的取舍这里有一个工程上容易纠结的分叉点有人觉得溺水检测应该标注“person”和“drowning”两个类后者表示正在挣扎的人。我不建议这么做原因很现实溺水姿势的定义在数据上无法做到一致标注。同一张图三个人标注可能给出三种不同的边界框和类别判断模型学到的是标注者的噪声而非溺水特征。更稳的做法是只标一个person类让模型专注于“人在画面中的位置和范围”把“是不是溺水”的判断全部交给后端的时序逻辑。这个逻辑可以根据检测框的移动轨迹、面积变化、在某个区域停留的时长等多个维度来定义可解释性强而且改起来不用重新训练模型。告警规则写错了改代码就行类别标错了得重新标几百张图成本完全不是一个量级。3.3 labelme标注结果转YOLO格式转换脚本和四个边界坑labelme是标注阶段最顺手的工具保存的是JSON文件。但YOLO训练需要的是txt格式的归一化边界框所以转换这一步几乎是绕不开的。下面这个脚本可以直接用来处理标注结果import json import os from glob import glob label_map {person: 0} # 只标注了person一个类 def convert_labelme_to_yolo(json_path, out_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] base_name os.path.basename(json_path).replace(.json, ) out_path os.path.join(out_dir, base_name .txt) lines [] for shape in data[shapes]: label shape[label] if label not in label_map: continue # labelme坐标是[x1, y1, x2, y2]形式需要转成YOLO的中心点格式 points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 归一化到0~1区间 cx ((x_min x_max) / 2) / img_w cy ((y_min y_max) / 2) / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h # 过滤掉无效标注坐标反了或越界 if w 0 or h 0 or cx 0 or cy 0 or cx 1 or cy 1: print(f跳过无效标注: {json_path}, label{label}) continue lines.append(f{label_map[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n) if lines: with open(out_path, w, encodingutf-8) as f: f.writelines(lines) # 转换整个标注目录 json_files glob(labelme_annotations/*.json) os.makedirs(yolo_annotations, exist_okTrue) for jf in json_files: convert_labelme_to_yolo(jf, yolo_annotations)这段脚本里有几个参数和细节需要留意。第一labelme的points字段可能存储多边形而不是矩形所以脚本里取了所有点的最小和最大坐标来构造外接框这在目标近似矩形的情况下够用。第二如果一张图里有多个人txt文件里就会有多行数据每行对应一个目标。第三img_w和img_h必须和原图尺寸一致如果labelme的JSON是从缩略图标注的转换时必须先确认尺寸匹配。第四YOLO的归一化坐标全部是浮点数保留六位小数足够多了反而会造成txt文件体积膨胀。3.4 训练集和验证集划分比例、同源策略和数据增强划分train和val的时候最容易犯的错误是直接对全部图片做随机切分。泳池视频通常都是连续帧相邻帧之间的内容高度相似随机切分会导致验证集里出现大量和训练集几乎相同的画面评估指标虚高。正确的做法是按视频或拍摄会话分组同一个视频的帧要么全进train要么全进val测试出来的mAP才有参考意义。我一般按8:2划分子集然后只在训练集上做增强。数据增强环节Mosaic是YOLO系列训练时的默认选项把四张图拼在一起训练对小目标检测提升明显。泳池场景里还要额外加两类增强一是HSV颜色扰动模拟不同水质和不同时间段的色调变化让模型不依赖水色的固定值二是随机翻转但注意必须水平翻转垂直翻转会把水面倒影变成天空语义完全错乱。4. 训练与评估指标曲线训练自己的数据集从环境搭建到看懂每一条曲线4.1 环境搭建CPU版本还是GPU版本别在这上面浪费时间看到热词里搜“ubuntu20.04搭建yolov8环境cpu版本”的人很多说明环境问题确实是拦路虎。我的建议很直接如果手里没有NVIDIA显卡就用CPU训练但要把模型换成yolov8n输入分辨率降到480epochs适当增加训练时间完全能接受如果有显卡优先装CUDA版的PyTorch训练速度差一个数量级。环境搭建的常见做法是创建一个独立的Python虚拟环境避免和系统Python或者其他项目互相污染conda create -n yolo python3.10 -y conda activate yolo pip install ultralytics安装完先跑一行命令验证环境可用yolo predict modelyolov8s.pt sourcehttps://ultralytics.com/images/bus.jpg如果这条命令能正常输出检测结果说明torch、ultralytics、opencv都已经就绪。如果报CUDA相关错误先检查torch.cuda.is_available()是否为True再确认显卡驱动版本如果报ImportError多数是opencv-python和头文件版本的兼容问题重装opencv-python-headless通常能解决。4.2 训练命令和超参数怎么设一份可以直接抄的参数表训练自己数据集的核心命令就一条但参数的含义必须搞清楚否则翻车了都不知道调哪个yolo detect train \ modelyolov8s.pt \ datadataset.yaml \ epochs150 \ imgsz640 \ batch16 \ device0 \ projectruns/detect \ namedrowning_v1data.yaml是数据集的配置文件放在项目根目录下内容指向训练和验证图片的路径path: ./dataset train: images/train val: images/val nc: 1 names: 0: person参数选择的逻辑说几个重点。epochs设150是因为溺水检测规模不大100到200轮基本上能看到完整的收敛过程太少模型欠拟合太多会过拟合。batch的设置取决于显存8GB显存以下先用默认的16试报CUDA out of memory就把batch改成8或4。imgsz默认640如果你的监控画面里人很小改成768或960能直接提升小目标召回率。device0表示用第一张显卡CPU训练则改成devicecpu但训练速度会慢很多。模型选择上用yolov8s.pt做预训练权重而不是随机初始化。迁移学习能继承COCO数据集里学到的通用特征尤其是“人”这个类别的先验知识收敛更快最终精度也更高。4.3 训练过程中的loss曲线什么时候该停什么时候是翻车训练完成后runs/detect/drowning_v1/目录下会生成results.csv文件里面记录了每一轮的各类loss和指标。想直接看图可以用ultralytics自带的曲线也可以自己用脚本从csv里画loss曲线import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/drowning_v1/results.csv) fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(df[epoch], df[train/box_loss], labeltrain box loss) axes[0].plot(df[epoch], df[val/box_loss], labelval box loss) axes[0].set_xlabel(epoch) axes[0].set_ylabel(box loss) axes[0].legend() axes[0].set_title(边界框回归损失) axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].set_xlabel(epoch) axes[1].set_ylabel(mAP50) axes[1].legend() axes[1].set_title(mAP0.5 曲线) plt.tight_layout() plt.savefig(training_analysis.png, dpi150)看懂这条曲线的关键点在于对比train loss和val loss的走向。正常收敛的情况是两条曲线同步下降最终在某个区间内趋于平稳。最常见的翻车形态是train loss持续下降但val loss先降后升这是过拟合的典型信号解决方法是减少epochs或增加数据增强强度。另一种情况是loss曲线剧烈震荡不呈现下降趋势通常是学习率过大或batch_size太小优先把lr0从0.01降到0.001。epoch数不代表训练好坏val loss不降反升的那一刻就是该停的时候。ultralytics默认开启了早停机制patience参数控制连续多少轮验证指标不提升就自动停止保持默认即可。4.4 评估指标曲线mAP、PR曲线和混淆矩阵到底怎么读训练结束后val阶段会生成一系列评估图表这些就是标题里说的“评估指标曲线”。重点看三样PR_curve.png、confusion_matrix.png和results.csv里记录的mAP数值。PR曲线横轴是Recall纵轴是Precision曲线越靠近右上角越好。曲线下的面积就是AP值mAP是所有类别的AP平均。对单类别的溺水检测来说mAP0.5达到0.9以上说明模型在“人”这个目标上的检测能力已经非常可靠mAP0.5:0.95则更严格通常比mAP0.5低二三十个百分点这是正常现象。混淆矩阵关注的是误检来源。溺水检测场景里最常见的误检是把泳池边的救生圈、浮板甚至水面波纹检测成人。如果混淆矩阵里person所在行有大量其他类别的计数说明负样本不够需要在训练集里补充没有人的泳池空镜。还有一个容易被忽略的点模型评估时用的置信度阈值和推理时不同。评估报告基于多个阈值绘制曲线而实际推理时conf参数设为多少直接决定了告警的灵敏度。conf设太低误检框变多告警会疯响conf设太高漏检变多真出事了没反应。对这个项目我建议从conf0.4开始调观察误报率再上下微调。4.5 模型导出从pt到ONNX为GUI和边缘部署铺路训练完的best.pt只适合在Python环境里用PyTorch跑。真实监控系统里GUI模块或者边缘设备需要更轻量的部署格式。先用ultralytics的导出命令把模型转成ONNXyolo export modelruns/detect/drowning_v1/weights/best.pt formatonnx imgsz640导出成功后同一个模型就有了两份权重。调试阶段用pt文件交付给GUI和后续部署时用ONNX推理速度更快且不依赖PyTorch完整环境。像rk3588这类边缘开发板上部署yolov8也是从ONNX继续转RKNN格式这条链路是通的。导出后建议顺手验证一次ONNX模型和pt模型的推理结果一致性差异通常在置信度的小数点后几位不影响告警逻辑。5. 溺水检测避坑五个最容易在真实水域场景翻车的具体问题5.1 水面反光和波纹把检测框打得乱跳现象泳池水面有阳光直射或灯光反射时检测结果出现大量短命检测框置信度在0.3到0.5之间反复横跳告警系统在无人状态下频繁触发。原因水面反光区域的纹理特征在特定光照下和人形轮廓非常相似模型把高光斑块当成了目标。水波纹不断变化导致这些误检框无法稳定存在看起来就是检测框乱跳。解决先把推理置信度从0.2往上拉到0.45左右过滤掉低置信度的反光误检。如果误检还有残留把输入分辨率从640降到416能削弱高频纹理信号对反光抑制很有效。再不行就收集一批带反光的空镜帧加入训练集标上无目标让模型学会忽视这类背景。5.2 漂浮物和救生圈被当成溺水者报警现象泳池里的浮板、救生圈、漂浮的气球被稳定检出为人且置信度很高告警持续触发。这类误检比反光更难处理因为检测框非常稳定。原因训练集里缺少“水面上的非人漂浮物”这类负样本。模型没有见过足够多的负例自然会把和人体尺度相近的物体归为人。解决收集五十到一百张含漂浮物但没有人的泳池图片放进训练集里不标注任何类别。YOLO训练时这些图片作为纯背景参与loss计算模型会学到“这种区域不应该输出检测框”。如果还是压不住加一个轻量级的二分类后置过滤器从检测框区域裁图再用一个小网络判断“是不是人”。5.3 人沉底或半浸在水中时小目标漏检现象人已经溺水下沉身体大部分被水淹没只露出头部或一小块背部检测框完全消失系统没有告警。这是所有坑里最致命的。原因down到的目标太小YOLOv8在640分辨率下的特征图对这类目标的响应微弱加上水的折射和浑浊度进一步减弱了轮廓特征模型给出的置信度低于设定阈值。解决把推理分辨率从640提升到960小目标的像素覆盖更多检测稳定性提升明显。同时结合时序策略——如果在连续N帧中一个人从正常的检测框逐步变小直到消失这本身就是溺水的过程信号应该触发“目标消失”告警而不是只依赖单帧检测。5.4 逆光和夜间环境下检测精度断崖式下降现象泳池在下午逆光时段或者夜间灯光昏暗的情况下人的检测框时有时无精度比白天差一大截。原因训练数据里白天的样本占比太高模型对低照度和强逆光的适应能力不足。逆光时人变成剪影纹理细节全部丢失检测器仅凭轮廓特征难以稳定工作。解决最直接的办法是采集夜间和逆光时段的数据补进训练集。如果采集条件不允许用OpenCV的CLAHE自适应直方图均衡化对输入帧做预处理提升暗部细节后再送进模型。这个方法对逆光场景的改善非常明显代价是每帧多了两三毫秒的处理时间。5.5 检测到人稳定存在但GUI不触发告警现象模型检测没问题人静止在水面上很久告警就是一声不吭。原因告警逻辑只看单帧而单帧逻辑觉得“人存在是正常的”或者告警模块的坐标判断出了问题——比如把泳池岸边散步的人也算进监测区域他一直在动所以不触发再或者ROI区域画反了把水里的人排除在外。解决告警判定必须基于时序而非单帧核心逻辑是“同一个目标在限定区域内持续停留超过阈值”。同时检查ROI坐标是否正确映射到当前帧的分辨率很多GUI项目在窗口缩放后ROI坐标没有跟着缩放导致判定区域完全错位。6. 把GUI和告警接上用连续帧去抖和置信度阈值调解误报GUI界面做得再精美告警逻辑不对也白搭。YOLOv8的检测结果要接入GUI核心是解决两件事一是让检测框和视频帧同步渲染不卡顿二是让告警判定具备时间维度不能一检测到人就报警。下面这段去抖逻辑是这个项目里我认为最值得抄的部分class AlarmJudge: def __init__(self, trigger_frames15, stay_threshold8, conf_threshold0.45): self.trigger_frames trigger_frames self.stay_threshold stay_threshold self.conf_threshold conf_threshold self.history {} def judge(self, boxes, confs, track_ids): for tid, box, conf in zip(track_ids, boxes, confs): if conf self.conf_threshold: continue cx (box[0] box[2]) / 2 cy (box[1] box[3]) / 2 if tid not in self.history: self.history[tid] [cx, cy, 1] continue prev self.history[tid] moved abs(cx - prev[0]) abs(cy - prev[1]) if moved self.stay_threshold: prev[2] 1 else: prev[2] 1 prev[0], prev[1] cx, cy if prev[2] self.trigger_frames: return tid, box return None这段代码的核心逻辑是给每个跟踪ID维护一个“连续停滞帧数”计数器。目标物每次出现在新位置时都要判断它相对上一帧的位移量是否小于stay_threshold。连续15帧都没有明显移动才触发告警并返回这个目标的ID和边界框坐标。这么做的好处是自由泳和蝶泳的人虽然一直在检测框内但因为位置持续变化永远不会触发告警而溺水者沉底后基本静止很快就能被识别出来。GUI主循环里用QTimer驱动视频帧读取和推理时间间隔设置为30毫秒左右对应约30fps的显示帧率。在这个循环里调用AlarmJudge然后把告警状态用红色边框和提示文字渲染到画面右上角。每帧推理耗时如果超过30毫秒视频显示就会卡顿这时候优先考虑关闭GUI里的绘制特效或者换用ONNX模型提升推理速度。这套系统从数据集标注、模型训练到告警集成走一遍完整流程大概需要一周时间其中数据准备占掉一半。我个人的习惯是先把告警逻辑用单帧测试跑通再套GUI界面否则界面和模型一起调出了问题根本分不清是推理问题还是渲染问题。最后再补一句我在每个项目里都会做的事触发告警的同时把当前帧保存成带时间戳的截图既是给业主的证据也是排查误报的记录。希望这套梳理能帮你在自己的溺水检测项目上少踩几个坑。本文还有配套的精品资源点击获取