简介:基于YOLOv8的智能会议室参会人数统计项目,面向计算机相关专业学生、老师及企业员工,可作为毕业设计、课程设计、大作业或项目初期立项演示完整方案。压缩包共8个文件,涵盖3个Python源码(模型训练配置、可视化交互界面、视频实时检测脚本)、3个PyTorch权重文件(yolov8n、yolo11n、best)以及2个说明文档,整体仅15.91MB,轻量易传输。代码均经过测试运行成功,训练过程可生成核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图,覆盖标准目标检测评估全流程,便于答辩评审与效果展示。项目附带完整数据集与部署教程,可视化界面支持直观统计会议室人数,修改配置即可适配不同场景,简单部署即可复现实验。目前已有34人学习下载,适合目标检测入门、毕设开发及快速搭建演示系统的场景。
1. 从“数人头”到“懂会场”:一个毕设项目能走多远
会议室人数统计这件事,听起来简单,真正落地过的人都知道它有多少坑。灯光忽明忽暗、人挨着人坐、后排被前排挡住、低头看手机只露个头顶——每一帧画面都在考验检测模型的泛化能力。基于YOLOv8的智能会议室参会人数统计,把目标检测、人数去重、可视化界面和离线部署串成一条完整链路,正好覆盖了毕业设计最需要的几块拼图:算法能讲清楚、代码能跑通、界面能演示、数据集能复现。适合正在做毕设和课程设计的学生,也适合想快速在本地验证YOLOv8工程能力的入门开发者。它的价值不在于“数得准”这一个点,而在于你能从数据标注走到模型部署,完整走一遍目标检测项目的所有环节——这一步走完,后续换场景、换数据集、换硬件,都是在已有骨架上填肉。
2. 为什么是YOLOv8:会议室人数统计的模型选型逻辑
2.1 YOLOv8在检测精度和部署友好度之间取了一个平衡点
会议室人数量统计的首选目标不是“多准”,而是“稳”。参会者可能站着、坐着、走动,姿态变化大,遮挡频繁,这对检测器的召回率提出了比一般场景更高的要求。YOLOv8相比更早的YOLOv5,主要改动集中在C2f模块替换了C3模块,用anchor-free的Decoupled Head替换了原来的耦合检测头,这两处改动直接让模型在拥挤场景下的小目标召回率有了可见提升。对于会议室这种中等密度场景,YOLOv8s在COCO上的mAP 50-95大约能跑到44左右,而推理速度在GPU上维持实时完全没问题;即使是纯CPU环境,把输入尺寸压到640以下,也能做到1-3 FPS的可用水平。
很多人纠结要不要直接上YOLOv9或YOLOv10,我的看法是:如果这是毕设或课程设计,稳定跑通、资料多、换数据集方便比追求那零点几个点的mAP重要得多。YOLOv8最大的优势是Ultralytics生态把训练、验证、导出、部署全链路打通了,你不需要自己写数据加载器,不需要手撸NMS,连可视化界面都能直接拿官方predict的results来画框。这意味着你可以把主要精力放在“人数统计逻辑”和“界面交互”这些更有区分度的部分,而不是在底层调试上耗尽时间。
用Ultralytics加载一个预训练模型只需要几行代码:
from ultralytics import YOLO # 加载预训练权重,yolov8s.pt 是体积与精度的折中选型 # n/s/m/l/x 对应参数量递增,会议室场景 s 已够用 model = YOLO("yolov8s.pt") # 跑一张图看效果,save=True 会把标注结果存到 runs/detect/ 下 results = model.predict( source="demo_meeting.jpg", conf=0.25, # 置信度阈值,会议室低一点更不容易漏人 iou=0.45, # NMS 的 IoU 阈值,重叠框多时可以适当调高 save=True, classes=[0] # COCO 数据集中 0 是 person 类,只保留人 ) print(results[0].boxes.cls) # 打印每个框的类别 ID print(len(results[0].boxes)) # 这一帧检测到的人数这里classes=[0]是一个很多人不知道但很关键的参数。COCO数据集有80个类别,如果不加限制,模型会把显示器、椅子误判成别的目标,白白消耗算力;只保留person类,既减少误检,又让统计逻辑更干净。conf=0.25在会议室场景下通常是合理的起始值,如果发现漏检多可以再降到0.1,但如果开视频流,太低的conf会让闪烁框变多,后续做去重时要小心。
2.2 从单帧检测到人数统计:跨帧去重才是核心
模型输出的只是“这一帧里有几个人”,但会议室人数统计需要的是“这个会议室里一共有几个人”。这两者的距离,就是跨帧去重的算法差距。最简单也最常用的做法是“帧间IoU匹配”:维护一个当前活跃目标列表,每来一帧就把新检测框与上一帧的框做IoU计算,IoU超过阈值的认为是同一个人的延续,没匹配上的新框作为新人加入列表;如果一个目标连续超过N帧没被匹配到,就把它从活跃列表里移除。这个逻辑不需要任何额外依赖,一个字典加一个距离函数就能实现。
也有项目会用ByteTrack或DeepSORT做更正式的多目标跟踪,精度确实更好,但代价是引入了卡尔曼滤波和外观特征提取,调参成本翻倍。对于毕设和课程设计,帧间IoU方案完全够用,而且你能在论文里把它作为“基于交并比的轻量级跨帧跟踪策略”来讲,逻辑自洽、代码量小、答辩时不怕被追问细节。下面是一个最小实现的思路:
class PersonTracker: def __init__(self, iou_threshold=0.3, max_age=5): self.iou_threshold = iou_threshold self.max_age = max_age # 连续丢失多少帧后移除该目标 self.tracked = {} # key: 目标ID, value: (bbox, age) self.next_id = 0 def update(self, boxes): # 新帧的 box 与已有目标做 IoU 匹配 matched_ids = set() for box in boxes: best_id, best_iou = None, 0.0 for tid, (tbox, _age) in self.tracked.items(): iou = compute_iou(box, tbox) if iou > best_iou: best_iou = iou best_id = tid if best_id is not None and best_iou >= self.iou_threshold: self.tracked[best_id] = (box, 0) # 重置年龄 matched_ids.add(best_id) else: self.tracked[self.next_id] = (box, 0) matched_ids.add(self.next_id) self.next_id += 1 # 未被匹配的目标 age +1,超过阈值则移除 for tid in list(self.tracked): if tid not in matched_ids: _box, age = self.tracked[tid] age += 1 if age > self.max_age: del self.tracked[tid] else: self.tracked[tid] = (_box, age) return len(self.tracked)这段代码里的compute_iou(box, tbox)你可以自己写,两个框的交集面积除以并集面积,十行以内搞定。iou_threshold=0.3是个经验值,如果摄像头俯视角度大、人走动频繁,可以调到0.2;如果是侧面平视、人基本坐着不动,0.4更好。max_age=5表示一个目标连续5帧没被检测到就认为它离开了,在25 FPS的输入下这等于0.2秒,响应足够快;如果摄像头帧率低到5 FPS,max_age应该升到10以上,否则人低头看手机几帧没被检测到就会被误删。
3. 完整数据集准备:从零标注到训练前的边界处理
3.1 会议室数据集的两条路:现成可下载与自行标注
标题里提到的“完整数据集”,在会议室人数统计场景下通常是COCO person类的一个子集,或者是从公开数据集(如CrowdHuman、MOT Challenge)里截取的室内人员画面。用现成数据集的优势是省时间,模型训练出来泛化能力有保障,适合只想快速跑通流程的课程设计。但如果你的毕设需要体现“自己做的工作”,我建议至少自行标注一部分以本校会议室为背景的数据,哪怕只有两三百张,也能显著提升模型在你实际部署环境下的表现。
自行标注最常见的是用labelme,导出格式是JSON多边形标注。YOLOv8训练需要的是YOLO格式的TXT文件,每行“类别ID x_center y_center width height”,坐标是相对图片宽高的归一化值,所以需要写一段转换脚本。任何一个从labelme转YOLO格式的人都踩过坐标归一化的坑,这里直接给出可行做法:
import json import os from PIL import Image def labelme_to_yolo(json_path, img_dir, out_dir, class_names=["person"]): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_path = os.path.join(img_dir, data["imagePath"]) img_w, img_h = Image.open(img_path).size txt_name = os.path.splitext(os.path.basename(json_path))[0] + ".txt" lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_names: continue 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],且中心坐标是相对图片宽高 x_center = ((x_min + x_max) / 2.0) / img_w y_center = ((y_min + y_max) / 2.0) / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h # 边界值裁剪,防止浮点误差导致坐标越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines))这段脚本里最容易翻车的是json_path里的imagePath字段。labelme保存的JSON里,imagePath有时是相对路径,有时只有文件名,如果直接用os.path.join(img_dir, data["imagePath"])找不到图片,改用os.path.join(img_dir, os.path.basename(data["imagePath"]))就稳了。另一个坑是标注的边界坐标碰到图片边缘时,归一化后会得到略大于1的值,YOLO训练时会把超出边界的框当作错误样本,这就是代码里加了min/max裁剪的原因。
3.2 数据划分与增强参数:会议室场景的三个特定设置
数据划分用Ultralytics会自动处理,只要按下面的目录结构放好就行。images/和labels/下都分为train和val两个子目录,YOLOv8不要求固定的训练验证比例,但建议验证集至少留10%到15%的图片。会议室场景的验证集尤其要包含几种困难样本:窗边强逆光的画面、人坐在椅子上只露出半身的画面、多人密集坐在长桌两侧的画面。如果这些场景在验证集里一个都没有,训练时模型loss很漂亮,一到真实会议室就露馅。
dataset/ ├── images/ │ ├── train/ # 训练图片,建议800张以上 │ └── val/ # 验证图片,建议100-200张 ├── labels/ │ ├── train/ # 每个图片名对应的txt标注 │ └── val/ └── data.yaml # 数据集配置文件data.yaml里关键是这几行:
path: dataset # 数据集根目录,建议用绝对路径 train: images/train val: images/val nc: 1 # 类别数:只有 person 一类 names: ['person']path字段用相对路径在换机器跑的时候容易出问题,我一般直接写成绝对路径,省得排查“找不到数据集”的玄学问题。nc和names要严格对应,classes数量写错会直接报错或者训练出无法解释的loss曲线。训练时可以开启hsv_h、hsv_s、hsv_v的颜色增强,对会议室灯光偏黄或偏冷的情况有帮助;flipud翻转变换建议关掉,因为正常摄像头安装不会倒置,上下翻转会让模型学到错误的语义。
4. 训练参数与会话可视化的落地实现
4.1 yolov8s在会议室数据上的训练命令与关键参数
训练这一步是最能体现“参数怎么设”的环节。会议室场景的图片数量通常不大,几百张到一千多张,因此训练轮数、批次大小、图像尺寸都跟大规模数据集的标准配置有区别。直接给一个可复制的命令:
yolo detect train \ model=yolov8s.pt \ data=/absolute/path/to/dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=8 \ lr0=0.01 \ patience=20 \ device=0 \ project=meeting_count \ name=exp1几个参数说清楚。epochs=100代表训练100轮,会议室数据集小,通常50到100轮就会收敛,多了反而容易过拟合。batch=8是显存只有6GB左右时的安全值,如果你的显卡是16GB显存可以开到16甚至32。lr0=0.01是初始学习率,Ultralytics默认就用这个值,小数据集上不需要调低。patience=20表示验证集指标连续20轮不提升就提前停止,这能避免你挂机睡觉时模型在过拟合的边缘反复横跳。device=0指定用第一块GPU;没有GPU就改成device=cpu,代价是训练时长拉长10倍以上。
训练完成后,项目里会出现一个runs/detect/exp1目录,里面有weights/best.pt和weights/last.pt。best.pt是按验证集指标选出的最优权重,部署时用它;last.pt是最后一轮的权重,一般只用来看过拟合程度。用best.pt做推理,跟预训练权重一样简单:
model = YOLO("runs/detect/exp1/weights/best.pt")4.2 用PySide6做一个会议人数可视化界面:线程分离是命门
标题里提到的“可视化界面”,常见做法是PySide6加OpenCV,左半边显示检测视频流,右半边显示实时人数曲线和进出记录。界面本身不复杂,真正的命门是线程模型。如果你把检测放在GUI主线程里跑,视频流一卡,整个窗口就假死,这是所有“界面能跑但一开摄像头就转菊花”案例的根源。
下面的写法是经过多次翻车之后沉淀下来的:
import cv2 import sys from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget from PySide6.QtCore import QThread, Signal, Qt from ultralytics import YOLO class DetectThread(QThread): frame_ready = Signal(object) # 把处理后的帧传给主线程显示 count_updated = Signal(int) # 把当前人数传给主线程更新标签 def __init__(self, model_path, source=0, parent=None): super().__init__(parent) self.model = YOLO(model_path) self.cap = cv2.VideoCapture(source) self.running = True def run(self): while self.running: ret, frame = self.cap.read() if not ret: break results = self.model.predict(frame, conf=0.3, classes=[0], verbose=False) boxes = results[0].boxes.xyxy.cpu().numpy() # 这里可以调用上一章的 PersonTracker 对 boxes 做跨帧去重 count = len(boxes) for box in boxes: x1, y1, x2, y2 = box.astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) self.frame_ready.emit(frame) self.count_updated.emit(count) def stop(self): self.running = False self.cap.release() class MainWindow(QWidget): def __init__(self): super().__init__() self.label = QLabel("当前人数: 0") self.video_area = QLabel() layout = QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.video_area) self.setLayout(layout) self.thread = DetectThread("runs/detect/exp1/weights/best.pt", source=0) self.thread.frame_ready.connect(self.update_frame) self.thread.count_updated.connect(self.update_count) self.thread.start() def update_frame(self, frame): rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, _ = rgb.shape from PySide6.QtGui import QImage, QPixmap qimg = QImage(rgb.data, w, h, 3 * w, QImage.Format_RGB888) self.video_area.setPixmap(QPixmap.fromImage(qimg)) def update_count(self, count): self.label.setText(f"当前人数: {count}") if __name__ == "__main__": app = QApplication(sys.argv) win = MainWindow() win.show() sys.exit(app.exec())关键点有三处。第一,DetectThread继承自QThread,检测循环放在run()里,不阻塞GUI。第二,跨线程传数据用Signal,千万不要在子线程里直接改QLabel,那会触发Qt的线程安全异常。第三,cv2.imshow在子线程里一样会卡顿和闪退,正确做法是把帧转成QImage发给主线程的QLabel显示。新手最容易犯的错是直接拿cv2.VideoCapture在主线程里循环读帧,一拖窗口拖动就卡成PPT。
5. 避坑指南:会议室人数统计里最容易翻车的五个细节
5.1 现象:训练时loss下降正常,但真实会议室效果极差
原因:训练集和验证集都来自同一个固定场景,模型泛化到新会议室时灯光、布局、摄像头视角全变了。解决:训练集中至少混入三分之一“陌生场景”图片,哪怕是从COCO val里抽出来的室内人物图,让模型见过足够多的亮度分布和拍摄角度。
5.2 现象:窗口逆光下检测框大量丢失,一到下午就失灵
原因:会议室靠窗位置强光导致人脸和衣服纹理过曝,YOLOv8对低对比度区域的特征提取退化。解决:在数据增强里打开hsv_v(亮度增强)选项,值设在0.4左右;更直接的做法是推理时对帧做一次自适应直方图均衡(CLAHE),代码就一行cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(gray),加在predict之前。
5.3 现象:帧间IoU去重逻辑看起来没问题,但人数反复横跳
原因:摄像头视角较低或人坐得密集时,框的位置在帧间抖动较大,固定IoU阈值匹配不上同一人。解决:把iou_threshold从0.3降到0.2,同时给每个目标增加“最近一次匹配框”的缓存,用这个缓存框而不是最新帧的原始框去参与下一帧的匹配,能平滑掉大部分抖动。
5.4 现象:用CPU跑推理,界面卡到无法接受
原因:YOLOv8s在CPU上单帧推理耗时约0.3到1.2秒,直接在视频循环里串行处理当然卡。解决:把imgsz从640降到480,conf从0.25升到0.4,推理时间能缩短一半以上;如果还不行,把模型换成yolov8n并在导出时做INT8量化,CPU上能到10-15 FPS。
5.5 现象:训练时显存占用爆炸,OOM直接中断
原因:batch和imgsz两个参数组合导致单批数据过大。解决:会议室数据集图片分辨率往往很高,Ultralytics默认会自动做letterbox缩放,但如果你自己改了预处理把大图直接喂进去,6GB显存必炸。还原到imgsz=640,batch=4起步,OOM时优先降batch而不是降imgsz,因为图像尺寸变化会影响检测精度。
6. 进阶验证:用量化导出和场景压测确认方案能不能扛住
训练完模型、界面也跑通了,最后一件值得做的事是把模型从PyTorch导出为ONNX并做一次INT8量化。这不仅是为了部署到更低成本的设备,更重要的是验证整个方案在脱离训练环境后依然稳定。导出命令非常简单:
yolo export model=runs/detect/exp1/weights/best.pt format=onnx opset=12 dynamic=True导出的best.onnx可以用onnxruntime直接加载推理,不需要Ultralytics环境。你可以在界面代码里把YOLO("best.pt")换成YOLO("best.onnx"),对用户来说无感,但你的项目就多了一个“支持边缘设备部署”的亮点,答辩时很加分。
最后做一个最朴素的压测:录制一段5分钟的真实会议室视频,里面有人进出、有人低头、有人走动,跑一遍完整流程,统计三个指标——漏检率、误检率、人数稳定时间(从进入画面到被计入当前人数所需帧数)。如果漏检率超过5%,先调颜色增强;如果人数稳定时间超过30帧,降低max_age。这一套验证做完,你的项目就不再是“能跑”,而是“可交付”。
我自己的习惯是永远在正式部署前一天用新环境从零跑一遍部署教程,因为项目“换台机器就起不来”的教训实在太多了。依赖版本、绝对路径、数据集路径这些事,只有在你换机时才会暴露。希望这篇笔记能帮你少走几段弯路,早点把会议室人数统计从“跑通”推到“好用”。
本文还有配套的精品资源,点击获取