YOLO+PyQt5实现超市商品识别:从选型到落地的完整方案
2026/8/26 11:18:56 网站建设 项目流程

简介:目标检测是计算机视觉的核心任务,旨在从图像中定位并分类多个物体。YOLO作为单阶段检测算法的代表,一次前向传播即可完成定位与分类,凭借实时性和对小目标的良好支持,在工业场景中得到广泛应用。结合Python生态中的PyQt5与OpenCV,能够快速搭建具备图形界面的桌面视觉应用,支持图片、摄像头和RTSP视频流等多种输入源。基于这一技术组合,可以实现超市商品识别、自助结算等实用系统,解决传统图像处理在光照变化和相似外观区分上的痛点。本文从技术选型、环境配置到核心代码实现,完整梳理了一套可落地的工程方案,为开发者提供从模型训练到界面交互的实战参考。 做超市商品识别这个项目,说实话最开始我是被朋友拉去救场的。他们想做一个自助结算的原型机,找的几家外包报价离谱,而且交付的模型在真实货架上一测就露馅,小瓶饮料和洗发水经常分不清。后来这个项目落到我手上,我用YOLO+PyQt5重新搭了一套,图片、摄像头、RTSP视频流都能跑,界面也做成了带操作按钮的桌面程序,客户看了演示直接拍板。这篇文章就把整套方案的选型思路、核心代码、踩坑记录都摊开讲,给想快速落地同类项目的朋友一个参考。文章适合有Python基础、想了解YOLO实际工程落地或正在做桌面端视觉应用的开发者,新手也能跟着环境配置部分一步步跑起来。

1. 项目整体设计与技术选型思路

1.1 为什么超市商品识别选择了YOLO方案

超市商品识别这个场景,第一眼看过去好像不难,但真正做起来全是坑。货架上的商品密集摆放,同类商品不同口味的外包装高度相似,光照变化大,还有反光、遮挡、变形的问题。最典型的例子就是可乐和雪碧,瓶身形状一样,颜色一深一浅,传统图像处理靠颜色直方图去区分,换个灯光环境就废了。更别说同一个商品换个角度、被其他商品挡住一半,模板匹配类算法基本全部失效。

YOLO属于单阶段目标检测算法,一次前向传播同时完成目标定位和分类,速度快,特别适合超市这种需要实时反馈的场景。而且YOLO家族发展到现在,对小目标的检测能力已经有了很大提升,密集排列的饮料瓶、牙膏盒都不在话下。对比一下传统方案和YOLO方案的差异就很清楚了:

对比维度传统图像处理方案YOLO方案
目标定位需要手写特征+滑动窗口,复杂度高端到端回归边界框,天然支持多目标
光照鲁棒性对光照极其敏感,需大量预处理CNN特征对光照变化有较强适应力
相似外观区分纹理、颜色特征区分度不足可学习深层语义特征,区分相似SKU
实时性能多阶段串行,处理慢单阶段GPU加速,轻松跑实时
新增品类每增加一类都要重新设计特征只需补充标注数据,重新训练即可

人工设计特征的时代已经过去了,让模型自己从数据里学特征才是正解。YOLO在准确率和召回率之间能做到很好的平衡,推理速度也够用,是商品识别这类落地项目最稳妥的算法底座。

1.2 技术栈选型的真实考量

整套系统我选的是Python+YOLO+PyQt5+OpenCV的组合,这个组合不是随便凑的,每一步都有明确考量。

Python作为主语言,是因为整个深度学习生态它的支持最好。Ultralytics官方YOLO包、PyTorch推理框架、OpenCV图像处理库,全是Python优先支持,算法验证和工程落地的效率都很高。虽然Python在GUI性能和启动速度上不如C++,但对于商用原型机和中小型项目来说,开发效率的收益远大于这点性能损耗。

YOLO模型选用Ultralytics YOLOv8。需要注意一点,YOLOv8的Ultralytics版本使用的是AGPL-3.0协议,如果是商用项目,要么购买企业授权,要么就要认真评估合规风险。这个我在后面专门说。模型本身支持图片、视频流、摄像头等多种输入源,一键推理接口封装得很好,对快速交付非常友好。

PyQt5做桌面界面,看中的是它的成熟稳定和控件丰富程度。Qlabel显示图像、QPushButton绑定操作、QThread处理多线程,这套组合在工业视觉项目里已经被验证过无数次了。相比PySide6,PyQt5的文档和踩坑案例更多,遇到问题基本都能搜到解决方案。

OpenCV负责视频流的读取和图像预处理。VideoCapture配合多线程可以稳定拉取USB摄像头和RTSP网络摄像头的数据流,转成YOLO输入格式也方便。整个链路从图像采集到结果展示,用这四个开源组件就能完整覆盖。

1.3 商用落地前必须想清楚的三件事

技术方案能跑通只是第一步,真正商用落地还得想清楚三件事。

第一件是模型训练数据从哪来。超市商品SKU动辄几千个,每个品类的有效标注样本至少需要一两百张,这还不算同品类的不同包装、不同批次。实际项目中我通常先用公开数据集把模型预训练到能用的程度,再用真实货架照片做迁移学习。拍照的时候要注意覆盖不同光照、不同角度、不同摆放姿态,最好让客户提供多门店的实拍素材,这样模型的泛化能力才有保障。

第二件是推理硬件成本。用一个RTX 3060级别显卡跑YOLOv8s模型,一张图大约20到40毫秒,客户现场如果是普通办公电脑没有独立显卡,CPU推理就要慢很多。所以项目开始前一定要确认客户现场的硬件条件,如果没有GPU,要么选YOLOv8n这种轻量模型,要么建议客户加一块显卡,这两者的方案差距很大,事先不确认清楚后面会很难办。

第三件事是后续维护责任边界。商品包装更新换代是常态,模型部署上线之后,新增SKU、旧SKU下架,谁来负责数据更新和模型重训,都要在合同里写清楚。不然客户每周都有新品上架,全指望你免费迭代,这个项目就变成一个填不满的坑了。

2. 环境准备与基础依赖配置

2.1 从零搭建Python虚拟环境

不管你是Windows还是Linux机器,第一步都是创建一个独立的Python环境,千万别直接往系统Python里装一大堆包。我之前吃过亏,系统Python环境被装乱了,连pip都用不了,最后只能重装系统,那种痛苦希望你不要体验。

# 创建虚拟环境 python -m venv venv_supermarket # 激活虚拟环境 # Windows venv_supermarket\Scripts\activate # Linux/Mac source venv_supermarket/bin/activate

虚拟环境激活后,再安装依赖包。下面是我的requirements.txt,版本都经过实测验证,直接用不会出问题。

ultralytics==8.0.222 PyQt5==5.15.9 opencv-python==4.8.1.78 numpy==1.24.4 Pillow==10.0.0 torch==2.0.1 torchvision==0.15.2

提示:PyTorch的安装建议去PyTorch官网用CUDA匹配的命令安装,不要直接pip install torch。GPU版本的torch对显存利用率和推理速度的影响非常大,CPU版本在视频流场景下很容易成为性能瓶颈。

2.2 PyQt5安装中的两个高频坑

PyQt5的安装本身不复杂,但有两个坑我每次遇到都有人问。

第一个坑是pip安装速度极慢或者直接超时。PyQt5的wheel包体积不小,几十兆到上百兆都有,国内网络直连官方PyPI源经常下载到一半就断了。解决方法是使用国内镜像源,速度能提升十倍以上。

pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple

第二个坑是界面中文乱码。PyQt5的默认字体对中文支持不好,界面上按钮、标签一遇到中文就显示成方块。解决办法是在程序启动时统一设置字体,我习惯用微软雅黑,Windows和Linux的兼容性都还不错。

from PyQt5.QtGui import QFont app.setFont(QFont("Microsoft YaHei", 9))

2.3 商品识别模型权重与数据标注准备

项目里我用的预训练权重是yolov8s.pt和yolov8n.pt,分别应对有GPU和无GPU两种现场条件。如果你做的是全新品类的识别,千万不要直接拿COCO预训练权重去识别你的商品,COCO的80个类别里没有洗发水、薯片、饮料这些具体SKU,直接用的话什么都检测不出来。必须要用你自己标注的商品数据重新训练。

数据标注工具我用的是labelImg,老牌稳定,支持YOLO格式的txt标注文件直接导出。每张图片的标注文件格式是:

<class_index> <x_center> <y_center> <width> <height>

其中x_center、y_center、width、height都是归一化到0-1区间的比例值,不是像素坐标。训练完成后生成best.pt权重文件,这个就是后面部署推理用的核心模型文件。

标注的时候有个细节,一个商品如果被遮挡超过一半,我通常还是会把没被遮挡的部分标出来,但会打上低置信度的标签。这样模型能学到部分遮挡情况下的特征。完全无法辨认的目标就不标,避免给模型传递错误信息。

3. 核心功能模块实现与关键代码拆解

3.1 图片检测模块:从单张图片开始验证效果

图片检测是整套系统的基础模块,也是效果验证最快的方式。模型拿到一张图片,输出检测框、类别和置信度,画框显示出来。这个模块的实现逻辑很清晰。

from ultralytics import YOLO import cv2 # 加载训练好的模型 model = YOLO("weights/best.pt") # 执行推理 results = model.predict( source="test_images/shelf_01.jpg", conf=0.35, # 置信度阈值 iou=0.45, # NMS IoU阈值 imgsz=640, # 输入尺寸 device="cuda:0", # 使用GPU,无GPU则填"cpu" ) # 输出检测结果 for r in results: boxes = r.boxes for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = [int(p) for p in box.xyxy[0]] label = f"{model.names[cls_id]} {conf:.2f}" print(f"检测到: {label},坐标: ({x1}, {y1}, {x2}, {y2})")

这里有一个非常重要的参数:conf置信度阈值。阈值设高了,漏检多,货架上密集摆放的商品容易漏掉后排的;阈值设低了,误检多,相似的包装很容易被认错。我在实际项目中一般先用0.25跑一遍看效果,再根据检测结果逐步上调到0.35到0.4之间,找到一个漏检和误检平衡的点。

如果单独跑predict没有出现任何报错,但结果图里什么都没有,先别怀疑代码,直接用下面这段代码把推理后的图像保存出来看看,确认模型是否真的输出了空结果,以及画框是否成功。

annotated_frame = results[0].plot() cv2.imwrite("output/shelf_result.jpg", annotated_frame)

3.2 视频流检测模块:本地视频、摄像头、RTSP全支持

视频流检测是这套系统的重头戏。商品识别如果只能看静态图片,实用性会大打折扣,接入摄像头或者RTSP视频流才能真正用于超市的实时监控和自助结算场景。

视频流处理的关键在于逐帧读取和多线程并发,不然界面会卡成PPT。OpenCV的VideoCapture可以统一处理本地视频文件、USB摄像头和RTSP网络流,只是source参数不同:

# 本地视频 cap = cv2.VideoCapture("test_videos/checkout.mp4") # USB摄像头 cap = cv2.VideoCapture(0) # RTSP网络摄像头流 cap = cv2.VideoCapture("rtsp://admin:password@192.168.1.100:554/stream1")

RTSP接入的关键在于协议参数,很多摄像头默认配置用OpenCV打开会黑屏或者断流。我一般会加上ffmpeg后端参数,并且关掉缓冲来降低延迟:

cap = cv2.VideoCapture( "rtsp://admin:password@192.168.1.100:554/stream1", cv2.CAP_FFMPEG ) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 降低缓冲,减少延迟

帧率控制也需要关注。如果摄像头是30帧,但模型推理只有10帧的处理能力,直接逐帧推理会导致积压和延迟越来越大。标准做法是设置一个目标FPS,通过控制帧间隔来做丢帧处理。

target_fps = 15 frame_interval = int(1000 / target_fps) last_time = cv2.getTickCount() while True: ret, frame = cap.read() if not ret: break current_time = cv2.getTickCount() elapsed = (current_time - last_time) / cv2.getTickFrequency() * 1000 if elapsed >= frame_interval: results = model.predict(frame, conf=0.35, iou=0.45, imgsz=640) annotated = results[0].plot() # 显示或推送到界面 last_time = current_time

3.3 PyQt5桌面界面设计:布局与交互逻辑

PyQt5的界面我采用左右分栏布局,左侧是实时画面显示区,右侧是结果信息区。操作按钮放在底部,包括打开图片、打开摄像头、打开视频流、停止检测、保存截图这几个核心功能。

核心控件三件套是QLabel(显示画面)、QPushButton(操作按钮)、QTextEdit(日志信息)。界面代码如下:

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("超市商品识别系统") self.setMinimumSize(1280, 800) # 中间画面显示 self.video_label = QLabel(self) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(960, 600) self.video_label.setStyleSheet("background-color: #1e1e1e; color: #ffffff;") # 右侧信息面板 self.info_text = QTextEdit(self) self.info_text.setReadOnly(True) # 按钮区 self.btn_image = QPushButton("打开图片") self.btn_camera = QPushButton("打开摄像头") self.btn_stream = QPushButton("打开视频流") self.btn_stop = QPushButton("停止检测") self.btn_stop.setEnabled(False) # 用布局管理器排列 layout = self._build_layout() # ...省略布局细节

界面设计有个值得强调的思路,信号与槽机制是PyQt5的核心。按钮点击后触发信号,槽函数里启动相应的检测任务。这里有个关键点,所有耗时操作不能在主线程里执行,否则界面会卡死。检测任务要放到QThread工作线程里,通过信号把检测结果传回主线程更新界面。

from PyQt5.QtCore import QThread, pyqtSignal class DetectionThread(QThread): frame_ready = pyqtSignal(object) # 画面更新信号 result_ready = pyqtSignal(dict) # 检测结果信号 def __init__(self): super().__init__() self.running = False self.source = None self.model = YOLO("weights/best.pt") def run(self): cap = cv2.VideoCapture(self.source) while self.running: ret, frame = cap.read() if not ret: break results = self.model.predict(frame, conf=0.35, iou=0.45) annotated = results[0].plot() # 将OpenCV BGR图像转为RGB,再转成QImage用于界面显示 rgb_image = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimage = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimage)

这个线程类是整个视频检测模块的核心。main window拿到frame_ready信号后,把QImage显示到QLabel上,界面就实时刷新了。结果信息每次检测后统计各类商品数量,通过result_ready信号传给界面右侧显示。

4. 线程架构、推理性能与界面交互的最优实践

4.1 多线程架构:别把主线程拖死

PyQt5用户界面运行在GUI主线程里,如果在主线程里直接调用模型推理,视频流一来界面就全卡住,按钮点了没反应,最小化都费劲。所有的耗时操作,包括视频读取、模型推理、图像处理,都必须放到单独的工作线程里。

我推荐用QThread方案,结构清晰,信号与槽天然适合跨线程通信。检测线程拿到每一帧,推理完通过信号把结果发回主线程,主线程只负责显示,两边各干各的事,互不阻塞。

更复杂的场景还可以引入任务队列。视频读取线程只负责读帧,往队列里塞;推理线程从队列里取帧处理;显示线程展示结果。三个线程用Queue解耦,好处是每一帧的耗时波动不会影响其他环节。比如某一帧推理特别慢,读帧线程不会卡住,队列可以缓冲,显示线程也不会因为这一帧拖慢后续帧。

4.2 推理参数工程化调优:不只是调conf和iou

很多人只调conf和iou两个参数就完事了,实际上还有几个参数对最终效果影响巨大。

imgsz参数决定输入模型的图像尺寸。默认640,如果你货架上的商品都很小,可以试960,小目标的检出率会明显提升。但代价是推理时间翻倍,这个要根据实际硬件来权衡。我用RTX 3060测试,640尺寸约25毫秒/帧,960尺寸约60毫秒/帧,流畅度差距明显。

半精度推理是白捡的性能提升。GPU推理时把模型和输入转为fp16,推理速度可以提升30%到50%,精度损失在绝大多数场景下肉眼不可见。只需在predict时加一个参数:

results = model.predict(frame, conf=0.35, iou=0.45, imgsz=640, half=True)

批处理vs单帧推理。如果做的是离线批量识别几百张图片,可以一次性传入整个图片列表,模型自动批处理,吞吐量会高很多。但视频流场景必须逐帧推理,批次大小固定为1,此时线程架构比推理参数更影响整体流畅度。

4.3 界面流畅度优化:QImage格式转换的小技巧

视频流画面从OpenCV到PyQt5展示,中间必须经过数据类型转换。这个转换如果做不好,性能差距可以到好几倍。

OpenCV的图像格式是BGR,PyQt5的QImage是RGB。很多人用cv2.cvtColor一个个通道转换,速度很慢。我的做法是直接用QImage的Format_RGB888格式,配合numpy数组的内存布局,一次性转换到位,避免逐像素操作。

def convert_cv_to_qimage(cv_img): rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w return QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)

还要注意QLabel显示大图时,使用scaled缩放会消耗不少CPU。商品识别画面通常是1920x1080的,而界面显示区域可能只有960x600,直接setPixmap原始尺寸会让界面刷新变慢。正确做法是做一次带比例缩放的resize:

pixmap = QPixmap.fromImage(qimage) scaled_pixmap = pixmap.scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.video_label.setPixmap(scaled_pixmap)

5. 实际项目中的典型问题与排查经验

5.1 线上环境常见问题速查表

做过的项目多了,踩过的坑也多了。我把商品识别项目里最常遇到的几个问题整理成一张速查表,遇到对应症状可以直接按表排查。

症状问题原因解决方案
启动后窗口假死模型加载放到了主线程模型初始化放到检测线程的run方法里
视频画面卡顿但CPU占用不高未加帧间间隔,无限循环读帧按目标FPS控制帧处理间隔
检测框错位输入图像尺寸与显示尺寸不一致图像resize时记录缩放比例,坐标按比例映射
中文标签显示乱码PyQt5默认字体不支持中文程序启动时统一设置QFont中文字体
GPU显存占用持续上涨推理结果未释放或video capture缓存堆积每次循环结束时显式del results,控制队列长度
RTSP断流后无法自动恢复没有重连机制检测线程捕获异常后自动延迟重连
小商品总检测不到imgsz太小,小目标特征丢失提升imgsz至960或裁剪感兴趣区域后检测

5.2 一个最典型的排查案例

有一次客户反馈说摄像头接入后,画面断断续续,检测结果还经常延迟好几秒。我远程看了日志,发现是RTSP拉流后每帧都做推理,但摄像头是25帧,推理只有8帧的处理能力,队列里积压了大量帧,延迟越来越大。

排查思路是先把推理性能和推流性能分开测。用一段本地视频测试,发现推理稳定在8帧左右,排除了模型本身的问题。再单独拉RTSP测试,发现视频读取本身没问题。最后定位到问题就是没有做帧丢弃和队列长度控制。

解决方案是加了一个有界队列,队列满时直接丢弃最旧的帧,保证处理的一定是最新画面。修改后延迟降到300毫秒以内,客户现场体验完全可接受。

暴露这个案例是想说明,视频处理项目里80%的性能问题都不是算法的问题,而是架构设计的问题。帧的生产速度和消费速度不匹配,就必须引入缓冲和丢帧策略,这个思想在任何视频AI项目里都通用。

5.3 训练数据不充分的应急方案

有些项目时间很紧,客户给的图片只有几十张,直接训练出来的模型泛化能力很差。这个阶段我一般会做两件事。

第一是数据增强。用albumentations库做随机水平翻转、亮度对比度扰动、随机裁剪缩放、加噪声,把几十张图片膨胀到两三百张。注意翻转操作要谨慎,商品上的文字翻转后会变成反的,这类样本应去掉或者特殊处理,否则模型会学到错误特征。

第二是加载预训练权重做迁移学习。用COCO预训练模型作为初始权重,冻结前几层卷积特征提取层,只训练后面的检测头。这样即使数据量少,也能利用到预训练模型学到的通用视觉特征。做法是在训练脚本里设置:

model = YOLO("yolov8s.pt") # 加载预训练权重 # freeze层数可以根据数据量调整 results = model.train( data="supermarket.yaml", epochs=100, imgsz=640, freeze=10, # 冻结前10层 lr0=0.001, # 学习率调低,防止破坏预训练特征 batch=16, )

数据量不足时,训练轮数不能开太大,否则会过拟合。我一般控制在100轮左右,同时用早停策略,验证集损失连续20轮不下降就自动停止。

6. 项目交付与持续迭代的实操细节

6.1 客户现场的模型更新流程

模型训练完善后,客户现场要更新模型怎么办?很多项目死在交付后的维护环节,因为每次都在客户机器上手动换文件、重启程序,既低效又容易出错。

我的做法是把模型文件放到一个固定的models目录,程序启动时扫描目录下所有.pt文件,界面上加一个下拉框切换模型。客户拿到新模型文件,放进目录后在界面上一选,程序自动重新加载并生效,完全不需要重启。代码逻辑很简单:

def reload_model(self, model_path): if hasattr(self, "detection_thread") and self.detection_thread.isRunning(): self.detection_thread.stop() self.detection_thread.wait() self.detection_thread = DetectionThread() self.detection_thread.set_model(model_path) self.detection_thread.start()

这个流程极大降低了客户的维护门槛,后期新增SKU或优化识别效果时,客户自己就能操作,不需要每次叫你去现场。

6.2 误检率和漏检率如何平衡

商品识别项目验收时,客户最关心的就是误检率和漏检率。这两个指标此消彼长。你把conf阈值调高,漏检减少但误检增加,因为模型不确定的样本会被当作负样本过滤掉;调低conf阈值,模型更容易把相似的误检为同一类,但漏检会减少。

没有一个参数是万能解。我的经验是分场景定策略。自助结算场景,宁可多问一句也不放过任何商品,适合用高召回率,即把conf调低到0.2左右;盘点机器人场景,追求的是准确统计库存,宁可漏检也能事后补充,适合用高精度,conf调到0.5以上。

实操中我会在程序里加一个精确度模式切换下拉框,让现场人员根据实际场景选择模式,不同模式自动切换不同的conf和iou参数组合。这样既保证了灵活性,也让客户感受到系统的人性化设计。

6.3 关于商用授权的一点提醒

最后这个话题一定要提,千万不要忽略。如果你用的是Ultralytics官方YOLOv8,它默认是AGPL-3.0协议,这个协议要求任何基于它的衍生作品都必须以相同协议开源。换句话说,如果你直接把它封装成商业软件卖给客户,又不开放自己软件的源代码,严格来说是存在合规风险的。

我在商用项目中会提前做合规评估。方案一是购买Ultralytics的企业授权,价格按项目规模计算,预算充足又追求省事的客户可以选这个。方案二是自行实现或选用其他宽松协议的目标检测模型,比如YOLOv5的某些开源版本用的是GPL协议,情况也不同,需要具体分析。方案三是在合同中明确告知客户模型算法的授权限制,把风险转移给客户决策。

不要因为这个事情翻车,技术再好,授权问题没有理清,项目交付后还可能面临法律风险。我在项目交付前都会把这一项列成文档,和客户核对确认,保护自己也保护客户。

这套系统的完整落地思路到这里就差不多讲完了。从YOLO选型、PyQt5界面设计到线程架构、参数调优,再到客户现场交付的细节,都是我在项目里一步步验证过的方法。最后再说一个我个人的操作习惯:每次上线之前,我都会找一批客户现场完全没见过的照片跑一遍模型,专门看那些置信度徘徊在0.3到0.4之间的样本。这批样本往往能暴露模型泛化能力的真实水平,比看训练集指标有效得多。多花半小时做这个验证,能帮你在客户现场少熬三个通宵。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询