简介这是一套面向机器视觉与自动化方向学习者的水果分拣系统实战源码基于Python实现适合具备一定图像处理与机器学习基础、希望打通「识别—分类—控制」完整链路的开发者参考。系统以摄像头采集水果图像为起点经灰度化、滤波去噪、边缘检测等预处理后提取形状、颜色、纹理特征再借助scikit-learn、TensorFlow等库完成种类识别并驱动传送带、推杆等执行机构完成分拣同时包含串口通信与机械臂控制模块。资源包共61个文件以23个py源码与29个pyc编译文件为主另含界面文件、配置与图片素材整体约224KB结构紧凑、模块划分清晰。目前已有184人学习下载。读者可从中获取图像定位、图像分割、图像识别、百度API调用及运动控制等可复用脚本理解多线程实时处理与模块化集成思路并参考其目录组织方式快速搭建自己的分拣原型。1. 水果分拣线上为什么传统视觉方案总在反光果皮上翻车做过水果分拣的同行大概都有过这种经历实验室里跑得好好的模型搬到产线上一开灯苹果表面的高光直接把缺陷检测的准确率从 95% 拉到 60% 出头。这不是模型不行而是机器视觉在真实工业场景里遇到的第一个硬骨头——光照不可控。基于机器视觉的水果分拣系统Python这个方向本质上要解决的是三件事把水果从背景里稳定分割出来、按外观特征分类大小、颜色、瑕疵、成熟度、再把分类结果翻译成执行机构能懂的控制信号。Python 在这里的角色不是玩具语言而是从相机 SDK 调用、OpenCV 图像处理、到深度学习推理、再到串口/PLC 通信的胶水层。适合谁看如果你手上有工业相机、传送带、气动推杆想用 Python 搭一套能跑起来的分拣原型或者你正在做机器视觉应用工程师的转型准备这篇笔记里的参数和踩坑记录可以直接拿去用。整套系统的核心链路是图像采集 → 预处理 → 分割/检测 → 分类决策 → 执行控制每一环都有它自己的脾气。2. 从相机取流到 OpenCV 预处理把图像质量钉死在源头2.1 相机选型与 Python 取流的三种常见接法水果分拣对相机的核心要求是帧率够、触发同步、色彩还原准。常见的工业相机接口有 USB3.0、GigE、Camera Link 三种。USB3.0 便宜好上手但线缆超过 3 米就容易丢帧GigE 适合长距离布线但需要配置网卡巨帧Camera Link 性能最强但采集卡成本高。我一般给中小产线推荐 GigE 相机配 Python 的 harvesters 或 pypylon 库。以 Basler 相机为例用 pypylon 取流的最小可跑代码from pypylon import pylon import cv2 import numpy as np # 创建相机实例 camera pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateFirstDevice()) camera.Open() # 关键参数设置 camera.ExposureTime.SetValue(8000) # 曝光时间 8000us反光果皮适当降低 camera.Gain.SetValue(2.0) # 增益不要超过 5否则噪点爆炸 camera.PixelFormat.SetValue(BGR8) # 直接输出 BGR省一次转换 camera.AcquisitionFrameRateEnable.SetValue(True) camera.AcquisitionFrameRate.SetValue(30) # 30fps 匹配传送带速度 # 软触发模式由光电传感器触发 camera.TriggerMode.SetValue(On) camera.TriggerSource.SetValue(Software) converter pylon.ImageFormatConverter() converter.OutputPixelFormat pylon.PixelType_BGR8packed camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly) while camera.IsGrabbing(): grabResult camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException) if grabResult.GrabSucceeded(): img converter.Convert(grabResult).GetArray() cv2.imshow(fruit, img) if cv2.waitKey(1) 0xFF ord(q): break grabResult.Release() camera.Close()逻辑说明ExposureTime是第一个要调的参数水果表面反光强时曝光时间从 10000us 降到 6000~8000us 能显著压住高光Gain是后悔药调高了画面亮但噪点跟着涨宁可加光源也别猛拉增益。GrabStrategy_LatestImageOnly保证你拿到的永远是最新帧不会因为处理慢而积压旧图。触发模式一定要用硬触发或软触发自由运行模式在传送带上会导致每张图的水果位置漂移。2.2 预处理流水线白平衡、去噪、背景分割拿到原始图之后直接送进模型是大忌。水果分拣的预处理一般走这条链路白平衡校正 → 高斯去噪 → HSV 色彩空间转换 → 背景分割。def preprocess(img): # 1. 白平衡以画面左上角 50x50 区域为参考白 ref img[0:50, 0:50] b, g, r np.mean(ref[:,:,0]), np.mean(ref[:,:,1]), np.mean(ref[:,:,2]) gray (b g r) / 3 img img.astype(np.float32) img[:,:,0] * gray / b img[:,:,1] * gray / g img[:,:,2] * gray / r img np.clip(img, 0, 255).astype(np.uint8) # 2. 高斯去噪核大小 5x5 img cv2.GaussianBlur(img, (5, 5), 0) # 3. 转 HSV用色相通道分割水果 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 以苹果为例红色色相范围 mask1 cv2.inRange(hsv, (0, 80, 60), (10, 255, 255)) mask2 cv2.inRange(hsv, (170, 80, 60), (180, 255, 255)) mask cv2.bitwise_or(mask1, mask2) # 4. 形态学去毛刺 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) return img, mask参数说明白平衡参考区选画面边缘而不是中心因为中心经常有水果占位。HSV 的 S 通道下限设 80 是为了滤掉灰色传送带V 通道下限 60 是为了滤掉阴影。形态学核大小 7x7 是经验值太小去不掉噪点太大会把水果边缘啃掉一圈。如果你的传送带是白色或浅色这套 HSV 阈值基本能直接用如果是深色传送带S 下限可以降到 50。2.3 轮廓提取与水果定位的坐标换算分割出 mask 之后用findContours拿到每个水果的轮廓再算最小外接圆或外接矩形。这里有个容易被忽略的点像素坐标到物理坐标的标定。传送带平面和相机光轴不垂直时同一个水果在画面中心和边缘的像素尺寸是不一样的。contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) results [] for cnt in contours: area cv2.contourArea(cnt) if area 2000: # 滤掉小噪点 continue (x, y), radius cv2.minEnclosingCircle(cnt) # 像素半径转物理半径calib_ratio 由标定块测得 calib_ratio 0.32 # mm/pixel需实际标定 real_radius radius * calib_ratio results.append({ center: (int(x), int(y)), radius_px: radius, radius_mm: real_radius })标定方法很简单放一个已知直径的硬币或标准球在传送带上拍一张图量出像素直径除一下就是calib_ratio。注意这个值只在相机高度和焦距不变时有效动了相机就得重新标。3. 分类决策传统特征SVM 还是直接上 YOLO3.1 两条路线的选型对比水果分拣的分类任务通常分两级一级是大小分级按直径分 S/M/L二级是瑕疵检测碰伤、虫眼、腐烂。大小分级用传统方法就够了算个外接圆直径查表分级毫秒级出结果。瑕疵检测才是分水岭。方案准确率推理速度数据需求部署难度传统特征SVM75~85%极快每类 50 张低YOLOv8n90~95%30fps每类 200 张中YOLOv8s93~97%15fps每类 300 张中我的建议如果瑕疵类型少于 3 种且背景干净传统方法够用如果瑕疵种类多、形态不规则直接上 YOLOv8n别在传统方法上浪费时间调特征。3.2 用 YOLOv8 训练水果瑕疵检测模型数据标注用 labelImg 或 Roboflow导出 YOLO 格式。目录结构dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml内容path: ./dataset train: images/train val: images/val nc: 4 names: [good, bruise, insect, rot]训练命令yolo detect train modelyolov8n.pt datadataset/data.yaml epochs100 imgsz640 batch16 device0参数说明imgsz640是精度和速度的平衡点水果目标通常占画面 1/4 以上640 够用batch16看显存8G 显存跑 640 的 yolov8n 可以到 32epochs100配合早停一般 60~80 轮就收敛了。训练完用yolo detect val看 mAP50低于 0.85 就别急着部署先查数据。3.3 推理端与产线节拍的匹配训练完导出 ONNX 或 TensorRT 引擎Python 端用ultralytics或onnxruntime加载from ultralytics import YOLO model YOLO(best.engine) # TensorRT 引擎比 pt 快 2~3 倍 def classify(img): results model(img, conf0.5, iou0.45, verboseFalse) detections [] for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) xyxy box.xyxy[0].tolist() detections.append({class: cls_id, conf: conf, bbox: xyxy}) return detectionsconf0.5是置信度阈值产线上宁可漏检也别误检误检会把好果子踢进废品箱。iou0.45控制重叠框合并。如果传送带速度是 0.5m/s相机视野 0.4m那每帧间隔 0.8s30fps 的推理速度绰绰有余。但如果速度提到 1.5m/s就得考虑 TensorRT FP16 量化或者换更小的模型。4. 避坑与排查产线上翻车的五个真实场景4.1 反光导致分割 mask 出现空洞现象苹果顶部高光区域被 HSV 阈值判为背景mask 中间出现空洞轮廓面积偏小分级误判为小果。原因高光区域 S 通道值极低接近白色落在阈值范围外。解决在 HSV 分割前先做一次高光抑制——用cv2.inpaint或者简单点对 V 通道大于 240 的区域用邻域均值填充。另一个办法是加偏振片物理层面压高光效果最稳。4.2 传送带速度变化导致触发丢帧现象光电传感器触发相机但偶尔丢一两帧对应位置的水果没被分拣。原因软触发模式下 Python 主循环处理上一帧耗时超过帧间隔RetrieveResult超时。解决把图像采集和处理拆到两个线程采集线程只管存图到队列处理线程从队列取。队列长度设 3~5满了就丢最旧的。别用LatestImageOnly策略配软触发会丢触发信号。4.3 模型在实验室准、产线不准现象验证集 mAP50 有 0.93产线上误检率却很高。原因训练数据的光照条件和产线不一致或者产线相机白平衡和训练时不同。解决训练数据必须包含产线实际光照下的样本至少 30% 的图要在产线环境拍。另外推理前加一步白平衡校正和训练时的预处理对齐。4.4 串口通信延迟导致分拣位置偏移现象分类结果正确但气动推杆动作时机不对水果被推到传送带边缘。原因从相机触发到推杆动作的总延迟没有补偿水果已经移动了一段距离。解决测出总延迟触发→推理→串口发送→PLC 响应假设 80ms传送带 0.5m/s那水果移动了 40mm。在发送坐标时把 x 坐标加上这个偏移量。延迟会随系统负载波动最好用编码器实时读传送带位置做闭环。4.5 OpenCV 版本差异导致 API 行为不一致现象开发机cv2.findContours返回两个值产线机返回三个值代码直接崩。原因OpenCV 3.x 和 4.x 的findContours返回值数量不同。解决统一环境用requirements.txt锁版本。产线部署前在目标机器上跑一遍完整流程别信我本地能跑。5. 把分拣系统跑出实验室标定、节拍与长期稳定性的几个硬功夫整套系统能不能从 demo 变成产线设备分水岭不在模型精度而在标定精度和长期稳定性。我踩过最深的坑是相机标定一开始用张正友标定法算内参和外参结果传送带平面和标定板平面差了 2mm 高度导致边缘水果的物理尺寸算出来偏大 5%。后来改成在传送带实际高度上放标定板重新算单应性矩阵误差降到 1% 以内。标定的具体做法打印一张棋盘格放在传送带平面上拍 15~20 张不同角度的图用cv2.calibrateCamera算内参再用cv2.findHomography算传送带平面到相机平面的单应矩阵。之后所有像素坐标都通过这个矩阵转到物理坐标。注意标定板要覆盖整个视野边缘区域至少出现 3 次。节拍匹配是另一个容易翻车的地方。假设产线要求 5 个/秒的分拣速度那从触发到推杆动作必须在 200ms 内完成。拆解一下相机曝光 8ms 传输 5ms 预处理 15ms 推理 30ms 通信 10ms 推杆响应 50ms 118ms看起来够。但这是理想值实际跑起来 Python 的 GIL、系统调度、USB 带宽波动都会吃掉余量。我的习惯是留 50% 余量即目标 100ms 实际按 200ms 设计。如果不够优先砍预处理——很多步骤可以合并或者用 GPU 加速。长期稳定性方面三个措施最有效一是每天开机跑一次标定验证用标准球测直径偏差超过 2% 就重新标定二是模型每周用新采集的产线数据做一次增量训练防止光照季节变化导致漂移三是所有关键参数写进配置文件别硬编码在代码里换产线时改配置不改代码。最后说一个我自己的习惯每次调完参数把当天的产线视频存一份标注好日期和参数版本。出问题的时候回看视频比看日志快十倍。这套系统我前后调了三个月最大的教训是——别在实验室里追求 99% 的准确率产线上 90% 的稳定运行比实验室 99% 的峰值有价值得多。希望帮到你。本文还有配套的精品资源点击获取