简介:这份PDF文献聚焦计算机视觉在马铃薯自动检测分级中的工程实现,面向农业工程、图像处理与模式识别方向的学习者和研究人员,帮助理解如何将视觉算法落地到农产品品质检测场景。资源包共1个文件,为267KB的PDF文档,内容源自《农业机械学报》相关研究,涵盖改进果径法大小分级、离心率法形状分级、灰度值差值法发芽检测以及相邻采样边界点归一化半径差畸形识别等核心方法,并给出在线综合检测分级系统的PC-PLC架构设计。读者可从中获取完整的算法思路、特征提取流程与试验数据,文中系统分级精度达88.0%,并梳理了Marchant、Deck、Tao、Zhou等人的相关研究进展,适合作为课程设计、论文写作或工程方案选型的参考文献。目前已有151人学习关注,对计算机视觉与图形处理在农业检测中的应用具有专业指导价值。
1. 从一篇 2009 年的论文说起:88% 精度的马铃薯分级系统到底怎么复现
如果你手头有一堆马铃薯要分选,靠人工盯着看,一小时下来眼睛基本就废了。2009 年《农业机械学报》上有一篇被引了不少次的文章,标题是《基于计算机视觉的马铃薯自动检测分级》,作者来自中国农业大学工学院。它做的事情很具体:用一台 PC 加一个 PLC,配合工业相机和传送带,把马铃薯按大小、形状、发芽、畸形四个维度在线分级,最终综合分级精度做到 88.0%。这个数字放在今天不算惊艳,但它的方法论——改进果径法测大小、离心率法判形状、G 通道灰度差值法查发芽、相邻采样边界点归一化半径差检畸形——放到现在的计算机视觉大作业或者农产品分选项目里,依然是能直接抄作业的骨架。这篇文章适合两类人:一是做计算机视觉入门项目、想找一个完整闭环案例的学生;二是做农产品加工设备、需要一套可落地的视觉分级方案的一线工程师。下面我按“原理怎么立住、代码怎么落地、坑在哪”的顺序拆一遍。
2. 大小与形状分级:改进果径法和离心率法的代码实现
2.1 为什么选最长轴而不是投影面积
马铃薯是类球型果蔬,传统机械分级用漏网层或筛选筒,本质上是拿一个物理孔径去卡尺寸。换成计算机视觉之后,最直接的思路是算投影面积,但投影面积受马铃薯摆放姿态影响极大——同一个薯,平放和侧放拍出来面积能差 20% 以上。论文里用的是改进果径法,也就是最长轴法:对每个马铃薯拍 3 幅不同表面的图像,每幅图里找轮廓的最长轴,取 3 幅中的最大值 M 作为大小特征值。这个逻辑的好处是,最长轴是刚体本身的固有属性,不随拍摄角度变化,3 幅图覆盖 90% 以上薯面,基本能保证最长轴被捕捉到。
常见做法是先用背景差分把马铃薯从黑色传送带上抠出来,再对二值掩膜做轮廓提取,然后用最小外接矩形或者椭圆拟合拿到长轴。我一般会优先用cv2.minAreaRect,因为它对边界噪声的鲁棒性比直接算像素点对距离要好。
import cv2 import numpy as np def get_major_axis(mask): """ 输入:单通道二值掩膜,马铃薯区域为255,背景为0 输出:最长轴长度(像素) """ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return 0 cnt = max(contours, key=cv2.contourArea) rect = cv2.minAreaRect(cnt) # 返回 (中心点, (宽,高), 角度) width, height = rect[1] major = max(width, height) # 长边即最长轴 return major # 对3幅图分别计算,取最大值 major_axes = [get_major_axis(m) for m in masks] M = max(major_axes)逻辑说明:minAreaRect返回的是最小外接旋转矩形,它的长边对应马铃薯的最长轴方向。参数上唯一需要注意的是掩膜质量——如果背景差分后边缘有毛刺,minAreaRect会被拉偏,所以前面最好加一步形态学开运算。3 幅图取最大值这一步不能省,论文里明确说了这是覆盖 90% 薯面的前提。
2.2 离心率法分球型和椭球型
形状分级用的是离心率 R = MA / MB,其中 MA 是最长轴,MB 是与最长轴垂直方向上的长轴。正球型物体 R 趋近于 1,越扁长 R 越大。论文里给的实测数据:球型马铃薯离心率 1.112,椭球型 1.397。取 3 幅图像中最大的离心率 RM 作为形状系数,然后设一个阈值把两者分开。
这里有个容易翻车的地方:MB 不能简单用外接矩形的短边代替。因为马铃薯轮廓不是标准椭圆,外接矩形短边会被轮廓凹陷处影响。更稳的做法是沿最长轴方向做垂直投影,取投影宽度作为 MB。
def get_eccentricity(mask): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt = max(contours, key=cv2.contourArea) rect = cv2.minAreaRect(cnt) (cx, cy), (w, h), angle = rect MA = max(w, h) MB = min(w, h) if MB == 0: return float('inf') return MA / MB # 3幅图取最大离心率 R_list = [get_eccentricity(m) for m in masks] RM = max(R_list) # 阈值建议:论文未给明确分界,实测1.2~1.25之间较稳 shape_label = "球型" if RM < 1.22 else "椭球型"参数说明:阈值 1.22 是我根据论文中 1.112 和 1.397 两个实测值取的中位偏下,实际项目里建议用一批已标注样本跑 ROC 曲线来定。注意minAreaRect的 w 和 h 本身就对应了最长轴和垂直方向,所以这里直接用 min 和 max 是合理的,不需要额外做投影。
2.3 大小分级的四个等级怎么切
论文按 NY/T 1066-2006 马铃薯等级规格,把大小分为特级、一级、二级和等级外共 4 档。具体阈值论文正文没有逐条列出,但根据国标常见做法,特级通常要求最长轴 ≥ 80mm,一级 60~80mm,二级 40~60mm,低于 40mm 为等级外。实际写代码时,把 M 值映射到等级即可:
def grade_by_size(M, thresholds=(80, 60, 40)): if M >= thresholds[0]: return "特级" elif M >= thresholds[1]: return "一级" elif M >= thresholds[2]: return "二级" else: return "等级外"注意这里的单位是像素,需要事先用标定板换算出像素-毫米当量。我一般会在传送带旁边放一个已知尺寸的参照物,每批次开机时拍一张做标定,避免因相机高度变化导致阈值漂移。
3. 发芽与畸形检测:G 通道差值和归一化半径差的落地细节
3.1 G 通道灰度差值法检测发芽
发芽马铃薯的芽体颜色偏亮偏绿,和正常表皮的暗黄色在 G 通道上差异明显。论文的做法是:先提取马铃薯单体区域,算该区域 G 通道平均灰度值 GA;再逐点扫描,算每个像素 G 通道值 GT 与 GA 的差值 GD = GT - GA;如果 GD 大于阈值 TD,标记为候选芽体点;最后统计候选点总数 NG,当 NG > 10 时判定为发芽。
这个方法的精髓在于“先算全局均值再逐点比”,而不是直接设一个固定灰度阈值。因为不同批次马铃薯表皮颜色本身有差异,固定阈值很容易误判。用区域均值做基准,相当于做了自适应归一化。
def detect_sprout(bgr_img, mask, TD=30, NG_thresh=10): """ bgr_img: 原始彩色图 mask: 马铃薯区域二值掩膜 TD: 灰度差阈值,需根据光照调整 NG_thresh: 芽体点数量阈值 """ g_channel = bgr_img[:, :, 1] # OpenCV中G通道索引为1 potato_pixels = g_channel[mask == 255] if len(potato_pixels) == 0: return False, 0 GA = np.mean(potato_pixels) GD = g_channel.astype(np.float32) - GA sprout_mask = (GD > TD) & (mask == 255) NG = np.count_nonzero(sprout_mask) return NG > NG_thresh, NG逻辑说明:TD=30是经验值,光照强的时候要往上调,否则正常表皮的亮斑也会被算进去。论文里特别提到“当马铃薯表面有明显亮点时,会将该亮点误判为芽体点”,所以加了 NG > 10 的数量约束来过滤孤立噪点。这个 10 不是随便定的,是试验统计出来的——真实芽体通常是一小片连续区域,采样后点数不会太少。
3.2 归一化半径差检测畸形
畸形检测是四个维度里最绕的一个。论文的思路是:提取马铃薯边界轮廓,用等间距法对边界点采样,采样间隔 8 像素;计算每个采样点到形心的距离,即半径 r(k);对半径序列做归一化,得到 r'(k) = r(k) / r_mean;然后算相邻两个采样点归一化半径之差的绝对值 Δr(m) = |r'(k+1) - r'(k)|;取 3 幅图中 Δr(m) 的最大值作为畸形特征值,超过阈值就判为畸形。
论文给的实测曲线很直观:畸形马铃薯 Δr 峰值达到 0.1 以上,椭球型略小于 0.07,球型不到 0.05。所以阈值设在 0.08~0.09 之间比较合理。
def detect_misshapen(mask, sample_interval=8, delta_thresh=0.085): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) cnt = max(contours, key=cv2.contourArea) pts = cnt.reshape(-1, 2) # 等间距采样 sampled = pts[::sample_interval] if len(sampled) < 3: return False, 0 # 形心 M = cv2.moments(cnt) cx = M['m10'] / M['m00'] cy = M['m01'] / M['m00'] # 半径序列 radii = np.sqrt((sampled[:, 0] - cx)**2 + (sampled[:, 1] - cy)**2) r_mean = np.mean(radii) if r_mean == 0: return False, 0 r_norm = radii / r_mean # 相邻归一化半径差绝对值 delta_r = np.abs(np.diff(r_norm)) max_delta = np.max(delta_r) return max_delta > delta_thresh, max_delta参数说明:sample_interval=8直接来自论文。delta_thresh=0.085是在 0.07 和 0.1 之间取的。注意cv2.findContours要用CHAIN_APPROX_NONE,否则轮廓点会被压缩,等间距采样就失去意义了。另外形心用cv2.moments算的是区域矩的形心,比轮廓点平均更稳。
3.3 综合分级流程怎么串
四个维度各自出结果后,综合判级逻辑是:先看是否发芽或畸形,如果是,直接归入缺陷等级;否则按形状分球型/椭球型,再按大小分特级/一级/二级/等级外。论文的试验设计是球型和椭球型各分三档加等级外,共 7 个等级。
def comprehensive_grade(bgr_img, mask): is_sprout, ng = detect_sprout(bgr_img, mask) is_misshapen, delta = detect_misshapen(mask) if is_sprout or is_misshapen: return "缺陷薯" M = max([get_major_axis(m) for m in masks]) RM = max([get_eccentricity(m) for m in masks]) shape = "球型" if RM < 1.22 else "椭球型" size = grade_by_size(M) return f"{shape}-{size}"这个串联顺序不能反:缺陷检测必须放在大小形状之前,因为畸形薯的最长轴和离心率本身就不可靠,先判缺陷能避免把畸形薯错分到正常等级里。
4. 避坑与排查:这套 2009 年的方案放到今天会踩什么雷
4.1 现象:分级精度远低于 88%,窜果率飙升
原因:论文里用的是黑色传送带做背景,如果你换成浅色或反光传送带,背景差分直接失效,掩膜里混入大量背景噪声,最长轴和离心率全偏。解决:要么换黑色哑光传送带,要么改用基于颜色空间(如 HSV 中 S 通道)的分割,别硬套背景差分。
4.2 现象:发芽检测把正常薯的亮斑全标成芽点
原因:TD 阈值设太低,或者光照不均匀导致局部 G 通道值偏高。论文里也提到了这个误判。解决:先做光照均匀化(比如同态滤波或简单的顶帽变换),再把 TD 从 30 往上调,同时保留 NG > 10 的数量约束。如果还不行,加一步连通域分析,只保留面积大于一定值的候选区域。
4.3 现象:畸形检测对椭球型马铃薯误报率高
原因:椭球型本身的 Δr 峰值能到 0.07,如果阈值设成 0.06,正常椭球薯全被判畸形。解决:阈值必须设在 0.08 以上,论文数据是 0.1 对 0.07,中间只有 0.03 的余量,所以阈值取 0.085 左右最稳。另外采样间隔 8 像素是在特定分辨率下定的,如果你相机分辨率翻倍,采样间隔也要相应调整,否则 Δr 曲线会太平滑,峰值被抹掉。
4.4 现象:3 幅图拍不全 90% 薯面
原因:马铃薯在传送带上滚动姿态不可控,如果光电传感器触发时机不对,3 幅图可能拍到几乎同一面。解决:论文里马铃薯是“不停滚动”的,靠传送带摩擦带动旋转。实际复现时,传送带速度和相机触发频率要匹配,一般建议相邻两幅图之间马铃薯至少转过 120 度。可以在传送带侧面加毛刷条强制翻滚。
4.5 现象:PLC 通信延迟导致卸料错位
原因:PC 端图像处理耗时波动大,如果 PLC 等结果再动作,高速下马铃薯已经过了卸料口。解决:论文用的是 PC-PLC 主从架构,PC 只负责图像采集和分析,PLC 负责控制时序。实际做的时候要把图像处理放在马铃薯到达卸料口之前完成,留出固定延时。如果处理时间不稳定,可以在 PLC 侧做位置追踪,按编码器脉冲补偿。
5. 进阶技巧:用轮廓曲率替代归一化半径差,把畸形检测阈值变成自适应
论文的归一化半径差方法本质上是找边界上半径突变的位置,但它对采样间隔和阈值都很敏感。我在实际项目里更习惯用轮廓曲率来做畸形检测,好处是阈值可以自适应,不用每换一批薯就重新调参。
具体做法:对轮廓做高斯平滑后计算每点的曲率,畸形处的曲率会出现尖峰。然后对曲率序列做 Z-score 标准化,取最大 Z 值作为畸形特征。这样不管球型还是椭球型,正常轮廓的 Z 值都在 2 以内,畸形处能到 4 以上,分界非常清晰。
def curvature_anomaly(mask, sigma=3): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) cnt = max(contours, key=cv2.contourArea).reshape(-1, 2).astype(np.float32) # 高斯平滑轮廓 from scipy.ndimage import gaussian_filter1d x = gaussian_filter1d(cnt[:, 0], sigma) y = gaussian_filter1d(cnt[:, 1], sigma) # 一阶导和二阶导 dx = np.gradient(x) dy = np.gradient(y) ddx = np.gradient(dx) ddy = np.gradient(dy) # 曲率 curvature = np.abs(dx * ddy - dy * ddx) / (dx**2 + dy**2 + 1e-6)**1.5 # Z-score z = (curvature - np.mean(curvature)) / (np.std(curvature) + 1e-6) return np.max(z) # 判定:Z > 3.5 视为畸形 is_misshapen = curvature_anomaly(mask) > 3.5这个方法的参数只有sigma一个,控制平滑程度,一般取 3~5。sigma越大,对噪声越不敏感,但太大会把真实畸形也抹平。我一般先用论文那套归一化半径差跑一遍做基准,再用曲率法做交叉验证,两者都判畸形的才最终归入缺陷薯,这样能把误报压到最低。
从那以后我每次做农产品视觉分级,都强制先用一批已知标签的样本把四个维度的阈值各跑一遍 ROC,确认没有哪个维度拖后腿再上线。这套 2009 年的方法虽然老,但骨架清晰、每一步都有物理意义,比直接端一个深度学习模型上去要可控得多。希望帮到你。
本文还有配套的精品资源,点击获取