基于OpenCV轮廓检测的植物叶片识别与特征提取实战

发布时间:2026/10/23 5:19:34
基于OpenCV轮廓检测的植物叶片识别与特征提取实战 简介Python-OpenCV 植物叶片识别实验文档是一份面向计算机视觉初学者的课程实验材料主要针对图像分割、表示与特征提取这一核心任务帮助读者理解如何利用 OpenCV 完成叶片区域的轮廓检测与形状分析。压缩包内仅含 1 个 doc 格式文档体积约 331KB内容按实验目的、实验要求、原理讲解与代码示例组织适合作为图像处理课程的配套参考资料。文档系统介绍了 cv2.findContours()、cv2.drawContours() 等常用函数并通过具体代码演示二值化、阈值处理、轮廓查找与绘制的完整流程同时详细讲解了轮廓的面积、周长、矩、质心、边界框、最小外接圆等特征的计算方法并比较了直边界矩形与旋转边界矩形在适用场景上的区别还补充了长宽比、矩形面积比、凸包面积比等扩展特征。已有 5414 人浏览/学习该资源这一热度说明它在植物叶片识别入门和 OpenCV 轮廓特征学习方面具有较好的实践参考价值。按文档完成实验后读者可具备独立编写叶片图像轮廓提取与特征计算脚本的能力为后续更复杂的图像分析任务打下基础。1. 植物叶片识别项目的第一关先把 OpenCV 轮廓玩明白做 Python-opencv 植物叶片识别时最容易走的弯路是上来就套深度学习。某高校图像处理课上那份叶片实验恰恰用的是相反的思路不训练任何模型只靠 cv2.findContours 找轮廓、cv2.moments 算特征再加上长宽比、凸包面积比、极点位置就能把 leaf.jpg 到 leaf5.jpg 分成不同类。这套资源适合正在做课程设计、需要快速交付一个可演示叶片分类程序的人它的核心价值不在算法新颖而在于把图像分割、表示与特征提取的完整链条用 6 张真实叶片图串成了一条能跑通、能改参数、能写进报告的逻辑。我拆这套实验时最大的感触是轮廓相关函数的版本坑、阈值坑、索引坑比算法本身更能决定你今晚能不能跑出结果。2. 轮廓查找与绘制二值化预处理和 cv2.findContours 参数详解2.1 为什么必须先做二值化黑底白物原则找轮廓之前必须做阈值化或者 Canny 边缘检测这不是流程仪式而是 findContours 的工作原理决定的。它从一张二值图里提取白色物体的边界所以你给它的图里要找的叶片必须是白色背景必须是黑色。OpenCV 对这种操作有一个很形象的描述查找轮廓就像在黑色背景中找白色物体。实验材料里的 leaf.jpg 是典型的白底绿叶直接读灰度图后做全局阈值处理就行。这里有个新手最容易忽略的细节cv2.threshold 的 THRESH_BINARY 模式会把大于阈值的像素置为 255白色小于阈值的置为 0黑色。如果白底绿叶直接套这个模式背景反而变成白色叶片变成黑色轮廓提取方向就反了。我一般会先打印灰度直方图看一眼或者干脆用 THRESH_BINARY_INV 反转这两种处理在下面的代码里都能看到。import cv2 import numpy as np img cv2.imread(leaf.jpg) # BGR 彩色图 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转灰度 # 127 是硬阈值THRESH_BINARY 会把 127 的像素置 255 ret, thresh cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 如果叶片是深色、背景是浅色用下面这行替代上一行 # ret, thresh cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) cv2.imshow(gray, gray) cv2.imshow(thresh, thresh) cv2.waitKey(0) cv2.destroyAllWindows()这段代码的逻辑很直接imread 读进来的是三通道 BGR 图cvtColor 转成单通道灰度threshold 把灰度图变成黑白二值图。阈值 127 是经验值意思是亮度大于 127 的像素判为白小于等于 127 判为黑。cv2.waitKey(0) 是让窗口停住按任意键继续这一步在交互调试时非常有用别省。2.2 findContours 的三个参数与检索模式选择cv2.findContours 是整套实验的地基三个参数分别控制输入图像、轮廓检索模式、轮廓近似方法。实验报告里有人图省事写成 cv2.findContours(thresh, 1, 2)用数字代替枚举这在语法上能跑但工程里我强烈建议写完整的枚举名不然三个月后自己都看不懂当初的 1 和 2 是什么意思。# OpenCV 3.x 写法返回 image, contours, hierarchy 三个值 _, contours, hierarchy cv2.findContours( thresh, cv2.RETR_EXTERNAL, # 只取最外层轮廓忽略内部孔洞 cv2.CHAIN_APPROX_SIMPLE # 压缩冗余点只保留端点 ) print(type(contours), len(contours))contours 是一个 Python 列表每个元素是一个 Numpy 数组形状是 (N, 1, 2)N 是轮廓点数每个点存 x、y 坐标。RETR_EXTERNAL 是检索模式只拿外层轮廓适合叶片这种主体目标如果你要统计叶片上的虫洞、叶脉这类内部结构才需要换成 RETR_TREE 拿到层级关系。CHAIN_APPROX_SIMPLE 是近似方法它把一条直线上的点压缩成首尾两个端点极大减少内存占用如果换成 CHAIN_APPROX_NONE所有边界点都会保留轮廓点数会多一个数量级但后续算特征时几乎没有精度收益。这里必须提醒一个版本差异。OpenCV 3.x 的 findContours 返回三个值OpenCV 4.x 只返回 contours, hierarchy 两个值。同样一行代码换个环境就报ValueError: not enough values to unpack这是这个实验里最常见的翻车现场。兼容写法我在第 5 章的避坑部分会给出。2.3 绘制轮廓画到副本上别污染原图实验指导里特意强调查找轮廓的函数会修改原始图像这个说法在 OpenCV 3.x 下是真的——findContours 的底层实现会修改输入图像所以官方建议把原图存到其他变量里。虽然 4.x 不再修改输入但保留这个习惯没有坏处。canvas img.copy() # 复制一份再画原图留给后续步骤 cv2.drawContours(canvas, contours, -1, (0, 0, 255), 2) cv2.imshow(contours, canvas) cv2.waitKey(0) cv2.destroyAllWindows()drawContours 的第二个参数传轮廓列表第三个参数是索引-1 表示绘制全部轮廓如果只想画第 0 个轮廓写成 0 即可。颜色是 (0, 0, 255) 的 BGR 元组这里是大红色厚度 2 是线宽像素。绘制在 img.copy() 上而不是 img 上是为了后面还要用原始图做边界框叠加不被轮廓线污染。参数取值作用modeRETR_EXTERNAL只取最外层轮廓适合叶片主体modeRETR_TREE提取全部轮廓并建立层级关系适合带虫洞叶片modeRETR_LIST提取全部轮廓但不建层级速度最快methodCHAIN_APPROX_SIMPLE压缩直线段只存端点推荐methodCHAIN_APPROX_NONE保存全部边界点点数多3. 叶片特征量化用图像矩、面积和周长描述一片叶子3.1 图像矩m00 是面积m10/m01 找质心图像矩是从概率统计里借来的概念把轮廓像素当分布零阶矩是总质量面积一阶矩除以零阶矩就是质心。cv2.moments() 返回一个字典这是整个实验里性价比最高的一个函数——一次调用面积、质心全都有了还不用额外遍历像素。# 取面积最大的那个轮廓通常是叶片主体 cnt max(contours, keycv2.contourArea) M cv2.moments(cnt) print(M[m00]) # 0 阶矩轮廓面积单位是像素 cx int(M[m10] / M[m00]) # 质心 x 坐标 cy int(M[m01] / M[m00]) # 质心 y 坐标 # 在质心画一个小圆 cv2.circle(img.copy(), (cx, cy), 5, (0, 255, 0), -1)逻辑在这里很顺moments 先算出所有阶的矩再用 m10/m00 和 m01/m00 归一化得到质心坐标。这一步对后面的叶片分类有实际意义——不同种类的叶片质心相对几何中心的位置不一样比如尖头叶的质心会偏向叶尖一侧圆叶的质心基本居中。int() 取整是因为画圆函数需要整数坐标。3.2 面积与周长两个函数各自的隐藏细节面积和周长是轮廓最基础的两个量但 cv2.contourArea 有个冷知识它返回的是带符号值符号取决于轮廓点的排列方向是顺时针还是逆时针。绝大多数场景下取绝对值即可。周长则要注意 cv2.arcLength 的第二参数True 表示曲线闭合计算时会自动把首尾点连上False 表示当成一条开放的曲线差了一条边的距离。area abs(cv2.contourArea(cnt)) # abs 去掉方向符号 perimeter cv2.arcLength(cnt, True) # True 表示轮廓是闭合的 print(farea{area}, perimeter{perimeter}) # 圆形度面积与周长的平方比圆最大 circularity 4 * np.pi * area / (perimeter * perimeter) print(fcircularity{circularity})圆形度这个复合特征值得多说两句。它无量化单位不受图像缩放影响阔叶的圆形度通常大于 0.5细长叶、针叶会掉到 0.3 以下。实验里的 leaf1.jpg 和 leaf2.jpg 如果一张是卵形叶一张是披针形叶靠这个量就能粗分。area 和 perimeter 是像素单位的绝对值同一个叶片拍近拍远数值差很多所以分类时一定要用无量纲比值这是实验指导里没明说但实际最重要的经验。3.3 把特征拉成表6 张叶片图的批量测试实验要求对 leaf.jpg 到 leaf5.jpg 全部跑一遍并记录结果。手工一张张跑太慢我习惯写一个循环把每张图的轮廓特征打印成一行文本顺便过滤掉噪声小轮廓。import glob for path in glob.glob(leaf*.jpg): img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: continue c max(contours, keycv2.contourArea) a cv2.contourArea(c) p cv2.arcLength(c, True) if a 500: # 过滤面积小于 500 像素的噪点 continue circ 4 * np.pi * a / (p * p) print(f{path}: area{a:.0f}, perm{p:.0f}, circularity{circ:.3f})这段代码里出现了两个工程上的讲究。一是用 Otsu 自动阈值替代固定 127glob 循环里每张图的亮度分布不一样写死的阈值早晚会翻车二是 max(contours, keycv2.contourArea) 取最大轮廓把背景碎块、阴影噪点挡在门外。print 的浮点格式化也顺手把输出压成一行多张图横向对比时非常直观。4. 外接几何直边界框、旋转矩形和最小外接圆的取舍4.1 直边界框boundingRect 适合竖直摆放的叶片边界框是轮廓的包围盒实验里讲了直边界框和旋转边界框两种。cv2.boundingRect 返回的是一个与坐标轴对齐的矩形不管叶片怎么旋转它都按 x、y 轴方向框。这个矩形的面积不一定是包围这个轮廓的最小矩形但胜在计算快、直观适合叶片本身接近竖直摆放的场景。x, y, w, h cv2.boundingRect(cnt) img_box img.copy() cv2.rectangle(img_box, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(bounding rect, img_box) cv2.waitKey(0)x、y 是矩形左上角坐标w、h 是宽和高。cv2.rectangle 接收左上角和右下角两个点所以右下角要自己算成 (xw, yh)。这一步的逻辑就是把几何信息转成绘图坐标后面算长宽比时 w/h 直接用这两个值。4.2 旋转边界框minAreaRect 与 boxPoints 的版本坑旋转边界框考虑叶片旋转用 cv2.minAreaRect 求的是最小面积矩形。这个函数返回一个 Box2D 结构包含中心坐标、宽高和旋转角度。但要注意学校里老教材写的是 cv2.cv.BoxPointsOpenCV 3.x 之后改成 cv2.boxPoints名字少了中间那层 cv很多人抄老代码直接报 AttributeError。rect cv2.minAreaRect(cnt) # 返回 ((cx,cy), (w,h), angle) box cv2.boxPoints(rect) # 得到 4 个角点浮点坐标 box np.int0(box) # 转成整数供 drawContours 使用 img_rot img.copy() cv2.drawContours(img_rot, [box], 0, (0, 0, 255), 2) cv2.imshow(rotated rect, img_rot) cv2.waitKey(0)boxPoints 返回的 4 个角点是无序的drawContours 需要你把它作为轮廓列表传进去。np.int0 是把浮点数组截断成整数这个转换不做的话drawContours 会报类型错误这是旋转框绘制里最常见的报错点。旋转框的特点是面积最小适合斜着放的叶片比如 leaf3.jpg 如果是 45 度角拍的直框会框进大量空白区域旋转框能贴得更紧。4.3 最小外接圆与椭圆方向尺度归一化的起点cv2.minEnclosingCircle 找的是能包住轮廓的最小圆返回圆心和半径。它的价值不只是画一个圆更在于提供了一个和叶片旋转无关的尺度基准——用圆的半径做归一化长宽比、面积这些特征就能在不同拍摄距离下对比。(x, y), radius cv2.minEnclosingCircle(cnt) center (int(x), int(y)) radius int(radius) img_circle img.copy() cv2.circle(img_circle, center, radius, (255, 0, 0), 2) cv2.imshow(min enclosing circle, img_circle) cv2.waitKey(0) # 椭圆拟合得到长短轴和旋转角 ellipse cv2.fitEllipse(cnt) cv2.ellipse(img.copy(), ellipse, (0, 255, 255), 2)fitEllipse 返回 (中心点, 长短轴, 旋转角)它能把叶片拟合成一个椭圆长短轴之比直接反映叶片是细长还是圆润旋转角反映叶片的朝向。实验 2.6.5 里要求做对象的方向就是靠这个函数。实际用的时候注意轮廓点数至少要 5 个以上才能做椭圆拟合面积滤波之后的叶片轮廓通常有几百个点完全够。外接方式函数特点适用场景直边界框boundingRect与坐标轴对齐计算快叶片竖直放置旋转边界框minAreaRect boxPoints面积最小贴合旋转目标斜拍叶片最小外接圆minEnclosingCircle尺度归一化基准不同距离拍摄对比椭圆拟合fitEllipse长短轴 方向角叶片朝向和细长度5. 叶片分类常见问题五个高频翻车点对照排查5.1 阈值选错叶片和背景糊成一片现象leaf.jpg 跑出来轮廓画满了整张图叶片边缘根本找不到或者找出来的叶片轮廓比实际大好几十倍。原因白底绿叶的图直接套 THRESH_BINARY背景被置成 255 白色叶片被置成 0 黑色而 findContours 只认白色物体等于在找背景而不是找叶片。解决先看灰度直方图再定阈值最省事的做法是直接用 Otsu 自动阈值或者手动用 THRESH_BINARY_INV 反转。我一般会对同一批实验图写死一个函数内部统一走 Otsu# Otsu 自动阈值避免每张图手动调 127 ret, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)参数含义0 是阈值初值Otsu 会忽略它算法自动在灰度直方图上找最优分割点。注意要加 cv2.THRESH_OTSU 这个标志位光写 THRESH_BINARY 它不会自动算阈值。5.2 findContours 返回值数量随 OpenCV 版本变化现象同一份代码在自己电脑上跑得好好的换到同学电脑上报ValueError: not enough values to unpack或者反过来多了一个值。原因OpenCV 3.x 的 findContours 返回 image, contours, hierarchy 三个值4.x 返回 contours, hierarchy 两个值。实验报告里的示例代码是 3.x 写法你的环境是 4.x 就直接翻车。解决用 try/except 兼容两个版本或者直接按 4.x 写法接收两个值3.x 环境下用下划线丢第一个try: contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) except ValueError: _, contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)这个写法丑但稳。实验报告交付时最好在开头注明 OpenCV 版本不然评测老师的环境和你不一致程序连启动都做不到。5.3 背景噪点被当成轮廓特征全被污染现象drawContours 之后画面上多了十几个小轮廓有的是边框阴影有的是阈值分割留下的碎块。max(contours, keycv2.contourArea) 取到的最大轮廓其实是背景区域的一部分。原因光照不均匀、叶片边缘有阴影、或者原始图本身背景就不干净。阈值处理只管亮度不管连通性背景里的暗斑一旦和叶片粘连就会被当作同一个轮廓处理。解决加面积滤波只保留面积大于阈值的轮廓。更稳妥的做法是先找所有轮廓再按面积排序取前几个不要默认 contours[0] 是目标contours [c for c in contours if cv2.contourArea(c) 800] if not contours: continue cnt max(contours, keycv2.contourArea)面积门槛 800 是经验值和图片分辨率有关。如果叶片占满全图门槛可以放到 5000如果叶片很小800 可能把目标也滤掉。调参时打印出所有轮廓的面积列表一眼就能看出该设多少。5.4 minAreaRect 的绘制结果错位或报错现象旋转矩形画出来的位置完全不对或者报TypeError: function takes exactly 2 arguments之类的错误。原因两个问题叠加。一是 cv2.boxPoints 返回的是浮点坐标drawContours 要求整数二是 2.x 老代码里的 cv2.cv.BoxPoints 在新版本里不存在。解决统一用 cv2.boxPoints输出后立刻 np.int0 转整数。还有一个隐藏细节boxPoints 返回的 4 个点顺序不是固定顺序画出来可能出现菱形交叉这时把绘制的轮廓索引从 0 改成 -1或者把 box 数组按顺序重新排列都能解决。5.5 掩模提取像素点失败轮廓没有填充现象2.6.6 节里用 mask 提取轮廓内像素点结果 np.nonzero(mask) 只返回了边框一圈的点根本没有内部像素。原因cv2.drawContours 的厚度参数默认是 1只画轮廓线不填充内部。掩模操作需要的是整个轮廓内部的像素必须把厚度设为 -1 表示填充。解决把厚度参数写成 -1并且注意掩模的尺寸要和原灰度图一致mask np.zeros(gray.shape, np.uint8) cv2.drawContours(mask, [cnt], 0, 255, -1) # -1 表示填充 # 之后就能用掩模做各种统计了 mean_val cv2.mean(img, maskmask) # 叶片平均颜色 min_val, max_val, min_loc, max_loc cv2.minMaxLoc(gray, maskmask)这个坑的隐蔽之处在于不报错只是结果不对。很多人看到像素点只有一圈会以为是轮廓检测的问题折腾半天才发现是填充参数没写对。从那以后我每次写 drawContours 到掩模上都会条件反射地检查厚度参数是不是 -1。6. 把轮廓性质装进轻量分类器朝向归一化与阈值表实验的最后要求是综合多个叶片特征做自动分类。我倾向于不训练模型直接把第 3、4 章算出来的特征拼成一个特征向量用阈值查表判断。OpenCV 的轮廓性质里有一组现成的无量纲特征正好适合做这件事。特征计算方式物理含义长宽比boundingRect 的 w/h叶片细长程度extent轮廓面积 / 边界框面积叶片占边界框的饱满度solidity轮廓面积 / 凸包面积叶缘锯齿、缺刻程度等效直径sqrt(4*area/pi)和面积相等的圆直径椭圆长短轴比fitEllipse 返回的 MA/ma整体细长度分类器的逻辑可以做成一个逐级判断的树先算 solidity如果小于 0.8 说明叶缘有明显缺刻再算长宽比大于 2 划到细长叶最后用圆形度区分卵形叶和近圆形叶。每一级判断都打印出特征值方便调试。视频输入时取每一帧的最大轮廓帧间抖动只需要做简单的面积平滑分类阈值不需要跟着变。我自己的验收习惯是用摄像头对准一张叶片图程序输出类别后再把轮廓、边界框、外接圆画在同一帧上肉眼确认特征值和实际叶片形状是否对得上。这个步骤看起来麻烦但能一次暴露二值化方向、轮廓索引、归一化尺度三个层面的问题。最后补一个血泪教训这套实验里所有的阈值和面积门槛都不要写死在代码深处统一放到文件开头用变量定义。从那次把阈值写死翻车之后我每次复现这套流程都强制先看一眼灰度直方图用 Otsu 兜底再进 findContours最后才调分类阈值。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询