简介:基于YOLOv8的智慧教室人数统计应用,是一份面向毕业设计和课程设计的完整项目,涵盖源码、可视化界面、完整数据集与部署教程,适用于计算机视觉、人工智能等专业学生及需要快速搭建目标检测演示的开发者。项目代码已经测试通过,支持生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果和标签分布图,能直观展示模型训练效果,为答辩评审提供可信依据。压缩包共8个文件,以Python脚本、PyTorch模型权重和说明文档为主,其中3个py文件对应训练、检测和可视化页面模块,3个pt文件为训练好的模型权重,2个txt文件包含使用说明与资源标识信息,压缩后总大小约15.91MB。目前已有58人学习下载,可拿来即用,也可在现有代码基础上修改以扩展功能,适合作为毕设、课设或初期立项演示项目。
1. 这个项目在解决什么:一个教室、一台老电脑,也能跑起来的人数统计
很多教室这两年装的所谓智能考勤,本质还是三种老方案:人肉点名、红外传感器数进出、或者用贴纸二维码打卡。人肉点名慢,红外只知道有人没人,二维码只能说明人到了,不能说明教室里此刻到底有多少人。而在摄像头画面上做实时人数统计,用 YOLOv8 已经不算新技术,可一旦真把它放进一间没有独立显卡、只有老 CPU 的教室电脑里跑起来,还要带一个能点按钮看结果的可视化界面,就会撞上数据、训练、界面、部署这四堵墙。这个题目拆开来说,就是把智慧教室人数统计应用从数据集整理、模型微调、界面开发到部署踩坑完整过一遍。适合正在纠结毕设能不能这么做、课程设计怎么交差的学生,也适合想在教室场景快速验证一套人数统计原型的工程师。先提醒一句:你网上看到的很多指标和参数,在这个场景下要改一半。
2. 先想清楚三件事:为什么是 YOLOv8、交付物是什么、数据集从哪来
2.1 为什么是 YOLOv8:与 Faster R-CNN、SSD 在教室场景的对比
做目标检测,能选的开源框架很多,但教室这个场景有自己的特殊性:目标密集、后排人头小、光照变化大、摄像头位置基本固定。早期我见过有人用 Faster R-CNN 做教室考勤,精度确实过得去,但训练和推理都慢,CPU 上单帧推理近一秒,完全没法做实时统计;SSD 速度快一些,但对小目标的召回率差,后排学生坐在那里,框出来的往往是半个身子而不是一个人。
| 方案 | 推理速度(CPU) | 后排小目标 | 部署生态 | 二次开发成本 |
|---|---|---|---|---|
| Faster R-CNN | 慢,约 0.5s/帧 | 较好 | 一般 | 高 |
| SSD | 中等 | 差 | 一般 | 中 |
| YOLOv8 | 快,约 80~150ms/帧 | 好 | 完善 | 低 |
对比下来,YOLOv8 的优势不在某个单项指标上,而在综合体验。它原生是 anchor-free 结构,对尺度变化不敏感,教室摄像头拍到的画面里,第一排学生占画面高度三分之一,最后一排学生可能只有 30 像素,这种尺度跨度正是 anchor-free 的舒适区。更重要的是 ultralytics 这个包把训练、验证、导出、推理封装成一条命令,部署环节省掉大量体力活。我选它的核心理由就是:毕设或者小工程里最怕的不是模型效果差,而是卡在环境配置和格式转换上,YOLOv8 在这方面的生态成熟度目前没有对手。
2.2 一个能毕业的项目骨架:目录结构、训练脚本、界面、部署文档
拿到这类标题的资源,第一件事不是跑代码,而是先看项目的目录骨架。常见做法是一个工程文件夹里分四块:数据集、训练脚本、可视化界面、部署文档。即使你没拿到别人的包,自己也应该按这个结构组织,后面每一步操作都有地方放东西。
classroom_count/ ├── datasets/ # 数据集:图片、标签、划分好的 train 和 val │ └── classroom/ │ ├── images/ │ ├── labels/ │ └── data.yaml ├── runs/ # 训练输出:权重、曲线图、验证结果 ├── src/ │ ├── train.py # 训练入口 │ ├── detect.py # 单张图片 / 视频推理 │ └── ui_main.py # PyQt5 可视化界面 ├── docs/ │ └── deploy.md # 部署教程 └── requirements.txt命令和说明我都写在这段里,你可能注意到数据集、界面、部署文档这三样恰好就是这类应用的基本盘。训练脚本一般独立成文件,因为训练和推理的环境依赖不同,后续做模型改进时也不会污染界面代码。界面文件单独放,理由在后面讲线程时会说明。部署教程放到独立目录,等你训练完模型、在另一台机器上重新搭环境时,省得翻聊天记录和旧笔记。
2.3 把原始照片变成训练集:Labelme 标注与 VOC 转 YOLO 格式脚本
提示:数据集是整个项目里最不值钱也最值钱的部分。不值钱是因为教室图片网上就能拍,值钱是因为标注质量直接决定模型上限,后面所有的调参都不能弥补标注错误。
我一般标注用 Labelme,安装和启动命令就两行,启动后把图片目录拖进窗口,用多边形把每个人框出来。这里有个常见做法:像素级精细标注没有必要,检测框用矩形就够,类别统一填 person。
pip install labelme labelme标注完每张图片会生成一个同名 JSON 文件,里面存的是多边形坐标。但 YOLOv8 要的标签是归一化后的中心点坐标和宽高,存在与图片同名的 .txt 文件里,所以需要一个转换脚本。下面这段是我在多个项目里复用过的,关键逻辑是读取 JSON 里的形状坐标,算出最小外接矩形,再除以图片宽高得到归一化值。
import json import os def convert_labelme_json(json_path, img_w, img_h, out_txt): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) lines = [] for shape in data['shapes']: if shape['label'] != 'person': 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) # 转成 yolo 格式:中心点 x, y, 宽, 高,都要除以图片尺寸 cx = (x_min + x_max) / 2.0 / img_w cy = (y_min + y_max) / 2.0 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h # 防止裁剪时把坐标裁到图片外 cx = max(0.0, min(cx, 1.0)) cy = max(0.0, min(cy, 1.0)) w = max(0.0, min(w, 1.0)) h = max(0.0, min(h, 1.0)) lines.append(f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt, 'w') as f: f.write('\n'.join(lines))这段脚本里有三个地方值得说一下。第一个是类别索引固定写成了 0,因为当前只有 person 一类,如果后续加了 teacher 类别,就要改成从 0 开始的映射字典。第二个是坐标钳制的四行,很多人漏掉,结果标注目标有一半在画面外,训练时模型会被边缘的半个框干扰。第三个是归一化,YOLO 训练要求所有坐标在 0 到 1 之间,习惯先转归一化再做数据增强,顺序不要反。
2.4 数据增强该开多少:教室小目标场景下的四项建议
很多教程会教你开启所有增强来泛化,但智慧教室这个场景有别的问题:后排人头本来就小,如果用 mosaic 增强把四张图拼在一起,后排人头经常被拼接线切成两半,标签还在,模型学出来的特征就是“半个人头也算”。所以我在处理数据集用于 YOLOv8 训练时,会把默认增强改掉,具体建议如下:
- mosaic 从默认的 1.0 降到 0.5,让小目标被切碎的概率降低一半。
- 水平翻转开,垂直翻转不要开,教室里没有人倒立。
- 亮度、对比度扰动开到 0.3 左右,教室有晴天拉窗帘、阴天开灯的情况,这个扰动很有用。
- scale 增强要克制,默认的 0.5 可以保持,但上下不要超过 0.7,否则模型会把人缩小成噪点。
这里还涉及一个数据划分的问题:按整个数据集随机分 train 和 val,而不是按视频时间段分。同一段视频前后帧太相似,随机划分会把几乎相同的画面同时放进去导致验证结果虚高。我踩过一次这个坑,后面所有教室项目都改成按视频片段划分,先抽帧、再以片段为单位分配。
收集数据时还有一个反向技巧:故意采集十几分钟没有人的空教室画面,标注文件留空(也就是空 label),训练脚本会把这些当负样本。这个操作对降低误检非常有效,后面避坑章节会展开讲。
3. 训练自己的教室模型:环境搭建、命令模板与损失曲线解读
3.1 Ubuntu20.04 CPU 跑 YOLOv8:环境搭建与硬件边界
如果你手头只有一台 CPU 电脑,别急着买显卡,可以先把环境搭起来,跑通最小流程。以下命令在 Ubuntu20.04 上验证过,Python 版本建议选 3.10,因为 Ultralytics 对 3.8 以下的支持在逐渐弱化。
conda create -n yolov8 python=3.10 -y conda activate yolov8 pip install ultralytics opencv-python python -c "from ultralytics import YOLO; print('ok')"最后一行命令如果输出 ok,说明环境没问题。这里我要坦诚说一句:CPU 版本能跑,但你要接受它的上限。推理一张分辨率 640 的教室图片,用最轻量的 yolov8n 大约需要 80 到 150 毫秒,训练就另说了,CPU 上训 100 个 epoch 可能需要十几个小时,效率太低。常见做法是用云 GPU 或学校的服务器做训练,把训练好的 best.pt 拿回本地 CPU 机器做推理演示。我遇到过一个学生,用 CPU 硬训了两天一夜,结果因为忘记打开缓存,中途断电全部白干。所以训练环节,我给你两个救命的参数:在训练命令里加 cache=True,以及在 epochs 设置后加 patience 提前停止。
如果你有一块 GTX1660Ti 这类老显卡,那训练体验会好很多,6GB 显存跑 yolov8n 或 yolov8s 都够,batch 设 16 左右不会爆显存。
3.2 从预训练权重微调:一条可复现的训练命令与参数表
不要在 ImageNet 预训练模型上从零训,直接用 YOLOv8 自带的 COCO 预训练权重做微调,收敛快得多。COCO 里有 person 类,模型天生认识人,你只需要让它适应教室的视角和光照。训练命令如下:
yolo task=detect mode=train \ model=yolov8n.pt \ data=/home/user/classroom_count/datasets/classroom/data.yaml \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.001 \ cache=True \ patience=20 \ plots=True这里每个参数都不是摆设,挑五个关键的展开说。epochs 设 120,是因为教室数据集通常只有几千张,再多就会过拟合,patience=20 的意思是连续 20 轮验证指标不提升就提前停,多数情况下模型在第 60 到 90 轮就收敛了。imgsz 是输入分辨率,640 是默认均衡值,后排人头实在太小的时候可以试 960,但训练时间和显存占用逼近两倍,CPU 推理更是会明显变慢。batch=16 受显存限制,如果你的是 8GB 以下,可以降到 8 并同步把 lr0 降到 0.0005。lr0 这里选 0.001 而不是默认的 0.01,因为微调场景下学习率太大很容易让预训练权重瞬间崩掉,也就是常见的 loss 从 2 直接跳到 20 再回不来。
还有一点容易被忽略:data.yaml 里的路径最好写绝对路径,因为训练命令里中文路径或相对路径时,偶尔会出现数据集加载失败但报错不明确的情况。我一般写完 data.yaml 先跑一遍验证加载,确认类别数和图片数量是预期值再开训练。
3.3 训练监控:把 results.csv 画成损失函数曲线图
训练开始后,ultralytics 会在 runs/detect/train 下生成 results.csv,里面每一行是一个 epoch 的指标,包括 train/box_loss、train/cls_loss、metrics/mAP50、metrics/mAP50-95 等。看训练趋势不一定要用 TensorBoard,用 pandas 加 matplotlib 几行就能画出来。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") # 画 box_loss,观察是否收敛 plt.figure(figsize=(10, 4)) plt.plot(df["train/box_loss"], label="train box loss") plt.plot(df["val/box_loss"], label="val box loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.title("Box Loss Curve") plt.savefig("box_loss.png", dpi=150)这段代码读取 results.csv 并绘制训练集与验证集的 box loss 曲线。我要特别强调一种现象:如果 val/box_loss 在某个点之后开始掉头向上,而 train/box_loss 还在往下降,那就是过拟合了,不需要再调参,直接回到训练命令把 epochs 减半或者加大数据增强就能解决。mAP50 与 mAP50-95 的区别也要看懂:mAP50 只评估 IoU 大于 0.5 的预测框,符合人数统计的容错需求;mAP50-95 则把阈值从 0.5 一路提升到 0.95 取平均,指标更严格,但教室里框出头部中心点比框出完整身体更难,这项数值不用太焦虑。
3.4 训练不收敛时怎么定位:输出文件与现象对照
训练日志只能告诉你结果不好,不能告诉你为什么不好,这是标准的黑匣子。我的做法是优先看图,不看数字。
训练过程中开启 plots=True 会在 runs/detect/train 下生成三个关键文件:train_batch0.jpg、val_batch0_labels.jpg、val_batch0_pred.jpg。第一个文件展示了送入训练器的增强后图片,如果你看到图片里标注框明显偏移、或者有超过一半的框贴边,那基本可以断定是数据问题。第二个文件是验证集标注的真实框,用来看类别和坐标是否正确。第三个文件是模型预测结果,在训练的早期阶段,它通常是什么都框不出来的,这是正常现象,不用慌张。
如果这三个文件都正常但 mAP50 始终不到 0.6,下一步去 labels 目录里做一次统计,把每个标签文件的框数量打印出来,看看有没有某张图标注了 20 个人,而其他图都是两三个人。这种极端不均衡会让模型优化方向混乱,解决办法就是对类别做重采样,或者删掉标注数异常的图片。目标检测不像分类,超过画面承受能力的密集标注,模型往往会把边框学成了互相重叠的噪声。
4. 可视化界面与实时推理:PyQt5 骨架与置信度参数调校
4.1 推理前的最后一道闸门:conf、iou、imgsz 三个参数怎么设
界面这部分是这类项目能不能拿高分的分水岭。终端里跑通的人一抓一大把,稳定又美观的可视化界面才是亮点。但让人表演翻车的也正是在这里,问题通常不是模型不行,而是推理参数没调好。YOLOv8 推理时有三个参数我需要单独拧出来讲。
conf 是置信度阈值,默认 0.25。在教室场景里,后排学生只占 30 像素,模型给出的置信度普遍偏低,如果保持 0.25,后排会被大量漏检。我一般先调到 0.1 试跑一段视频,如果发现把角落的窗帘、黑板下沿误认成人,再往回升到 0.2 左右。目标检测追求召回,而人数统计更看重精确,宁可漏检也不要误检,所以 0.15 到 0.2 是教室场景的常见落点。iou 是 NMS 的阈值,控制两个重叠框是否合并,默认 0.7 在人群密集时会出现同一个学生被框两次的情况,降到 0.5 可以缓解。imgsz 在推理时可以和训练时不一致,CPU 上用 480 能明显提速,代价是后排小目标会更难识别,这一点看你的硬件取舍。
4.2 用一个 QThread 封装摄像头推流,避免界面卡死
PyQt5 界面新手最容易犯的错误,是把摄像头读帧和模型推理全塞进主线程。OpenCV 的 VideoCapture.read() 是阻塞的,等待摄像头返回一帧的时间不稳定,模型推理又要几百毫秒,两个阻塞操作叠加,界面就会表现为点一下按钮就整个卡住。解决方式很标准:把读帧和推理丢进 QThread,通过信号把结果传回主线程。
import sys import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget from ultralytics import YOLO class DetectThread(QThread): frame_ready = pyqtSignal(object) def __init__(self, model_path, source=0): super().__init__() self.cap = cv2.VideoCapture(source) self.model = YOLO(model_path) self.running = False def run(self): self.running = True while self.running: ret, frame = self.cap.read() if not ret: break # 推理,conf/iou 按 4.1 节的原则设置 results = self.model.predict(frame, conf=0.18, iou=0.5, imgsz=640) self.frame_ready.emit(results[0].plot()) self.cap.release() def stop(self): self.running = False逻辑不复杂:QThread 的 run 方法里循环读帧,在阻塞读帧的过程里模型推理也在工作线程中,主线程只负责接收处理完的画面并刷新 QLabel。这个代码里有一个需要特别注意的坑:不要在 run 内部直接修改任何 QLabel 或窗口控件的文本,Qt 的界面控件不是线程安全的,必须通过 frame_ready 信号传回主线程,并绑定到槽函数里再做 setPixmap。我还习惯在 stop 方法里加一个 running 标志而不是直接 terminate 线程,因为强制终止线程时摄像头资源经常来不及释放,下一次启动会提示摄像头被占用。
关闭窗口时也要做两步:先调 stop(),再调用 wait() 等线程真正结束,否则程序退出时偶尔会崩溃,报错信息还看不懂。
4.3 从“检测多少框”到“统计多少人”:取帧与均值平滑
模型输出的检测框数量不等于当前教室人数,因为摄像头一秒有 25 帧,如果每一帧都刷新人数,界面上的数字会跳个不停:这帧 27 人,下一帧 26 人,再下一帧 29 人。原因可能是某帧有人低头挡住了脸,或者画面上有人刚好叠在一起。
常见做法是维护一个长度为 30 左右的滑动窗口,对最近 N 帧的检测框数量做统计,取中位数而不是平均值。平均值的毛病在于一个突然的误检会把数字拉高一大截,过三四秒才恢复;中位数则对单帧异常有天然的抵抗力。
from collections import deque class PeopleCounter: def __init__(self, window_size=30): self.samples = deque(maxlen=window_size) def update(self, count): self.samples.append(count) sorted_samples = sorted(self.samples) return sorted_samples[len(sorted_samples) // 2]这个窗口大小 30 对应约一秒的实时画面,数字既不会频繁跳动,又能在一两秒内真实反映教室内的人员变化。这里有一个取舍:窗口越短,反应越快,但对遮挡和瞬间出入的噪声越敏感;窗口越长,数字越稳定,但人已经走出教室,数字还停在原值。教室人数统计通常不会要求毫秒级响应,我一般用的是 30 帧,你觉得卡顿感太强可以放到 15。
4.4 离线测试脚本,先让逻辑跑通再连摄像头
可视化界面的调试如果直接对着摄像头会非常痛苦,因为画面内容不受你控制,问题难以复现。所以我在做界面之前,一定会先有一个离线视频测试脚本,把录好的教室视频喂进去,同时输出标注后的视频文件。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_video.mp4", conf=0.18, iou=0.5, save=True, save_txt=False, show=False, )这段代码会把检测结果保存为一个新的 mp4 文件,在 runs/detect/predict 目录下。用离线脚本的好处是参数调整的结果可以反复对比:同样的视频、不同的置信度,一帧一帧看,误检漏检清清楚楚。等离线结果满意了再接入 UI,界面开发时碰到的就只是界面问题,不会和模型问题搅在一起。
5. 避坑记录:五次线上翻车与对应的处理方案
5.1 现象:CPU 推理只有 2 FPS,教室一节课都数不完
原因:用了一个别人打包的模型,里面对应的 imgsz 是 1280,而且模型本身是 yolov8m,CPU 跑这个组合基本就是幻灯片。还有一个隐藏问题:代码在每一帧都重新加载了一次模型,相当于每帧执行一次模型初始化。
解决:把模型换成 yolov8n 的 best.pt,imgsz 降到 640,一旦降到 640 还不够快,就再降到 480。推理前把模型对象创建在循环外部,只在程序启动时加载一次。我在 CPU 机器上的经验是:yolov8n + imgsz=640 约 3 FPS,imgsz=480 约 5 FPS,能勉强做到一两秒更新一次人数,这对统计场景已经完全够用,不一定非要 30FPS 实时。
5.2 现象:模型把窗帘当成人,半小时报一次 120 人
原因:训练集里全是有人坐着的教室照片,模型没学过“没有人但教室还在”的画面。拉升的窗帘有波浪褶皱,颜色和肤色接近,很容易成为误检来源。
解决:回到数据集,加拍两三百张没有人的空教室照片,标签全部为空。训练脚本会自动把空标签当负样本。我当时为了快速见效,在推理参数里直接把 conf 从 0.18 提到 0.35,误检确实少了大半,但后排学生也被滤掉了不少。真正的解法还是负样本,这个坑提醒我:目标检测的数据集不仅要包含要找的目标,还要包含容易混淆的干扰物体。
5.3 现象:训练 loss 一直不降,mAP 停在 0.5 左右不动
原因:检查了 labels 文件夹,发现标注框的类别索引写的是 1,而 data.yaml 里类别数写的是 2。为什么会有这种乌龙?因为转换脚本里 hardcode 过类别 0,后来有人想加 teacher 类,改成从 1 开始,却忘了 person 应该还是 0。
解决:训练前打印 labels 目录下的任意三个 txt 文件,第一列只能是 0。同时打开 images 目录里的训练图片,标注框必须紧紧包住人而不是包住半个桌子。这个坑的本质是工具链里多个脚本对类别定义不一致,解决方式是在转换脚本里写一个类别映射字典,之后只能通过这个字典修改,不要到处 hardcode。
5.4 现象:点下“开始检测”按钮界面立刻无响应,只能强杀进程
原因:按钮的点击槽函数里直接调用了摄像头读取,摄像头初始化失败时会一直阻塞等待,界面主线程被卡死。这是 PyQt 开发里最典型的新手问题。
解决:把摄像头初始化和读取全部放到 QThread 里,主线程只通过信号接收结果。另外还要注意摄像头的 index:笔记本自带摄像头通常是 0,USB 摄像头可能是 1 或 2,如果摄像头 index 错误,程序不一定报错,但会表现为黑屏或者长时间无响应。我的习惯是提供两个入口参数,界面加一个下拉框选择摄像头编号,省得用户反复改代码。
5.5 现象:换一间教室立刻失效,白天晚上表现天差地别
原因:训练集只在一间教室采集,而且全部是白天开灯的数据。模型把那个教室的墙角、黑板颜色、窗帘光照特征当成了一部分“人”的特征,换教室就泛化失败。
解决:收集数据时至少去三个不同的教室,覆盖白天、黄昏、夜晚灯光三种光照,还要采集投影幕布开启和关闭两种状态。如果项目进度不允许,可以在推理前对画面做自适应亮度均衡,OpenCV 的 CLAHE 能缓解一部分光照差异。另外在 YOLOv8 的训练增强里把 hsv_h、hsv_s、hsv_v 各调到 0.02 左右,也能让模型不那么依赖颜色信息。
6. 进阶:从“能跑”到“可靠”,用这三个手段验证再交差
6.1 先做离线视频回放,把 mAP 拆成漏检和误检
mAP 是一堆框的综合指标,但教室人数统计真正要回答的问题是:屏幕上的数字和真实人数差多少。我的做法是录三段 10 分钟以上的教室视频,人工数出每一帧的真实人数,和模型输出做对齐,算出平均绝对误差。这个数字比 mAP 更能说服答辩老师,也比单纯的效果演示可信。
6.2 ONNX 导出与边缘设备部署,给毕设答辩加一个亮点
如果你的目标是让这个应用更接近工业落地,可以再做一步:把 best.pt 导出成 ONNX 格式,再在 RK3588 这类带 NPU 的边缘设备上做部署。
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12导出后的 ONNX 模型可以用 onnxruntime 直接推理,也可以再接 RKNN-Toolkit 转成 NPU 能跑的格式。这里有个血泪经验:转 INT8 量化后,教室后排的小人头精度会掉得很明显,mAP50 可能下降 0.1 到 0.3,如果现场演示时发现小目标大量漏检,优先检查量化校准数据集是否包含了夜间画面。边缘部署的工程量比跑通一个电脑端界面要多一倍,但它能把项目从“课程设计”提升到“可交付产品”的质感。
结尾说一个我自己的习惯:每次调完一次模型,我会把训练命令、数据集改动、推理参数变化和由此带来的效果差异记在项目根目录的更新日志里。这个习惯帮我救回过很多次回退决策,也让我在被问到“你这个效果怎么复现”时,能几秒钟就给出完整链路。依赖记忆做项目,是最贵的项目。希望这篇笔记能帮你在做 YOLOv8 智慧教室人数统计时少走几段弯路。
本文还有配套的精品资源,点击获取