资讯动态

OpenCV轮廓特征驱动植物叶片识别:从二值化到最近邻分类

发布时间:2026/10/23 3:22:48 来源:尧图企业网站定制
简介面向Python与OpenCV学习者的植物叶片识别实验文档围绕图像分割、表示与特征提取展开适合正在完成图像处理课程实验或入门计算机视觉的读者。资源共1个doc文件压缩包大小331KB内容精炼集中已有5414人下载学习。文档以实验报告形式组织涵盖实验目的、要求与结果从轮廓概念与二值化预处理讲起逐步演示cv2.findContours()查找轮廓、cv2.drawContours()绘制轮廓并计算矩、面积、周长等基础特征。进一步介绍直边界矩形、旋转边界矩形和最小外接圆的求解方法同时补充长宽比、轮廓与边界矩形面积比、轮廓与凸包面积比、等效圆直径、拟合椭圆方向、掩模像素点及平均灰度等轮廓性质的计算代码。通过叶片图像的阈值分割与二值化处理读者可以掌握用OpenCV对植物叶片进行轮廓识别、特征提取与定量分析的完整流程尤其适合作为课程实验参考或相关项目起步材料。1. 植物叶片识别项目OpenCV 轮廓特征驱动的自动分类实验每年到了图像处理课程后期总有一批同学被同一个综合作业卡住给定几张植物叶片图要程序自动说出叶片种类。单纯做模板匹配显然不现实——叶片有旋转、有尺度变化光照也不均匀硬套相似度算法很容易翻车。我拆的这份 Python OpenCV 植物叶片识别实验走的是另一条更稳的路线不碰深度学习只靠轮廓特征长宽比、矩形度、圆形度、凸度、等效直径组合出分类向量再用最近邻规则做判别。整套代码从读取 leaf.jpg 到输出叶片类别核心链路就是「二值化 → 找轮廓 → 提特征 → 查表分类」每一层都有对应的 OpenCV 函数可以直接落点。它特别适合两类人一是刚学完轮廓相关 API 想找个完整闭环练手的初学者二是需要快速给叶片图像做形状分析又不想引入重型框架的开发者。2. 轮廓提取链路为什么先分割再谈识别2.1 二值化是轮廓提取的前置条件实验文档里反复强调一个观点找轮廓之前必须先做阈值化或者 Canny 边缘检测。原因很直接——cv2.findContours()处理的是「黑色背景上的白色物体」如果直接传入彩色图灰度值分布复杂轮廓提取会得到大量噪声边界后续的特征计算全部失真。实验里用到的cv2.threshold()是固定阈值法只适合叶片与背景灰度差异明显的图片遇到背景复杂的实拍图我会先转 HSV 用绿色通道做 inRange 提取叶片区域效果比全局阈值稳定得多。import cv2 import numpy as np # 读取叶片灰度图 img cv2.imread(leaf.jpg, 0) # 全局阈值分割像素值大于120的置为255(白)其余置为0(黑) ret, thresh cv2.threshold(img, 120, 255, cv2.THRESH_BINARY) # 可视化分割结果 cv2.imshow(threshold, thresh) cv2.waitKey(0)这段代码的参数含义要拆开说120是阈值实验素材里叶片与背景灰度差通常在 40 以上取 120 能把叶片完整摘出来255是目标像素值表示满足条件的像素统一赋成白色。cv2.THRESH_BINARY是正向二值化即大于阈值的变白、小于阈值的变黑。如果你发现叶片内部出现黑色空洞说明阈值偏高把叶片上的浅色区域误判成了背景反之背景噪点变多则是阈值偏低。这里有个坑固定阈值对每张图都要手工调参后面做批量分类时我会对每张图单独算一个 Otsu 自适应阈值避免一张图一个参数的尴尬。2.2 轮廓查找与绘制理解返回值和层级cv2.findContours()是整条链路的枢纽。实验文档里写着返回值有三个——图像、轮廓、层级但这里有个版本差异必须先说明OpenCV 3.x 之后第一个返回值不再是图像而是轮廓本身hierarchy变成第二个返回值。如果你照着旧教程写contours, hierarchy cv2.findContours(...)在 OpenCV 4.x 下会直接报错这是我跑这份实验遇到的第一个坑。# 查找轮廓RETR_TREE 返回完整层级CHAIN_APPROX_SIMPLE 只保留端点 contours, hierarchy cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 在原始图像的副本上绘制全部轮廓绿色线宽2 img_contour cv2.drawContours(img.copy(), contours, -1, (0, 255, 0), 2) cv2.imshow(contours, img_contour) cv2.waitKey(0)RETR_TREE会把所有轮廓按父子层级组织起来比如叶片外轮廓是父级叶片内部的叶脉空洞轮廓是子级CHAIN_APPROX_SIMPLE则压缩轮廓点只保留端点。参数说明上-1表示绘制所有轮廓想单独画某一个就把索引传进去。实验里强调「查找轮廓会修改原始图像」是因为findContours内部会改动传入的二值图所以绘制时我用img.copy()保护原图这是血泪经验——否则后面提取特征时拿到的就是被改过的图。3. 特征工程实战从矩到多维度形状描述3.1 矩与基础几何量面积、周长、质心轮廓本身是一堆坐标点直接比较坐标没有意义需要把坐标点压缩成几个数值。OpenCV 提供的cv2.moments()就是干这个的——它计算轮廓的几何矩返回一个字典其中m00是零阶矩代表轮廓面积m10 / m00和m01 / m00分别是质心的 x、y 坐标。实验文档里用M[m00]替代cv2.contourArea()两者在数学上等价但m00是从矩推导的能跟中心矩统一起来用。# 取第一个轮廓作为研究对象 cnt contours[0] M cv2.moments(cnt) area_moment M[m00] # 零阶矩 面积 cx int(M[m10] / M[m00]) # 质心x坐标 cy int(M[m01] / M[m00]) # 质心y坐标 area cv2.contourArea(cnt) # 直接计算面积 perimeter cv2.arcLength(cnt, True) # 闭合轮廓周长cv2.arcLength()的第二个参数True表示轮廓是闭合的如果传False它只计算折线长度。做叶片识别时叶片轮廓天然闭合必须用True否则周长会少算一段闭合边的距离。面积和周长这两个量本身不能直接用于分类——同一个叶片拍近拍远面积差好几倍这就是尺度敏感问题。解决办法是后面把面积换成「面积占边界矩形比例」这类归一化特征。在实验素材里出现过contours, hierarchy cv2.findContours(thresh, 1, 2)这种写法1和2分别对应RETR_EXTERNAL和CHAIN_APPROX_SIMPLE的数值形式。我只建议在交互式调试时用数字正式代码必须写常量名否则换 OpenCV 版本时数字含义变动代码就成了黑匣子。3.2 边界矩形与圆形度归一化特征提取只靠面积和周长做分类叶片角度一变结果就飘。实验给出了两类边界框来解决这个问题直边界矩形cv2.boundingRect()不考虑旋转得到的宽高比会随叶片角度变化旋转边界矩形cv2.minAreaRect()返回最小外接矩形包含中心点、宽高和旋转角它能更稳定地描述叶片的伸展方向。# 直边界矩形轴对齐 x, y, w, h cv2.boundingRect(cnt) aspect_ratio float(w) / h # 长宽比特征 rect_area w * h extent area / rect_area # 轮廓面积占边界矩形面积的比例 # 最小外接矩形考虑旋转 rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) # 获取旋转矩形的4个角点 box np.int0(box) # 等效圆直径与轮廓面积相等的圆的直径 equi_diameter np.sqrt(4 * area / np.pi)aspect_ratio描述叶片是狭长还是宽圆不同植物差异明显extent描述轮廓在矩形里的填充程度叶片边缘如果是深裂的extent 会偏小比如枫叶类裂叶的 extent 通常低于 0.6而完整椭圆叶能到 0.8 以上。equi_diameter用于排除尺度影响——它是面积的另一种表达本身仍受缩放影响一般会和周长一起构造「圆形度」特征。真正的圆形度公式是4 * pi * area / (perimeter * perimeter)取值越接近 1 形状越接近圆这个特征对叶片形态区分度非常高实验中虽然没直接给公式但从面积、周长能组合出来。3.3 凸包特征叶片裂痕的量化指标实验里有一组容易被忽略的特征——solidity轮廓面积与凸包面积之比和cv2.convexHull()。凸包可以理解为包裹轮廓的最小凸多边形。叶片如果有锯齿或深裂凸包面积会明显大于轮廓实际面积solidity 就会小于 1光滑叶片 solidity 接近 1。hull cv2.convexHull(cnt) hull_area cv2.contourArea(hull) solidity area / hull_area # 凸度特征越接近1叶片越饱满 # 极点轮廓的最上、最下、最左、最右点 leftmost tuple(cnt[cnt[:, :, 0].argmin()][0]) rightmost tuple(cnt[cnt[:, :, 0].argmax()][0]) topmost tuple(cnt[cnt[:, :, 1].argmin()][0]) bottommost tuple(cnt[cnt[:, :, 1].argmax()][0])极点坐标本身不直接参与分类但可以用来算叶片长度和宽度结合最小外接矩形的角度能估计叶片的主轴方向。我在做实验时发现极点的取法有个细节cnt[:, :, 0]取所有点的 x 坐标argmin()返回最小值的索引因为轮廓点的形状是(n, 1, 2)所以要加[0]才能取到正确坐标。如果不做这一步得到的极点是数组元组绘制时会报维度错误。4. 自动分类核心构建叶片特征向量与判别规则4.1 三类特征组合把实验中的量变成可分类的向量综合作业要求「综合使用多个叶片特征」构建自动分类算法。实验正文提供了九类轮廓性质但直接全部塞进分类器会出问题——量纲不统一比如周长是几百像素、长宽比是 1.5简单距离计算会被周长主导。我的做法是只挑归一化特征长宽比、extent、solidity、圆形度外加等效直径除以周长得到的「形状紧凑度」。def extract_features(contour): # 面积与周长 area cv2.contourArea(contour) perimeter cv2.arcLength(contour, True) if perimeter 0: return None # 长宽比基于直边界矩形 x, y, w, h cv2.boundingRect(contour) aspect_ratio w / float(h) # 矩形度面积占边界矩形面积比例 extent area / (w * h) # 凸度面积占凸包面积比例 hull cv2.convexHull(contour) hull_area cv2.contourArea(hull) solidity area / hull_area if hull_area 0 else 0 # 圆形度4πA/P² circularity 4 * np.pi * area / (perimeter * perimeter) # 紧凑度等效直径与周长之比 equi_diameter np.sqrt(4 * area / np.pi) compactness equi_diameter / perimeter return [aspect_ratio, extent, solidity, circularity, compactness]这段代码把所有特征压到 0 到 1 之间或者无量纲区间不同叶片图片的缩放不再影响特征值。extract_features输出一个长度为 5 的列表这就是后续分类器的输入向量。有个细节需要注意当轮廓只有一个点时perimeter为 0会触发除零错误所以加了if perimeter 0的防护。实际实验中噪声小区域也可能会被识别成轮廓我建议在进入特征提取前加一个面积过滤——只保留面积大于 500 像素的轮廓避免把背景杂质当成叶片。4.2 分类器设计为什么用最近邻而不是决策树实验数据集只有 6 张叶片图每类可能就是 1 到 2 个样本决策树、SVM 这类需要大量样本训练的分类器在这里完全施展不开。我选用了最朴素的最近邻1-NN策略离线提取各样本的特征向量存成数组在线时提取未知叶片的特征逐项算欧氏距离距离最小的样本类别就是预测结果。# 已知样本特征库示例数据实际从标注文件夹批量提取 X_train [ [1.42, 0.76, 0.91, 0.62, 0.35], # 类A阔叶 [3.10, 0.58, 0.85, 0.41, 0.28], # 类B狭叶 [1.20, 0.82, 0.95, 0.71, 0.38], # 类C圆叶 ] y_train [阔叶, 狭叶, 圆叶] def predict_leaf(features): min_dist float(inf) best_label None for i, sample in enumerate(X_train): # 欧氏距离 dist np.sqrt(sum((a - b) ** 2 for a, b in zip(features, sample))) if dist min_dist: min_dist dist best_label y_train[i] return best_label, min_distpredict_leaf的逻辑很简单遍历每个已知样本计算欧氏距离取最小距离对应的标签。zip把待分类特征和样本特征按位配对(a - b) ** 2算每个维度的差值平方。这个方案的好处是特征维度增加时不需要重新训练加一组特征就多一个维度参与距离计算。缺点是每个特征维度默认权重相等如果发现某个特征对分类特别重要我一般会加权重系数比如把 aspect_ratio 的权重设成 2.0放大它对距离的贡献。4.3 从视频帧到实时识别改造思路综合作业要求「通过视频获取叶片程序自动报告植物种类」这等于要把分类器接到视频流上。核心改造点有两个一是视频帧进来后要做背景减除或颜色阈值把叶片从复杂背景中分离二是每帧都可能出现多个轮廓需要按面积排序只对最大轮廓做特征提取和分类。cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 转HSV并提取绿色区域 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (35, 40, 40), (85, 255, 255)) mask cv2.medianBlur(mask, 5) # 中值滤波去噪 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 取最大轮廓 cnt max(contours, keycv2.contourArea) area cv2.contourArea(cnt) if area 500: # 过滤小面积噪声 features extract_features(cnt) label, dist predict_leaf(features) cv2.drawContours(frame, [cnt], -1, (0, 255, 0), 2) cv2.putText(frame, label, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(leaf recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段实时代码中cv2.inRange的 HSV 范围是关键参数(35, 40, 40) 到 (85, 255, 255) 是绿色的通用范围能覆盖大多数叶片颜色如果叶片偏黄可以把 H 下限降到 25偏红则要调整到 170 以上。cv2.medianBlur去掉二值图上的椒盐噪声避免把噪点轮廓当成叶片。max(contours, keycv2.contourArea)是 Python 内置函数搭配 OpenCV 面积函数一行实现「取最大轮廓」。最后用putText在画面左上角输出类别名这就是「程序自动报告植物种类」的直观呈现。5. 避坑记录OpenCV 轮廓识别常见问题与排查5.1 版本差异导致 findContours 返回值不匹配现象代码运行时抛出ValueError: not enough values to unpack (expected 3, got 2)定位到findContours这一行。原因OpenCV 3.x 之前findContours返回三个值image, contours, hierarchy3.x 之后改成了两个值contours, hierarchy原始图像不再返回。实验文档写的是旧版接口直接照抄就会挂。解决统一按contours, hierarchy cv2.findContours(...)解包并确认环境里安装的是 OpenCV 3.0 以上版本。在代码顶部加一句print(cv2.__version__)先验版本再决定解包方式是我现在写 OpenCV 脚本的习惯。5.2 背景目标和颜色方向反了现象阈值后叶片变成黑色背景是白色findContours找到的全是背景边框特征计算出来全部异常。原因cv2.threshold默认THRESH_BINARY是「大于阈值为白」如果叶片灰度低于背景叶片会变黑。OpenCV 找轮廓的约定是「白色物体、黑色背景」方向反了整个结果就是错的。解决先确认叶片的灰度范围。我常用的排查命令是np.unique(thresh)统计二值图的像素值分布如果白色像素占比超过 80%八成是方向反了。改用cv2.THRESH_BINARY_INV把黑白对调或者把输入图取反cv2.bitwise_not(img)再二值化。5.3 叶片内部空洞导致多层轮廓干扰现象叶片图像里有叶脉、斑点findContours找出了几十个轮廓最大轮廓面积远小于叶片实际面积。原因RETR_TREE模式返回所有层级轮廓叶片内部的孔洞会生成子轮廓。如果不加过滤直接全部绘制画面会非常乱。解决分类场景只用RETR_EXTERNAL获取最外层轮廓或者用hierarchy的层级信息把子轮廓过滤掉。我在代码里加面积阈值area 500是最简单的处理但注意这个阈值要跟图片分辨率对齐——3000 像素宽的大图噪声面积 500 很正常我一般会取图片总面积的 1% 作为过滤线。5.4 非闭合轮廓导致周长和圆形度异常现象叶片边缘有缺口时arcLength(cnt, True)计算出的周长偏大圆形度特征明显偏小分类结果不稳定。原因叶片边缘与背景灰度接近阈值分割后轮廓断裂一条完整边界被拆成多个开放轮廓。此时有些轮廓的起点和终点不重合被当成闭合曲线计算周长会引入额外误差。解决在找轮廓之前做形态学操作cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)用闭运算把断裂处补上。kernel 大小一般设(5, 5)如果边缘缺口大可以调成(9, 9)但要注意过大会把相邻叶片连成一片。5.5 minAreaRect 返回的宽高顺序不稳定现象同一叶片转 90 度后rect[1]的宽和高互换导致长宽比特征跳变。原因cv2.minAreaRect()返回的(center, (width, height), angle)中width 和 height 是矩形的两条边但 OpenCV 不保证 width 始终大于 height角度在 0 到 90 度之间浮动时宽高标签会交换。解决对返回的宽高做归一化处理取long_edge max(w, h); short_edge min(w, h)用long_edge / short_edge作为长宽比特征。这样无论叶片怎么旋转特征值保持稳定。这个坑花了我两个小时才定位到当时分类结果在同一个叶片的垂直和水平拍摄图上差异巨大查了半天才发现是宽高顺序在作怪。6. 二次开发把实验系统扩展成可用工具的三个改造点6.1 特征库从硬编码改为目录自动提取现在的X_train是写在代码里的硬编码数组新增叶片类型就得改代码加一行。我一般会把样本图片按类别放进文件夹启动时自动扫描提取特征import os X_train, y_train [], [] for label in os.listdir(samples/): label_path os.path.join(samples/, label) if not os.path.isdir(label_path): continue for fname in os.listdir(label_path): img cv2.imread(os.path.join(label_path, fname), 0) _, th cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: cnt max(contours, keycv2.contourArea) X_train.append(extract_features(cnt)) y_train.append(label)cv2.THRESH_OTSU是自适应阈值它会根据图像灰度分布自动计算最优分割点省去每张图手工调阈值的麻烦。这是从实验代码迈向实用工具的第一小步——我后来把所有叶子图都改成 Otsu 阈值后准确率反而上升了因为每张图的曝光条件不同固定阈值反而会引入噪声。6.2 特征维度诊断哪些特征真的有用把特征丢进分类器前值得花时间做一次单特征区分度检查把每个特征按类别画出分布观察是否有重叠区间。我在这个实验里发现extent和solidity对叶片的锯齿边缘区分度最高aspect_ratio对狭叶和阔叶非常敏感而compactness在多数类别上分布重叠单独使用价值不大。这个诊断过程能帮你决定要不要给特定特征加权重或者直接剔除干扰维度。在predict_leaf里我加过一组权重参数效果明显的是把aspect_ratio权重调成 1.5solidity调成 1.2。调整权重的依据不是瞎试是看两类混淆样本在哪个维度上距离过近——如果两个不同叶片在长宽比上的差距远小于其他维度就要把这个维度的权重降下来让更稳定的特征主导决策。6.3 完整封装从脚本到命令行工具把这个实验封装成命令行工具使用户可以传入图片路径直接获取分类结果是我做完实验后的第一件事。核心思路是保留extract_features和predict_leaf两个纯函数用标准库argparse做参数解析再加一层文件读写逻辑。注意特征库的持久化——用np.save(features.npy, X_train)和np.save(labels.npy, y_train)把耗时提取的特征存下来下次运行时直接加载避免每次都重新处理样本。我踩过最深的坑是把X_train用普通列表存成文本再读回来结果浮点精度丢失分类结果出现偶发错判。改成 NumPy 二进制存储后问题消失。从那次以后我每提取完一组特征第一件事就是验证np.load读回来的数组和原始数组是否逐位相等确认无误再进分类流程。这个习惯让我少走了很多弯路也希望这次分享的完整链路——从二值化、轮廓提取、特征计算到最近邻分类——能帮你把自己的叶片识别程序真正跑通让实验课的综合作业不再是玄学。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑