1. 为什么这次我坚持选YOLOv11——安全帽检测的真实痛点与选型逻辑
先聊点实在的。安全帽检测这活儿,做安防和工业视觉的朋友应该都不陌生——工地上戴帽子和不戴帽子的差别,往小了说是违规行为,往大了说直接关系到人身安全。这两年相关监管越来越严,靠人工盯监控画面根本盯不过来,一个中大型工地几十路摄像头,保安眼睛熬成兔子也看不住。所以用深度学习模型做自动检测,把"谁没戴安全帽"从视频流里实时揪出来,是个需求非常刚性、落地场景非常明确的方向。
我这次做的是一个完整的安全帽检测系统,技术栈是 YOLOv11 + Python + UI界面 + 登录注册模块,目标不是只跑一个模型Demo,而是做成一个能拿出去演示、能接真实摄像头、能给别人用的"成品项目"。
先说为什么模型选YOLOv11而不是别的。之前在YOLOv5时代,我做类似项目主要用YOLOv5s或YOLOv5m,速度和精度平衡不错,但有两个痛点一直绕不开:一是小目标检测能力一般,工地上人离摄像头远的时候,安全帽在画面里可能只占二三十个像素,漏检率很高;二是部署到低配机器上,帧率上不去。YOLOv11发布之后,我第一批就跑去实测了。它在C3K2模块和注意力机制上做了不少改动,检测头也重新设计过,用小模型跑同样的数据集,mAP比v8同体积模型有明显提升,而且推理速度并没有变慢太多。对我来说最直观的感受是:用nano或者small版本跑安全帽这类单类别检测,精度已经够用,推理速度在普通办公笔记本的CPU上也能跑到每秒十几帧,这在以前是不敢想的。
这个项目除了模型本身,还配了UI界面和登录注册界面。很多人觉得这是花架子,但实际做项目交付的时候你会发现,没有图形界面,客户根本不认。你跟客户说"模型mAP 95%",他毫无感觉;你双击打开一个登录窗口,输入账号密码,看到实时画面上框住没戴安全帽的人并报警,他马上点头。所以这套系统把后端推理和前端交互打通了,属于"工程化完整度"比较高的一个项目。
适用人群我觉得有三类:一是正在做毕业设计或课程项目的学生,需要一套能讲清楚原理、能演示、能写进论文的完整系统;二是安防或工业视觉方向的技术人员,想快速搭一个安全帽检测原型验证业务;三是打算系统学习YOLO系列工程化开发的朋友——从数据集准备、模型训练到UI封装、登录注册,一整条链路走下来,比零散看教程要高效得多。
这里先说一下项目整体架构,后面每个模块我都会展开讲。系统大概分四层:第一层是数据层,用的YOLO格式的安全帽数据集,包含标注好的图片和txt标签文件;第二层是模型层,加载训练好的YOLOv11权重做推理;第三层是业务层,负责检测结果的后处理、报警逻辑、检测记录管理等;第四层是展示层,PyQt5做的桌面UI,包含登录注册界面和主检测界面。下面我从数据集开始,把每一步实际怎么做的都写清楚。
2. 数据集准备:比训练更费时间的环节,避坑全记录
模型效果的上限,在数据准备阶段就定死了。做安全帽检测,数据集质量比模型参数重要得多。这次用的YOLO格式数据集,我花的时间比训练本身多两三倍。
2.1 数据来源与类别设计
安全帽检测在绝大多数实际场景下是二分类问题:戴了安全帽(helmet)和没戴安全帽(no_helmet或head)。有些数据集会把"head"单独做一类,也就是检测人头,然后通过判断人头区域里有没有安全帽来做逻辑判断。但我更推荐直接训练两个类别:helmet和no_helmet。原因很简单,端到端检测省事,推理时直接看类别就能输出结果,不用额外写复杂的高度判断逻辑。
数据来源方面,网上公开的安全帽检测数据集不少,但直接用之前一定要做清洗。我这次的数据集大概八千多张图片,来源包括公开数据集和部分自采数据。自采数据很重要,因为工地场景复杂,光照、遮挡、摄像头角度、安全帽颜色五花八门,公开数据集覆盖不足。建议有条件的话拿手机在工地外围、工厂门口拍一些,不同角度、不同距离都拍,尽量覆盖早晨逆光、中午强光、傍晚昏暗这些光照条件。不要嫌麻烦,后面你会发现模型在自采数据上的表现往往决定项目成败。
2.2 数据标注要点
标注工作是纯体力活,但有一些细节直接决定训练效果。
第一,安全帽区域的边界框要略微留一点余量,不要框得死死的。比如安全帽顶到帽檐的边缘,标注框稍微往外扩两三个像素,模型学到的特征更完整,尤其是安全帽这种半圆弧形目标,框太紧反而导致特征被截断。
第二,"半遮挡"情况要认真处理。工地上很常见的情况是工人侧身站着,安全帽只露出一半。这种样本不要删,标注时按可见部分框。模型见过的遮挡情况越多,实际推理时鲁棒性越好。如果数据集里全是完整清晰的安全帽,一到现场就抓瞎。
第三,难例挖掘。训练完第一轮之后,把模型预测错误的图片挑出来,人工确认后重新标注、加入训练集。这个迭代过程我跑了三轮,每一轮都在明显涨点。难例一般集中在:安全帽颜色和安全服颜色相近、远距离小人头、逆光剪影、帽子拿在手上被误检成"戴了"等场景。
2.3 YOLO格式的目录结构说明
数据打好标签之后,要整理成YOLO格式。目录结构如下:
helmet_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamlimages目录下放图片,labels目录下放同名txt标签文件。txt里每一行是一个目标:类别id 中心点x 中心点y 宽度 高度,其中坐标值全部归一化到0到1之间。data.yaml内容大致长这样:
train: ./images/train val: ./images/val test: ./images/test nc: 2 names: ['helmet', 'no_helmet']有一个细节容易踩坑:YOLO要求images和labels的图片名一一对应,文件名前缀必须完全相同,哪怕扩展名不同也不行。很多人从网上下载数据集之后,图片是.jpg、标签是.txt,看着没问题,但训练时频繁报"no labels found",就是因为文件名对不上。我建议写个脚本校验一下:
import os from pathlib import Path img_dir = Path('helmet_dataset/images/train') label_dir = Path('helmet_dataset/labels/train') img_files = [p.stem for p in img_dir.glob('*')] label_files = [p.stem for p in label_dir.glob('*.txt')] missing_labels = set(img_files) - set(label_files) missing_imgs = set(label_files) - set(img_files) print(f'缺标签的图片数量: {len(missing_labels)}') print(f'缺图片的标签数量: {len(missing_imgs)}')这个检查我每次整理数据集都会跑一遍,能省后面排查问题的大量时间。
2.4 train-val-test划分比例
数据划分上,我习惯用8:1:1的比例。训练集8264张、验证集1033张、测试集1033张。注意分配的时候要按目录随机抽,不要按时间段抽——如果前8成图片全是白天、后2成全是傍晚,验证集会失真。可以用Python的random.shuffle打乱后再划分,或者直接用sklearn的train_test_split,设好random_state保证可复现。
提示:数据集的划分不是一锤子买卖。后续做难例挖掘时,要把新增样本合理补进各集合,避免验证集被污染。验证集一旦被你反复用来调参,它就不再客观了。真有条件的话,预留一部分完全没有参与训练过程的图片做最终测试,才是真正反映模型泛化能力的数据。
3. 模型训练:配置、调参与撞墙记录
数据准备好之后进入训练阶段。这一部分我不打算只贴配置,我把实际跑训过程中遇到的坑和参数调整思路一起写出来。
3.1 训练环境与参数初始化
环境方面,我用的PyTorch 2.x,CUDA 11.8,显卡是单张消费级卡,显存12G。这种配置跑YOLOv11s完全没问题,理论上调到YOLOv11m也可以跑,但batch size会受限制。
第一次训练我建议直接用YOLOv11s,不要一开始就上nano,也不要上large。理由很简单:s版本是综合性价比之王,模型容量够学安全帽这种不算特别复杂的特征,训练速度和显存占用也友好。如果你的机器只有8G显存,那就用nano,别硬上s。
训练参数我贴一份实际跑通过的配置,可以直接抄作业:
from ultralytics import YOLO model = YOLO('yolo11s.pt') results = model.train( data='helmet_dataset/data.yaml', epochs=120, imgsz=640, batch=16, lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3, warmup_momentum=0.8, warmup_bias_lr=0.1, device=0, workers=8, patience=20, save_period=10, project='runs/helmet_train', name='exp1', pretrained=True, optimizer='SGD', seed=42, )几个参数背后的考虑说一下。lr0选0.01是基于预训练权重做的微调,这个量级对YOLO系列来说是比较安全的起点。warmup_epochs设3轮,让模型前几轮用较小的学习率慢慢进入状态,避免初期loss爆炸。
patience=20表示连续20轮验证集mAP不提升就早停。这个参数一定要注意,不要省。有一次我跑140个epoch的任务,实际到90多轮就停了,如果没有早停,后面几十轮全是浪费时间。imgsz=640是速度和精度的平衡点,用512会快一点但小目标检测明显变差,用960对显存压力大收益也有限,安全帽这个场景640够用。
3.2 训练过程中的loss曲线与判断
训练时我一般同时盯着三个指标:train/loss、val/box_loss、metrics/mAP50-95。
跑起来之后,前几个epoch会看到train/loss快速下降,这是正常的,预训练特征在起作用。大概到20轮左右,mAP50会进入快速爬升期,从70%一路冲到90%以上。如果你到40轮mAP50还在80%以下,大概率是数据有问题,先停下来查标注,不要盲目调参。
有一次我遇到一个诡异现象:训练loss一直在降,但验证集mAP在60多轮之后开始波动不再上升。查了半天,原因是某类图片标注里混了不少错误标签——有些安全帽被标成了no_helmet。模型学的特征被错误标签干扰了。后来我把这个类别的样本抽出来人工复查了一遍,删掉几十张错误标注后重训,mAP直接涨了三个点。所以看到loss下降但mAP不动,优先怀疑数据,不要调学习率。
3.3 关键超参数的调整经验
对安全帽检测这个特定场景,有几个参数值得专门调一下。
一是anchor的适用性。YOLOv11的anchor是自适应学习的,但如果你的数据集里目标尺寸分布很极端——比如纯远距离小人头场景——建议用model.tune()先跑一轮超参搜索。不过说实话,数据集分布比较常规的情况下,默认anchor就够用。
二是mosaic增强策略。YOLO默认训练会开启mosaic,对提升小目标检测能力帮助很大,但mosaic在最后几十个epoch会关闭或降频,这是官方标准做法。我建议数据量少的时候不要关mosaic,数据量充足的情况下,过度的mosaic会让模型对真实场景的上下文感知变差,可以设mosaic=0.5试试。
三是类别不平衡问题。安全帽检测数据集里,戴帽子的样本通常远多于不戴帽子的,因为很多图片本身就是拍工人规范作业的场景。不平衡严重的时候,只调loss不好使。我的做法是收集数据时单独把no_helmet样本补到总样本的35%到40%。YOLOv11对类别不平衡有一定容忍度,但训练集里如果no_helmet占比不到10%,推理时误检率会明显升高。
3.4 最终效果:训练结果指标复盘
训练完成后的验证集结果,我贴一组实际数据供参考。用的还是YOLOv11s模型,mAP50到了94.8%,mAP50-95到了78.6%,Precision 92.3%,Recall 91.7%。安全帽检测属于目标相对规整的任务,这个指标在工程上已经可以用了。如果换成YOLOv11m,mAP50能到96%以上,但推理速度下降明显。做实时监控系统,我宁愿用s把速度保住,精度靠后续的帧级判断逻辑来兜。
模型导出的时候,我同时导出了.pt和.onnx两个格式。.pt用于Python推理和进一步训练,.onnx用于部署测试。导出ONNX的代码很简单:
model.export(format='onnx', opset=12, simplify=True)注意:导出ONNX时如果提示某些算子不支持,检查一下torch和onnx版本,老版本经常卡在C3K2模块的导出上。直接升级到最新稳定版可以解决大多数问题。
4. 从权重到可用服务:推理加速、置信度策略与帧级报警逻辑
训练完只是第一步,真正干活的时候才知道,一个裸模型离一个"可用系统"还差着十万八千里。这一步我把模型封装成真正能处理视频流的东西。
4.1 推理接口封装
我习惯把所有推理逻辑封装到一个类里,对外只暴露一个detect(frame)方法,不管UI还是命令行都能直接调用。封装的好处是后面换模型、加逻辑、做测试都不影响外面。
import cv2 import numpy as np from ultralytics import YOLO class HelmetDetector: def __init__(self, model_path, conf_thres=0.4, iou_thres=0.45): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect(self, frame): results = self.model( frame, conf=self.conf_thres, iou=self.iou_thres, verbose=False )[0] boxes = results.boxes if boxes is None or len(boxes) == 0: return [], frame data = boxes.data.cpu().numpy() detections = [] for row in data: x1, y1, x2, y2, score, cls = row detections.append({ 'bbox': [int(x1), int(y1), int(x2), int(y2)], 'score': float(score), 'cls': int(cls), 'name': self.model.names[int(cls)] }) return detections, frame这个类本身没什么高深技术,但把置信度阈值和NMS阈值作为参数暴露出来,后面调起来会很舒服。我分别用多个工地样本测过,conf_thres设在0.4到0.5之间效果最好。设太低,误检多,墙壁上的圆形标志、黄色安全帽图片都会报警;设太高,漏检增多,戴深色安全帽的人会被漏过去。
4.2 帧级报警和误报抑制
实时摄像头场景不能只看单帧,必须做时间维度的判断。我采用的策略是"连续N帧确认"机制:只有当同一位置的目标连续3帧以上被判定为no_helmet,才触发报警。实现方式很简单,用一个队列保存最近5帧的检测结果,检查当前帧的未戴帽目标框是否在之前几帧中有对应的重叠框。如果只是某两帧误报,队列里的连续性不够,自然不会触发。
同时,检测框的稳定性过滤也很重要。真实场景里画面是抖动的,同一个人的框在相邻帧之间会上下左右漂移。我计算当前帧每个检测框和历史帧检测框的IoU(交并比),IoU大于0.5就认为是同一个目标。如果连续3帧都大于这个阈值,目标跟踪ID就维持不变。这一套轻量逻辑在CPU上几乎不占资源,但能过滤掉90%以上的单帧误报。
4.3 实时监控画面的延迟实测
我用USB摄像头接入系统做了延迟测试。模型跑在GPU上时,一帧推理耗时约12到18毫秒(640分辨率),加上前后处理,整个链路延迟在50毫秒以内,肉眼基本无感。跑在CPU上时,YOLOv11s大约200到300毫秒一帧,勉强能看,但谈不上实时。如果项目要求纯CPU部署,建议换YOLOv11n并开启ONNX Runtime加速,延迟可以压到100毫秒左右。
这个延迟数据在做项目汇报时很有用,建议自己测一遍记录清楚,比PPT里写"实时性良好"有说服力得多。
5. 登录注册与UI界面:把检测能力变成"别人愿意用"的产品
模型再好,没有界面就没法交付。这也是这个项目被称为"系统"而不是"脚本"的原因。
5.1 登录注册模块的设计
登录注册用的是PyQt5,数据存储直接用本地文件或SQLite。对于演示项目和毕设项目,没必要上MySQL,SQLite零配置、单文件、好迁移,完全够用。
我先说表结构的设计。users表就四个字段:id、username、password、create_time。密码存储不要用明文,我用了hashlib的sha256加盐处理。盐值可以是一个固定字符串,也可以每个用户随机生成。这里给个简单实现:
import hashlib import sqlite3 import os from datetime import datetime def get_db_conn(db_path='users.db'): conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row return conn def hash_password(password, salt): sha = hashlib.sha256() sha.update((salt + password).encode('utf-8')) return sha.hexdigest() def register_user(username, password): conn = get_db_conn() cursor = conn.cursor() salt = os.urandom(16).hex() cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, salt TEXT NOT NULL, create_time TEXT NOT NULL ) ''') cursor.execute( 'INSERT INTO users (username, password, salt, create_time) VALUES (?, ?, ?, ?)', (username, hash_password(password, salt), salt, datetime.now().isoformat()) ) conn.commit() conn.close()注册逻辑要注意三点:用户名不能重复、密码长度至少六位、注册成功后直接跳转到主界面,不要让用户注册完还要重新登录一次,体验差很多。
5.2 登录验证与会话保持
登录验证就是把用户输入的密码加盐哈希后跟数据库里的比对。用户登录成功后,把当前用户信息存到全局变量里,后续所有检测记录、报警记录都关联这个用户名。这样每个用户的检测记录在界面上可以按账号区分。
很多教程里的登录界面就做个样子,登录完没有任何权限控制。我这次把检测记录的增删改查都跟用户ID绑定了,虽然只是个简单的UNIQUE约束和外键关联,但整体完整度立刻上了一个档次。
def verify_user(username, password): conn = get_db_conn() cursor = conn.cursor() cursor.execute('SELECT * FROM users WHERE username = ?', (username,)) user = cursor.fetchone() conn.close() if user is None: return False salt = user['salt'] hashed = hash_password(password, salt) return hashed == user['password']注意:这个实现适合教学和演示场景,生产环境建议直接用passlib的bcrypt或argon2这类成熟库处理密码哈希。不要试图自己发明加密算法,这是原则问题。
5.3 主检测界面的功能拆解
主界面我拆成四个功能区:左侧是视频显示区,右侧是控制面板和检测结果列表,底部是状态栏和报警信息区,顶部是菜单栏。四块功能看着简单,但布局和线程设计如果不做对,整个界面会卡到没法用。
视频显示区用QLabel实时刷新帧画面,刷新方式用QPixmap转换,效率足够。右侧控制面板包含"打开摄像头""打开视频文件""开始检测""停止检测""保存截图"五个按钮,每个按钮对应一个槽函数。检测结果列表用QTableWidget展示,每一行记录检测时间、目标类别、置信度、位置。底部报警信息区用QTextBrowser输出,触发报警时追加一行红色文字并播放系统提示音。
关键的架构问题:检测和UI必须在不同线程里跑。如果直接在UI线程里做YOLO推理,画面会卡死。我用了QThread + 信号槽机制——检测线程跑detect(),每处理完一帧就emit一个带帧图像和检测结果的信号,主线程在槽函数里更新画面。不要在槽函数里做耗时操作,只负责刷新UI,否则信号积压,延迟会越来越严重。
5.4 UI界面防卡顿的线程设计
这里给一个最简单的QThread写法框架:
import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class DetectThread(QThread): frame_signal = pyqtSignal(QImage, list) log_signal = pyqtSignal(str) def __init__(self, detector, source=0): super().__init__() self.detector = detector self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) while self.running: ret, frame = cap.read() if not ret: break detections, annotated = self.detector.detect(frame) rgb_img = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb_img.shape qimg = QImage(rgb_img.data, w, h, ch * w, QImage.Format_RGB888) self.frame_signal.emit(qimg.copy(), detections) cap.release() def stop(self): self.running = False self.wait()注意qimg.copy()这个细节不能省。如果直接emit QImage对象,Python的垃圾回收可能在槽函数执行前就把底层数据释放了,画面会出现花屏或随机黑块。这个坑我踩过一次,排查了半天才发现是数据生命周期的问题。
主界面里画检测框,我是在CV2层就把框和标签画好,再把整帧传给QLabel。这样UI代码简单,检测框的绘制还可以利用OpenCV的字体和线条特性,比Qt自带的绘图API更顺手。
6. 串起来之后:项目实测效果与打包发布的完整经验
所有模块都做完之后,项目才算真正进入了"可交付"状态。这个阶段有几个事必须处理,否则系统换个电脑就跑不起来。
6.1 完整系统联调日志
我找了三个场景做实测:一是工地实时摄像头,二是录好的监控视频文件,三是图片文件夹批量检测。
摄像头场景是最容易出问题的。我测试时遇到第一件事就是摄像头索引不对——笔记本自带摄像头是0,USB外接摄像头变成了1,代码里写死0就会一直读不到画面。我后来在UI里加了一个摄像头编号下拉框,让用户自己选择,问题解决。这虽然是个小细节,但项目交付时很能体现实用性。
视频文件场景要注意视频分辨率。有些监控视频是1080P甚至4K,直接把原图丢给模型推理,速度会非常慢(4K分辨率下模型要处理的像素是640的十几倍)。我加了一次预处理:推理前先把帧缩放到640宽,等比例缩小高度,推理完再把检测框坐标映射回原图尺寸。这样做既保证速度,又能在原分辨率画面上叠加框,显示效果很清楚。
图片批量检测主要用来做压测和演示。我放了500张测试图进去跑,全部处理完耗时大约40秒,单张平均80毫秒左右,数据和之前单帧测试基本吻合。
6.2 模型在不同环境下的表现差异
有一个必须提的坑:同样的权重文件,在GPU机器上跑和CPU机器上跑,检测结果可能不完全一样。这不是玄学,是因为浮点计算精度和CUDA的算子实现差异导致的。GPU上可能有些检测框置信度是0.43,过了阈值被保留;CPU上同一帧置信度变成0.39,被滤掉了。
所以别换来换去跑,选定部署环境后以那个环境的实测结果为准,不要拿GPU上的阈值设置直接套到CPU版上。我的处理方式是:把置信度阈值做成配置文件,部署时由现场人员根据实际误报漏报情况微调。
6.3 打包exe的完整流程与踩坑
项目需要交付给没有Python环境的人使用时,打包成exe是刚需。我用的是PyInstaller,打包命令如下:
pyinstaller -w -F main.py \ --hidden-import=ultralytics \ --hidden-import=cv2 \ --collect-all ultralytics \ --add-data "runs/helmet_train/exp1/weights/best.pt;runs/helmet_train/exp1/weights/" \ --add-data "helmet_dataset/data.yaml;helmet_dataset"-w表示无控制台窗口,-F表示打包成单个exe文件。这里有两个大坑必须提醒:
第一,YOLOv11模型文件如果直接作为外部文件加载,打包后路径会变,容易找不到权重。--add-data把权重文件打进包里,运行时用sys._MEIPASS拼接路径,这样exe拷到任何机器都能跑。
import sys import os def resource_path(relative_path): if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath('.'), relative_path) model_path = resource_path('runs/helmet_train/exp1/weights/best.pt')第二,PyInstaller经常漏掉ultralytics的一些依赖文件,打包后exe启动就报ModuleNotFoundError。解决方式是--collect-all ultralytics,把整个包的数据文件一起打包进去。我第一次打包时就漏了,exe跑到一半报找不到配置文件,加上这个参数后问题消失。
还有一个推荐操作:打包后的exe第一次启动会比较慢,因为需要解压资源。可以在exe同目录放一个说明文档,提醒使用人"首次启动等待数秒属正常现象",能减少很多不必要的售后咨询。
6.4 一套持续优化的方法论:难例池闭环
最后分享一个我强烈建议的做法:在生产环境跑一段时间后,持续收集现场漏检、误检的图片,存成一个"难例池",定期定量补充到训练集里重训模型。这套闭环做起来之后,系统在某个具体工地上的表现会越用越好。我有个项目在工厂部署三个月后,误报率比最初下降了近一半,靠的就是这个笨办法。
收集方式很简单,在系统里加一个"误报反馈"按钮,客户觉得哪一帧报警不对,点一下就把当前帧保存到本地。积累一两百张之后做一次增量训练,效果立竿见影。
7. 最后再聊几个实操中的小细节
整个项目从零到一跑通,还有几个零散但很实用的经验,想到哪写到哪。
数据库文件被占用的问题很常见。SQLite数据库如果在检测线程里频繁写入报警记录,用户关闭软件时有时会提示数据库locked。解决思路是写入操作集中到一个单独的队列线程,避免多个线程同时操作同一个连接。我在系统里加了一个简单的单连接复用,实测下来稳定很多。
界面上显示检测结果的表格,如果视频流一直跑,表格会不断加行,内存占用涨得很快。我限制表格最多显示200条记录,超出之后自动删除最旧的行。这个小逻辑不写的话,长时间运行的系统会越用越卡,最终内存溢出。
登录注册界面的UI设计,如果条件允许,尽量加点企业元素或项目标识。这跟技术无关,但客户感官会好很多。之前我做的一个版本,界面布局只是简单地堆了输入框和按钮,客户反馈"不够专业"。后来重新排了版,加了Logo区、标语区、分割线,同样的功能,评价完全不同。做项目交付的人应该懂这个感受。
模型目录和代码目录最好分开。权重文件动辄几十上百MB,跟源码堆在一起提交版本控制会让仓库非常臃肿。我习惯建一个单独的weights目录,用.gitignore忽略掉,只在文档里记录训练命令和权重下载方式。
现在回过头看,这个项目麻雀虽小五脏俱全:从数据标注清理、YOLOv11训练调参、推理封装、报警逻辑、PyQt5界面到登录注册、打包发布,每一个环节都值得单独写篇文章展开。如果你正在做类似的安全帽检测系统,我建议严格按照"数据先行、模型其次、工程兜底"的顺序推进。很多人一上来就调模型,参数热心调了半天,最后发现数据太烂,效果上不去,又回头补数据,白白浪费一周时间。先把数据集整理干净,模型训练其实是整个项目中最省心的部分。