
简介基于YOLOv8的智能结算系统是一套面向高校人工智能、深度学习方向课程设计与期末大作业的完整源码包适合需要快速搭建可演示项目的学生参考。整个压缩包共56个文件以Python脚本、UI界面、图片素材及说明文档为主涵盖14个py、2个ui、20个png、4个jpg、4个md、3个pdf、3个html等类型包体仅7.87MB目录包括业务处理、实时识别、数据库管理、界面交互等模块结构清晰、便于本地部署。项目已获导师指导并被评为97分的高分作品从登录注册、商品结算到YOLOv8目标检测识别均形成完整链路配有数据标注规则、环境搭建、模型训练等文档下载后无需修改即可直接运行也可作为课程设计答辩的完整示例。目前已有136人学习浏览对希望快速落地深度学习应用或完成课设任务的高校学生有参考价值。1. 基于YOLOv8的智能结算系统解决的到底是什么问题基于YOLOv8的智能结算系统解决的是这样一个现场问题顾客把商品放到结算台上摄像头拍一下屏幕上自动列出商品明细、数量和总价不需要扫码也不需要人工录入。它的核心是把YOLOv8当作视觉感知层从视频流里识别出画面中有哪几类商品、各自在什么位置再由一层业务逻辑把这些检测结果换算成订单金额。作为期末大作业这个选题的优势在于不需要海量数据和强算力5到10类商品就能跑通全流程模型可以用官方预训练权重做微调视觉部分由ultralytics框架兜底真正需要自己动手的是数据集组织、跟踪去重和计价逻辑。如果你有Python基础想弄明白模型训练完以后怎么变成一个能用的系统这个项目是很好的切入点下面的内容就是照着这个思路把每一步走通。2. 数据与训练先让YOLOv8认识你的商品2.1 从零整理一份能训练的商品数据集结算场景有一个特点摄像头固定在结算台正上方或斜上方视角基本不变。因此数据采集不必追求全场景泛化重点是把同一类商品的不同摆放姿态、重叠遮挡和光照变化拍全。我一般每个类别拍250张以上少了容易在遮挡时漏检多了标注成本吃不消。标注工具用LabelImg或X-AnyLabeling导出格式选YOLO每张图对应一个同名txt文件每行是类别id x_center y_center width height坐标归一化到0到1。类别数量控制在5到10类期末项目做10类以上没必要训练时间和标注成本都会翻倍。数据集目录结构要固定成ultralytics能识别的样子datasets/shop/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/data.yaml写清楚路径和类别名注意names的顺序就是模型输出类别id的顺序后面计价要依赖这个映射path: ./datasets/shop train: images/train val: images/val nc: 5 names: [cola, sprite, instant_noodles, mineral_water, snack]这里有个容易踩的坑train和val不要有重合图片否则val loss会失真答辩时mAP很漂亮现场演示却漏检。划分时我习惯用脚本按9比1随机分配并校验每个类别在val里都出现过至少一次类别缺失会让对应商品的mAP显示为0。2.2 训练命令与YOLOv8的C2f结构环境配置上ultralytics要求Python 3.8以上和PyTorch 1.8以上CUDA版本和torch对不上时训练会直接报错装之前先用nvidia-smi确认显卡驱动版本。框架本身安装很简单训练也封装成了一行命令pip install ultralytics yolo detect train \ datadatasets/shop/data.yaml \ modelyolov8s.yaml \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience20 \ workers4 \ ampTrue初次运行建议用yolov8s.yaml从零训练而不是直接拿yolov8s.pt继续微调。原因是商品数据集的分布和COCO差异很大从零训练能让模型完全适配你的商品如果时间紧用yolov8s.pt迁移学习也可以epochs砍到60就够。关键参数按这张表调参数推荐值说明modelyolov8s.yamln/s/m按显存选6G显存选s最稳imgsz640结算台商品是中近景640够用480可提速batch8~16显存不足优先减batch不要减imgszpatience20验证集指标连续不涨就早停ampTrue混合精度能省不少显存为什么YOLOv8适合这类任务要落到它的网络结构上。YOLOv8骨干网络中的C2f模块Cross Stage Partial with 2 convolutions and k splits会把前一层的特征图分成两路一路直接传递另一路经过两次卷积后再切成k段逐段处理最后把所有分支拼接起来。相比旧版C3C2f让梯度在短路径和长路径之间都有流通通道深层网络不容易出现梯度消失对中小目标的特征保留也更好。结算场景里商品尺寸差别不大但互相遮挡导致的特征残缺很常见C2f这种多分支拼接的设计在遮挡条件下比单纯加深卷积更可靠。YOLOv8的P3、P4、P5三个尺度特征层分别负责小、中、大目标。商品检测主要靠P3和P4P5防止商品贴近镜头时出现极端大框。理解这一层后面调imgsz和conf时就不会瞎试。2.3 用损失函数曲线判断训练是否到位训练完成后runs/detect/train目录下会生成results.png里面画了train和val的box_loss、cls_loss、dfl_loss三条损失曲线以及precision、recall、mAP50、mAP50-95。这张图是期末答辩最直接的训练效果证明。我一般看三个点val/box_loss和val/cls_loss在训练后半段不再下降并开始震荡说明模型已收敛继续跑只增加过拟合风险如果val loss某个epoch后明显反弹而train loss还在降就是过拟合该看早停而不是硬跑满epochsmAP50在0.85以上对5到10类的结算任务足够演示mAP50-95掉到0.6以下不用焦虑结算台场景对精确框的要求没那么高训练完拿best.pt做一次冒烟测试yolo predict modelruns/detect/train/weights/best.pt \ sourcedatasets/shop/val/images/cola_001.jpg \ conf0.45 \ iou0.5 \ saveTrue单张图框得准再往下走如果漏检多先回头查标注框是否贴边、类别是否混淆不要急着调模型结构。提示训练时设固定seed比如在启动命令前加torch.manual_seed(42)答辩时能复现同一份结果避免现场训练效果对不上报告里的图。3. 从检测框到结算金额系统分层与计价逻辑3.1 为什么要把检测和结算拆成两层拿到源码包第一件事是看目录结构。常见做法是分三个模块detect负责模型推理track负责目标跟踪billing负责计价。很多初学者把价格计算直接写进推理循环里表面省事但模型换版本、商品调价、加折扣规则时都要改推理代码。拆开后detect层只输出每一帧里有哪些商品、框在哪billing层只管这些商品多少钱各改各的。另外要把YOLOv8的类别ID和商品名之间建立一套查表关系。模型输出的cls是一个整数比如0代表cola这个编号对应data.yaml里names列表的下标。计价模块拿这个下标去查商品信息而不是直接拿字符串去匹配解析速度更快也避免中英文混用带来的编码问题。3.2 商品与价格的映射别把价格写死在代码里价格表单独放一个文件用SQLite或JSON都行。用SQLite的好处是改价和加商品不用动代码重新启动程序就生效CREATE TABLE goods ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, price REAL NOT NULL ); INSERT INTO goods (name, price) VALUES (cola, 3.5), (sprite, 3.0), (instant_noodles, 4.5), (mineral_water, 2.0), (snack, 5.5);项目初始化时把价格表读进内存import sqlite3 def load_price_table(db_pathgoods.db): conn sqlite3.connect(db_path) cur conn.execute(SELECT name, price FROM goods) table {name: price for name, price in cur.fetchall()} conn.close() return table读进内存是因为结算台查询频率高每帧都查SQLite没必要。注意类别名必须和data.yaml里names完全一致这是检测层和计价层之间的接口协议拼错一个字符就会出现识别出了商品但查不到价格的情况。3.3 重复计数与遮挡处理跟踪ID是去重的关键结算台最核心的问题是摄像头一直在拍同一个商品在被拿走前会出现在几十帧里如果每一帧都累加数量一瓶可乐能算成几十瓶。解决办法是引入目标跟踪。ultralytics封装了ByteTrack每一帧会给每个目标分配一个track_id同一个商品在连续帧里ID一直一样。计价逻辑改为首次见到某个track_id时加一次数量之后这个ID反复出现都忽略当ID从画面消失超过N帧认为商品已被拿走从活跃列表里清除。这套首次计一次、消失后释放ID的机制在答辩时几乎是必问点能讲清楚说明你真正理解了系统。遮挡是另一个常见问题。两个商品叠在一起时检测框会互相干扰置信度下降、类别可能跳变。处理思路有三个置信度阈值不要设太高0.35到0.45之间跟踪时用IOU关联前后帧的框而不是单独看每一帧对跳变的类别做多帧投票连续三帧里出现次数多的类别作为最终结果。这些都不用改模型纯逻辑层面就能解决大部分遮挡误判。价格计算本身不复杂def settle(items, price_table): detail [] total 0.0 for name, count in items.items(): unit price_table.get(name, 0.0) subtotal count * unit detail.append((name, count, unit, subtotal)) total subtotal return detail, total商品类别由模型决定数量由跟踪层决定单价由价格表决定三个来源互不干扰这就是结算系统的主干。4. 核心代码实现推理、跟踪与结算怎么串起来4.1 封装推理引擎模型加载一次就够了不要在主循环里反复初始化。ultralytics的YOLO类加载权重后有算子编译过程放循环里会掉帧。from ultralytics import YOLO class DetectEngine: def __init__(self, weightsruns/detect/train2/weights/best.pt, conf0.45, iou0.5, device0): self.model YOLO(weights) self.conf conf self.iou iou self.device device def detect(self, frame): # track开启后boxes里额外带track_id results self.model.track( frame, persistTrue, confself.conf, iouself.iou, verboseFalse, ) if results[0].boxes.id is None: return [], [], [] boxes results[0].boxes return boxes.id.int().tolist(), \ boxes.cls.int().tolist(), \ boxes.conf.tolist()track方法必须配persistTrue才能跨帧保持ID连续如果某一帧没有检测到目标boxes.id会是None必须先判空再取属性否则直接报TypeError。conf和iou的取值有讲究conf调高精度高但容易漏检遮挡商品调低漏检少但会把背景里的倒影、logo误判成商品。我习惯先设0.45演示时如果发现某个类频繁漏检单独给这个类降阈值而不是整体调低。4.2 结算计数器跟踪ID驱动数量统计from collections import defaultdict class SettlementCounter: def __init__(self, price_table, hold_frames20): self.price_table price_table self.hold_frames hold_frames self.track_alive {} # track_id - 剩余存活帧数 self.billed_track set() # 已经计过价的track_id self.quantities defaultdict(int) def update(self, track_ids, class_ids, class_names): current_ids set() for tid, cid in zip(track_ids, class_ids): current_ids.add(tid) self.track_alive[tid] self.hold_frames if tid not in self.billed_track: name class_names[cid] self.quantities[name] 1 self.billed_track.add(tid) # 对不在当前画面里的ID做倒计时到期后释放 expired [] for tid in self.track_alive: if tid not in current_ids: self.track_alive[tid] - 1 if self.track_alive[tid] 0: expired.append(tid) for tid in expired: self.track_alive.pop(tid) self.billed_track.discard(tid)hold_frames是需要实测的参数商品被拿走后跟踪器可能还会残留几帧的ID倒计时太短会导致同一商品短暂消失又被识别成新商品计两次价太长会导致上一个商品还没清干净、下一个商品放进来时ID串位。固定视角下我一般取15到20帧场景越复杂这个值越大。4.3 结算输出小票、日志与主循环import time def settle_and_print(quantities, price_table): detail [] total 0.0 for name, count in sorted(quantities.items()): unit price_table.get(name, 0.0) subtotal unit * count detail.append((name, count, unit, subtotal)) total subtotal print( 结算单 ) for name, count, unit, subtotal in detail: print(f{name:12} x{count:3} ¥{unit:6.2f} ¥{subtotal:.2f}) print(f合计¥{total:.2f}) with open(orders.log, a, encodingutf-8) as f: f.write(f{time.strftime(%Y-%m-%d %H:%M:%S)} f{len(detail)}类商品 合计¥{total:.2f}\n) return detail, total这个输出可以替换成任何上层展示命令行小票、Flask返回JSON给前端大屏、或写入SQLite订单表。替换边界很清楚detect和counter完全不用动。主循环只有三行import cv2 cap cv2.VideoCapture(0) class_names load_price_table().keys() # 实际应从data.yaml读names engine DetectEngine() counter SettlementCounter() while cap.isOpened(): ret, frame cap.read() ids, cls_ids, confs engine.detect(frame) if ids: counter.update(ids, cls_ids, class_names) # 叠加绘制检测框并显示按q退出 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()主循环里不要放任何耗时操作比如写数据库、打印完整订单这些放到结算动作触发时做。实时视频流里每帧只做推理和计数界面展示用另一个线程刷新否则帧率会掉到没法看。5. 优化与排错把期末演示跑稳5.1 GTX 1660 Ti 这类 6G 显存卡的参数取舍GTX 1660 Ti跑yolov8simgsz640、batch16在数据量小的情况下可以开amp凑合但显存吃紧时优先把batch降到8。推理端如果嫌慢固定输入尺寸导出TensorRT或ONNX都有明显收益帧率能翻倍。演示场景优先保帧率imgsz降到480、conf提到0.5延迟基本能压到人眼无感。如果还想做yolov8 head改进方向上的创新点常见做法是把检测头换成轻量解耦头或加P2小目标检测层但改完要重新训练和验证时间不够别动结构。5.2 边缘设备部署要注意的转换问题后续要上RK3588这类边缘设备的话优化思路完全不同。RK3588的NPU不能直接跑PyTorch模型需要先导出ONNX再转RKNN。转换时最容易出问题的是算子和动态输入尺寸导出时固定imgsz640能减少失败概率。C2f这类多分支结构在NPU上支持得不错但训练时的loss计算在NPU上不支持要放到后处理里做。从FP32转到INT8量化精度一般掉1到3个点conf阈值要相应调低0.05到0.1。5.3 常见报错排查报错信息原因处理CUDA out of memorybatch或imgsz超出显存减batch、开amp、换yolov8nlabel no img, img no label标注文件和图片文件名不一致检查train/val目录是否成对value bbox not found标注坐标越界或格式错误用脚本校验txt中坐标在0到1之间Model loading time过长首次加载权重要编译算子初始化后先warmup一帧再进主循环训练前设固定seed保证答辩时复现同一个结果。演示时除了摄像头实时画面建议提前录制一段固定摆放流程的视频作为备用输入现场出问题时切到视频源重放比现场调试更体面。本文还有配套的精品资源点击获取