
简介基于YOLOv8与PyQt5的课堂检测系统完整实现面向教育技术研究者、计算机视觉初学者及有课堂管理需求的学校/机构解决传统人工课堂监控效率低、无法实时量化学生行为的问题。系统利用YOLOv8目标检测算法进行高精度实时推理通过PyQt5搭建可视化操作界面用户无需编程基础即可完成参数配置、实时画面查看与行为结果分析适用于出勤统计、专注度识别及异常行为预警等场景。资源包共2000个文件压缩后约720.34MB以txt文本文件为主1975个另有11个Python脚本、7个XML配置、6个YAML模型配置及1个CSS样式文件分别覆盖数据集标注转换、模型训练配置、界面样式与主程序逻辑等环节。随包附带的工具脚本覆盖数据集格式转换、划分与预处理流程有助于理解从数据准备到模型应用的整体逻辑。目前已有109人学习/下载适合需要参考完整项目结构、快速上手YOLOv8PyQt5开发或希望将AI课堂行为分析落地到实际教学场景的开发者。1. 课堂行为检测系统从 YOLOv8 模型到 PyQt5 界面的全貌期末答辩前一周有学弟发消息问一套能自动数学生举手次数、能识别玩手机和睡觉的课堂检测系统到底怎么落地我拆完这套资源后的回答是三个东西配套才好用。YOLOv8 负责从视频帧里实时框出学生并判断行为类别PyQt5 负责把识别结果装进图形界面让你不用盯着终端输出看数字资源里还带了一份已经按 YOLO 格式整理好的学生行为数据集想自己重新训练也有数据可喂。适合三类人做课设、毕设要拿完整 demo 的在校生给竞赛或教改项目搭原型的老师以及想熟悉 YOLOv8 加 PyQt5 整套联动写法的开发者。需要提前泼冷水的也有一处所谓无需编程基础指的是跑起来之后用鼠标点按钮初次部署仍然要装 Python 环境、改配置文件、执行训练脚本命令行这关绕不开。接下来按数据→训练→界面→排错→部署这条线说透。2. 学生行为数据集YOLO 格式标注规范和验收脚本绝大多数课堂检测项目翻车的第一站不是模型是数据。标注乱套、类别编号对不上、坐标范围错位模型训练得再久也是白搭。这套系统里带的学生行为数据集已经按 YOLO 格式整理好但你要把它用明白得先知道里面是什么结构、能怎么改、怎么快速验收。2.1 资源包里的三个模块权重、数据集、界面代码资源包根目录下分三层。模型层放训练好的 best.pt 权重以及 yolov8n.pt 预训练权重前者让你开箱即用直接跑推理后者用来做迁移学习重新训练数据层是整理好的学生行为数据集train 和 val 已按常见惯例做了 8:2 划分图片是 jpg、标注是同名 txt界面层是整套 PyQt5 工程源码入口是 main.py项目结构大致如下classroom_detection/ ├── models/ │ ├── best.pt │ └── yolov8n.pt ├── datasets/ │ └── classroom/ │ ├── train/ │ │ ├── images/ │ │ └── labels/ │ └── val/ │ ├── images/ │ └── labels/ ├── ui/ │ ├── main_window.py │ └── detect_thread.py ├── train.py └── main.py理解这个结构有个关键点best.pt 是训练产物yolov8n.pt 是起点。你直接跑 main.py 能出界面用的是 best.pt但如果想把检测类别改成自己学校特有的场景比如实验室安全行为就得靠数据集重新训练。两层权重都放在包里就是为了让先跑起来和再自己训两条路都走得通。单独拿 best.pt 跑课堂监控检测的是举手、阅读这类通用课堂行为准确率在这个数据集覆盖的场景下表现不错。但是换一个教室、换摄像头角度框就开始飘。这就是为什么要强调数据集的价值它是给你留的后悔药别嫌占磁盘空间。2.2 行为类别怎么定八类还是六类直接影响标注工作量这套资源里常见做法是识别八类课堂行为举手、阅读、写字、趴桌、困惑、专注、玩手机、睡觉。类别定得细能覆盖教学评估里的大部分需求代价也很直接——标注成本高且 confusion 和 focus 这两类在视觉上边界模糊手动标注时最容易产生噪声标签。如果你只是做答辩 demo我会建议砍掉困惑和专注用六类就够。class_id类别名对应场景0raise举手回答/提问1read低头阅读教材2write书写笔记3bored趴桌、发呆4confused困惑表情边界模糊易误标5focus专注看黑板易和 read 混淆6play_phone低头玩手机7sleep睡觉改类别不是改个名字的事。你在 labelimg 或 Roboflow 里把类别列表改了重新标注导出后还得同步改 classroom.yaml 里的 nc 和 names 字段。nc 是类别总数names 的索引顺序必须和标注文件里的 class_id 严格一致顺序错了训出来的模型会把猫叫成狗。我见过不止一个人在这个地方翻车训练时 loss 降得很漂亮一测全是错位的。另外要提醒一句sleep 和 bored 这类类别天然样本少因为课堂监控视频里真正睡觉的学生占比不高。类别不平衡会导致小类别 mAP 特别低后续训练时可以考虑给这类样本做水平翻转、亮度扰动或者干脆把两类合并成非学习状态。2.3 标注格式与验收YOLO txt 的五个字段和一段检查脚本YOLO 格式的标注是每张图片配一个同名 txt 文件每行五个字段类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。最容易出问题的是归一化三个字——坐标是相对于图片宽高的比例取值范围 0 到 1不是像素坐标也不是左上角加宽高的那种 VOC 格式。从 VOC 转过来的人常犯的错误是把像素值直接填进去模型训练时坐标全飞到图片外面。拿到数据集第一件事不是训练是验收。我一般会先跑一段脚本检查标注文件完整性和坐标合法性import os from pathlib import Path root Path(datasets/classroom) for split in [train, val]: img_dir root / split / images lbl_dir root / split / labels print(f 检查 {split} 集 ) for img_path in sorted(img_dir.glob(*.jpg)): label_path lbl_dir / (img_path.stem .txt) if not label_path.exists(): print(f[缺失标注] {label_path}) continue with open(label_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] for line in lines: parts line.split() if len(parts) ! 5: print(f[字段数错误] {label_path}: {line}) break try: cx, cy, w, h map(float, parts[1:]) except ValueError: print(f[非数值坐标] {label_path}: {line}) break if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): print(f[坐标越界] {label_path}: {line}) break print(f{split} 集检查完成)这段脚本的逻辑很简单遍历 train 和 val 下的所有 jpg检查同名 txt 是否存在逐行解析五个字段验证坐标是否都在 0 到 1 区间内。跑完没有输出基本可以说明标注文件在格式层面没问题。但格式合法不代表标注贴框。我习惯再抽十张图用 OpenCV 把标注框画出来人工看一眼重点检查框是否包住人的上半身而不是只套住一个头。课堂行为检测的标注基准一般是头肩区域加上手部动作范围纯头部框会让 play_phone 和 read 这类依赖手部位置的行为完全分不开。这一步不要省数据质量直接决定后面所有环节的上限。3. 训练与推理YOLOv8 参数设置与损失曲线解读数据集验收通过进入正题。这章我会把训练入口的每个参数讲清楚再给两种推理写法最后教你怎么从 results.csv 里判断模型是不是真的收敛了。很多初学者拿到代码就改 epochs 然后硬跑跑完也不看曲线这属于把唯一的调试手段扔了。3.1 训练入口与核心参数yolo train 一行命令背后的设置资源里 train.py 本质上是调用 ultralytics 的训练接口核心是 model.train 这一段。直接放能跑的版本import torch from ultralytics import YOLO def main(): # 有 NVIDIA GPU 就用 0 号卡没有就退回 CPU device 0 if torch.cuda.is_available() else cpu # 用 COCO 预训练权重做迁移学习收敛速度远快于随机初始化 model YOLO(yolov8n.pt) model.train( dataclassroom.yaml, # 数据集配置文件 epochs100, # 最大训练轮数 imgsz640, # 训练图片分辨率 batch16, # 每批图片数显存小就调小 devicedevice, # 计算设备 workers4, # 数据加载线程数 lr00.01, # 初始学习率 patience20, # 连续 20 轮 val loss 不降则早停 ) if __name__ __main__: main()参数里最需要理解的是四个。imgsz640 是速度与精度的折中点学生行为检测依赖手部和头部细节640 够用上 1280 精度提升有限但帧率掉一半以上不建议上来就拉高。batch 受显存约束GTX 1660Ti 这种 6GB 显存的卡跑 YOLOv8n batch 可以到 16换 YOLOv8s 就得降到 8后面避坑章节会细说。lr0 在迁移学习场景下用 0.01 合适如果是从零训练这个值偏高容易震荡。patience 是替你兜底的loss 不降就停省电省时间。配套的 classroom.yaml 里面内容不多但一行都不能错path: datasets/classroom train: train/images val: val/images nc: 8 names: [raise, read, write, bored, confused, focus, play_phone, sleep]path 是数据集根目录相对路径train 和 val 指向图片目录ultralytics 会自动去同级 labels 目录找标注。nc 和 names 必须和 2.2 节说的类别定义一致这里写 8你的标注里 class_id 最大就只能是 7出现 8 训练会直接报错。提示训练前先确认数据集路径没有被中文或空格污染yaml 解析对路径里的特殊字符容忍度很低报错信息又不直观这个坑我踩过一次卡了半个多小时。3.2 推理的两种写法一次性识别和视频流处理训练完得到 runs/detect/train/weights/best.pt推理写法分两种。第一种是跑离线视频或图片集适合批量验证from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcedemo.mp4, # 支持图片、视频、文件夹 conf0.5, # 置信度阈值调低召回高但误检也多 saveTrue, # 保存带框输出 imgsz640, )predict 方式的好处是省事saveTrue 会自动生成 annotated 的图片和视频适合批量处理一整个文件夹的素材。conf 这个参数要注意0.5 是通用值但课堂场景里学生互相遮挡严重目标框的置信度普遍偏低我实际用下来 0.4 更合适低于 0.3 就会把路过的人误判成学生。第二种是实时视频流处理咱们的资源核心场景就是摄像头实时检测PyQt5 界面里跑的也是这套逻辑import cv2 from ultralytics import YOLO model YOLO(best.pt) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame cap.read() if not ret: break # frame 直接传给模型内部会自动处理预处理和后处理 results model(frame, imgsz640, conf0.4, verboseFalse) annotated results[0].plot() # 画好框的帧 cv2.imshow(classroom_detect, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里 model(frame) 返回的 results 是一个列表每个元素对应一帧的检测结果。results[0].plot() 把框、类别名、置信度一次性画到帧上。verboseFalse 很关键不开的话终端会刷屏每帧打印检测日志长时间运行终端会爆。这段代码里我没做任何多线程处理纯命令行演示够用但到 PyQt5 界面里必须改造原因下一章细讲。3.3 从 results.csv 画损失曲线判断模型是否真的收敛训练结束后runs/detect/train 下会生成 results.csv里面逐行记录了每个 epoch 的 box_loss、cls_loss、dfl_loss、mAP50、mAP50-95 等指标。直接从 CSV 看数字不直观我习惯画成曲线图一眼就知道模型有没有收敛、有没有过拟合。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) df.columns [col.strip() for col in df.columns] # 列名首尾有空格去掉 cols [train/box_loss, val/box_loss, metrics/mAP50(B), metrics/mAP50-95(B)] fig, ax plt.subplots(2, 2, figsize(12, 8)) for idx, col in enumerate(cols): row, col_idx divmod(idx, 2) ax[row][col_idx].plot(df[col], linewidth1.5) ax[row][col_idx].set_title(col) ax[row][col_idx].grid(True) plt.tight_layout() plt.savefig(loss_curves.png, dpi150)这段代码做了三件事读取 results.csv把列名里的空格剥掉然后在一个 2×2 的画布里分别画 train 和 val 的 box_loss、mAP50、mAP50-95 曲线。divmod(idx, 2) 负责把四个子图排成两行两列。怎么判断曲线是健康的看两个点。第一train/box_loss 和 val/box_loss 应该同步下降并在某个 epoch 后趋于平缓如果 train 还在降而 val 开始反弹说明过拟合应该调小 epochs 或加大数据增强。第二mAP50 曲线如果在前 30 轮还在明显上升说明训练不够让 patience 继续跑如果 mAP50 已经平了但 mAP50-95 还在爬说明模型定位精度还在提升可以多训几轮。只看 mAP50 不看 mAP50-95 是常见的误判方式前者衡量框选得准不准后者衡量框和真实标注的重合度课堂行为检测对框的贴紧程度要求高两个指标都要关注。4. PyQt5 界面集成从视频流到行为统计面板的完整链路数据集能训、权重能推理剩下就是把这一切装进图形界面。很多人在这一步把系统做成了命令行里能跑但没人敢点的半成品。PyQt5 这层要解决的核心问题不是画界面而是怎么让摄像头帧、模型推理、UI 刷新三者不互相卡死。4.1 界面模块拆解视频区、统计表和控制按钮各归其位课堂检测系统的图形界面按使用流拆分四个区域左侧大面积放摄像头预览右侧放实时行为统计表底部放开始、停止、导出报告三个按钮顶部状态栏显示模型加载状态和当前帧率。布局上用 QVBoxLayout 套 QHBoxLayout左侧视频区占窗口 70% 宽度右侧统计表占 30%。from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget, QTableWidgetItem class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(课堂行为检测系统) self.resize(1280, 720) central QWidget() self.setCentralWidget(central) layout QHBoxLayout(central) left QVBoxLayout() self.video_label QLabel(等待摄像头开启) self.video_label.setMinimumSize(640, 480) left.addWidget(self.video_label) layout.addLayout(left, stretch7) right QVBoxLayout() self.table QTableWidget(8, 2) self.table.setHorizontalHeaderLabels([行为类型, 人数]) right.addWidget(self.table) self.btn_start QPushButton(开始检测) self.btn_stop QPushButton(停止检测) right.addWidget(self.btn_start) right.addWidget(self.btn_stop) layout.addLayout(right, stretch3)这段代码搭建了最基础的骨架。video_label 用来贴视频帧QTableWidget 用来刷新统计数字。布局里 stretch7 和 stretch3 是关键参数控制左右区域宽度比视频区太小看不清统计区太大浪费。按钮先创建出来信号槽连接放后面。资源里实际界面会比这个花哨但剥掉样式跑的核心结构就是这一套。4.2 用 QThread 隔离推理别让检测卡死界面如果把 3.2 节的 while 循环直接塞进按钮的槽函数里点击开始检测后窗口立刻白屏整个界面变成未响应。原因很简单QThread 之外的代码都跑在主线程而 Qt 的事件循环被 while 循环阻塞界面刷新、按钮点击、窗口拖动全部停摆。这是 PyQt5 做实时视频最经典的翻车点必须把耗时操作挪到子线程。import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class DetectionThread(QThread): frame_ready pyqtSignal(dict) # 把帧和统计结果一起发出去 def __init__(self, model_path, source0, parentNone): super().__init__(parent) self.model YOLO(model_path) self.source source self.running True self.statistics {} def run(self): cap cv2.VideoCapture(self.source) if not cap.isOpened(): self.frame_ready.emit({error: 摄像头打开失败}) return while self.running: ret, frame cap.read() if not ret: break results self.model(frame, imgsz640, conf0.4, verboseFalse) boxes results[0].boxes annotated results[0].plot() # 统计每个类别的目标数量 self.statistics {} if len(boxes) 0: cls_ids boxes.cls.tolist() names results[0].names for cls_id in cls_ids: name names[int(cls_id)] self.statistics[name] self.statistics.get(name, 0) 1 # 转成 QImage 交给主线程显示 rgb_image cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape qimg QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit({ image: QPixmap.fromImage(qimg.copy()), statistics: self.statistics, }) cap.release() def stop(self): self.running False self.wait()这个类有三个设计点。第一QThread 的 run 方法里做全部耗时操作读帧、推理、画框都在子线程主线程只负责接收信号。第二frame_ready 信号用 dict 打包数据一张 QPixmap 和一份统计字典同时传出去避免写多个信号。第三stop 方法把 running 置为 False 后调用 wait()确保线程安全退出直接在 closeEvent 里粗暴 terminate 会造成段错误。有个细节必须注意QImage 构造时传的是 rgb_image.data但如果这张图在信号发射后被主线程引用底层数据可能被 Python 垃圾回收。所以我在 emit 之前调用 qimg.copy()把像素数据复制一份防止界面显示花屏或崩溃。这个坑排查起来非常隐蔽问题不在推理在 PyQt 对象生命周期。4.3 信号槽把检测结果刷进统计面板子线程把数据发出来主线程槽函数负责刷新界面。这是整个系统的最后一公里class MainWindow(QMainWindow): def __init__(self): super().__init__() # ... 省略 4.1 的界面初始化代码 ... self.detect_thread DetectionThread(best.pt, 0, self) self.detect_thread.frame_ready.connect(self.update_ui) self.btn_start.clicked.connect(self.detect_thread.start) self.btn_stop.clicked.connect(self.detect_thread.stop) def update_ui(self, data): if error in data: self.statusBar().showMessage(data[error]) return pixmap data[image].scaled( self.video_label.size(), aspectRatioMode1, # 保持宽高比不变形 ) self.video_label.setPixmap(pixmap) stats data[statistics] names [raise, read, write, bored, confused, focus, play_phone, sleep] for row, name in enumerate(names): self.table.setItem(row, 0, QTableWidgetItem(name)) self.table.setItem(row, 1, QTableWidgetItem(str(stats.get(name, 0)))) def closeEvent(self, event): self.detect_thread.stop() event.accept()update_ui 里做了两件事把子线程传来的 QPixmap 缩放后贴到 video_label然后把统计字典刷新进表格。scaled 的第二个参数 aspectRatioMode1 保持图片比例如果不加摄像头 16:9 的画面会被强行拉伸成 QLabel 的尺寸人脸全变形检测框也跟着视觉错位。按钮连接用的是 clicked.connect(self.detect_thread.start)注意 QThread 继承自 QObjectstart 是它的内置方法clicked 信号会触发线程启动。停止按钮同理。closeEvent 里调用 stop 并 accept确保关窗口时线程先退出否则 Python 解释器退出时子线程还在读摄像头经常报 cv2.error 或者直接崩溃。到这里整套系统已经从摄像头像素流转换成了界面上不断滚动的统计数字中间隔着一个信号槽的桥梁链路完整闭合。5. 避坑与常见问题五条踩坑记录与修复方法这套系统我拆的时候踩了不少坑挑五个最常见的记录在下面。每条都按现象→原因→解决写前两条是最多人问的后三条属于那种一晚上才能定位的玄学问题。5.1 一开摄像头界面就白屏卡死现象点击开始检测按钮后窗口立刻白屏标题栏出现未响应过几十秒后系统提示强制关闭。原因推理循环直接写在了按钮的槽函数里。我在 4.2 节强调过Qt 的界面刷新依赖事件循环而 while 循环读摄像头会霸占主线程不放事件循环永远得不到执行机会。从外面看就是界面死了实际是 CPU 在疯狂跑推理。解决把摄像头读取、模型推理、画框全部挪到 QThread 子线程主线程只保留 setPixmap 和 setItem 这类极轻量的 UI 操作。如果你实在不想引入线程另一个折中方案是用 QTimer 每 30 毫秒触发一次抓帧但推理耗时超过间隔时 QTimer 会堆积回调卡顿依旧。正解就是线程加信号槽没有第二条路。5.2 训练完 mAP 很低但 loss 正常现象训练日志里 box_loss 从 2 降到 0.4曲线非常漂亮但验证集 mAP50 只有 0.3 出头界面上框乱飞。原因loss 下降只能说明模型在拟合训练集不代表标注本身是对的。最常见的三个问题第一是类别 id 从 1 开始标而 yaml 里从 0 开始所有类别整体错位第二是图片和 txt 文件名对不上模型学到的是图 A 的标注对应图 B 的内容第三是数据集里混入大量没有目标的空图片模型被迫把背景学成目标。解决先跑 2.3 节的验收脚本确认每个 txt 都存在且类别 id 合法。然后看 runs/detect/train 下的 confusion_matrix.png这张混淆矩阵图会告诉你哪些类别互相污染。如果 sleep 类大量被识别成 bored说明这两类在视觉上确实接近检查标注框是不是没区分趴桌睡觉和趴桌发呆必要时合并类别。最后确认数据集里空图片比例超过 5% 就删掉空图会让模型产生幽灵框。5.3 import ultralytics 直接报错现象pip install ultralytics 显示安装成功但代码里 import 时报缺 numpy 依赖或 torch 版本冲突重装之后又报新的错。原因系统里装了多个 Pythonpip 装进 A 环境代码跑在 B 环境。更隐蔽的是 ultralytics 对 numpy 和 opencv 的版本范围有要求你手动升级过的 numpy 2.x 和它不兼容import 时在 C 扩展层直接炸掉。解决用 venv 建独立环境Python 版本用 3.10不要用 3.12部分依赖尚未完全适配。建完环境后按顺序装pip install torch torchvision再 pip install ultralytics最后 pip install pyqt5 opencv-python。装完先跑一句 import torch; import ultralytics; print(ultralytics.version) 确认环境干净再跑完整推理。不要偷懒在系统 Python 里直接装血泪教训环境混装的问题排查起来比写代码还耗时。5.4 检测画面颜色发蓝发绿现象摄像头画面偏色人脸的肤色变成青绿色或者检测框位置和实际物体偏移几个像素。原因OpenCV 读到的帧默认是 BGR 三通道而 YOLOv8 内部预处理按 RGB 处理。如果你在推理前手动做过某些转换又在显示前少转了一次颜色就会错位。PyQt 的 QImage 默认 Format_RGB888要求输入 RGB直接喂 BGR 数据就会看到蓝绿互换的诡异画面。解决统一颜色处理链。读取帧保持 BGR推理时直接传给 YOLO它会内部转画框后的 annotated 也是 BGR此时做一次 cv2.COLOR_BGR2RGB 转成 RGB 再交给 QImage。别在推理前后做多次转换每转一次就多一次出错机会。颜色对了框的位置自然跟着对——颜色通道颠倒不影响框坐标但影响你肉眼看画面后续手动标框和调试时会非常别扭。5.5 训练时显存溢出现象训练到第 20 轮左右终端报 CUDA out of memory进程直接被杀前面训的全部白费。原因batch 和 imgsz 同时开太大。GTX 1660Ti 这种 6GB 显存的卡跑 YOLOv8n 时 batch16 是极限换 YOLOv8s 显存翻倍不降 batch 必爆。如果开了多线程数据加载每个 worker 也占额外显存。解决三个参数联动调。batch 从 16 降到 8imgsz 从 640 降到 480worker 从 4 降到 2。ultralytics 默认开启 AMP 混合精度显存占用能省三分之一确认训练日志里没有显示 ampFalse。还炸就换模型yolov8n 已经是 nano 版再小只能剪枝。不要幻想用 CPU 训练替代100 轮的训练量 CPU 跑三天三夜GPU 两小时该升级硬件就升级实在没有 GPU 就租云实例几百张图的数据集也不用斥巨资。6. 进阶技巧导出 ONNX 并部署到 RK3588系统在 PC 上跑通只是第一步。很多读者拿到这套资源后问能不能部署到 RK3588 这类带 NPU 的边缘开发板上让检测不依赖电脑。这条路是通的但中间要过模型转换这一关。先看单张测试图from ultralytics import YOLO model YOLO(best.pt) results model.predict(sourcetest_classroom.jpg, conf0.4, saveTrue) print(results[0].speed) # 查看预处理/推理/后处理耗时在 PC 上确认模型没问题后导出 ONNXyolo export modelbest.pt formatonnx opset12 imgsz640导出的关键参数是 opsetRKNN 工具链对 opset 有兼容范围要求12 是稳妥值。导出后不要在 PC 上验证 ONNX 精度就完事RK3588 的 NPU 走的是 int8 量化从 FP16 到 int8 精度会掉 1 到 3 个点。量化用的校准图片要从训练集里抽不要用随机图片否则分布偏差会让量化后的模型在真实场景里误报暴增。我之前贪省事用二十张网图做校准部署后 conf0.5 阈值下框全飘后来才发现是把校准集给省没了。部署后验证时我会用一个固定视频做回归测试记录三组数据单帧推理延迟、mAP50、实际画面里的误报数。PC 上 GPU 跑 30 FPS 不代表 NPU 上不掉帧RK3588 的 int8 推理对遮挡场景更敏感conf 阈值从 0.4 回调到 0.45 到 0.5 之间观察一下。如果板子上只是为了跑推理服务PyQt5 界面可以留在上位机通过局域网把检测结果推过去这样界面开发不用改板子压力也小。我头一回做 RKNN 量化时没核对工具链版本板子端加载模型直接报版本错折腾一晚上才发现是 rknn-toolkit 和板端驱动不匹配。从那以后我每次转模型前都先把版本号写进 README转换完立刻在板子上跑一轮回归测试再收工。希望帮到你。本文还有配套的精品资源点击获取