当前位置: 首页 > news >正文

OpenCV-图像预处理③【图像梯度计算、边缘检测算法(如 Canny)、轮廓提取与分析、凸包特征检测,以及 轮廓的外接几何特征(如最小外接矩形、外接圆等)】

文章目录

  • 先言
  • 一、图像梯度:边缘的基础
    • 1.梯度的数学定义(Mathematical Definition of Gradient)
    • 2.filter2D函数二维卷积操作
    • 3.sobel算子(计算水平和垂直梯度)
    • 4.Laplacian算子
  • 二、.Canny 边缘检测的原理与步骤(高斯滤波、非极大值抑制、双阈值检测)
    • 1高斯滤波
    • 2.非极大值抑制
    • 1.双阈值检测
    • 2.API实现(cv.Canny)
  • 三、图像轮廓提取
    • 1.二值化图像中的轮廓发现(cv2.findContours())
    • 2.轮廓层级关系(mode)和图像存储方式(method)
        • mode参数
        • method参数
    • 3.轮廓的绘制(cv2.drawContours)
  • 四、凸包与凸性检测
    • 1.凸包的检测方法与几何意义
      • 1.1穷举法
      • 1.2QuickHull法
    • 2.凸包点的获取(cv2.convexHull)与绘制(cv2.polylines)
  • 五、轮廓的外接几何特征
    • 1.外接矩形与最小外接矩形
    • 2.最小外接圆
  • 结语


先言

在计算机视觉中,边缘与轮廓是图像分析的核心要素。无论是目标检测、形状识别还是工业质检,理解如何提取和描述图像中的结构特征至关重要。OpenCV 提供了强大的工具,帮助我们高效完成这些任务。

本文将深入探讨 图像梯度计算边缘检测算法(如 Canny)、轮廓提取与分析、凸包特征检测,以及 轮廓的外接几何特征(如最小外接矩形、外接圆等)。通过理论讲解和代码实践,带您掌握如何利用 OpenCV 提取和描述图像中的关键结构信息,为后续的目标识别与测量打下基础。


一、图像梯度:边缘的基础

如果你还记得高数中用一阶导数来求极值的话,就很容易理解了:把图片想象成连续函数,因为边缘部分的像素值是与旁边像素明显有区别的,所以对图片局部求极值,就可以得到整幅图片的边缘信息了。不过图片是二维的离散函数,导数就变成了差分,这个差分就称为图像的梯度。

1.梯度的数学定义(Mathematical Definition of Gradient)

在数学上,梯度是一个向量,表示函数在某一点处的最大变化率方向及其大小。对于二维图像 I(x,y),梯度 I定义为:
在这里插入图片描述
梯度的幅值(Gradient Magnitude)表示变化的强度,计算方式为:
在这里插入图片描述
梯度的方向(Gradient Direction)表示变化的方向,计算方式为:
在这里插入图片描述
幅值大:表示该点像素值变化剧烈(可能是边缘)。

方向:垂直于边缘方向(边缘的法向量)。

2.filter2D函数二维卷积操作

cv2.filter2D(src, ddepth, kernel)

filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许自定义卷积核(kernel)来实现各种图像处理效果,如平滑、锐化、边缘检测等

  • src: 输入图像,一般为numpy数组。
  • ddepth: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。
  • kernel: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权和。

代码如下(示例):

import cv2 as cv
import numpy as np
# 定义卷积核,
kernel=np.array([[-1,0,1],[-2,0,2],[-1,0,1]],dtype=np.float32)
# 二维卷积操作
img2=cv.filter2D(img,-1,kernel)
#读图
shudu = cv.imread("../images/shudu.png")
cv.imshow("shudu",shudu)
#二维卷积操作垂直边缘检测
shudu2 = cv.filter2D(shudu,-1,kernel)
cv.imshow("shudu2",shudu2)
#矩阵转置实现水平边缘检测
shudu3 = cv.filter2D(shudu,-1,kernel.T)
cv.imshow("shudu3",shudu3)
#饱和运算
shudu4 = cv.add(shudu2,shudu3)
cv.imshow("shudu4",shudu4)
cv.waitKey(0)
cv.destroyAllWindows()

执行效果:
在这里插入图片描述

3.sobel算子(计算水平和垂直梯度)

sobel_image = cv2.Sobel(src, ddepth, dx, dy, ksize)

src:这是输入图像,通常应该是一个灰度图像(单通道图像),因为 Sobel 算子是基于像素亮度梯度计算的。在彩色图像的情况下,通常需要先将其转换为灰度图像。

ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。

dx,dy:当组合为dx=1,dy=0时求x方向的一阶导数,在这里,设置为1意味着我们想要计算图像在水平方向(x轴)的梯度。当组合为 dx=0,dy=1时求y方向的一阶导数(如果同时为1,通常得不到想要的结果,想两个方向都处理的比较好 学习使用后面的算子)

ksize:Sobel算子的大小,可选择3、5、7,默认为3。

很多人疑问,Sobel算子的卷积核这几个值是怎么来的呢?事实上,并没有规定,你可以用你自己的。比如,最初只利用邻域间的原始差值来检测边缘的Prewitt算子:

k=[−101−101−101]k=\left[{\begin{array}{r r r}{-1}&{0}&{1}\\ {-1}&{0}&{1}\\ {-1}&{0}&{1}\end{array}}\right] k=111000111
还有比Sobel更好用的Scharr算子,大家可以了解下:
k=[−303−10010−303]k=\left[{\begin{array}{r r r}{-3}&{0}&{3}\\ {-10}&{0}&{10}\\ {-3}&{0}&{3}\end{array}}\right] k=31030003103
代码示例:

import cv2 as cv
import numpy as np#读图
shudu = cv.imread("../images/shudu.png",cv.IMREAD_GRAYSCALE)
cv.imshow("shudu",shudu)
#sobel算子 cv.Sobel(src,ddepth,dx,dy,ksize[,scale[,delta[,borderType]]])
#dx = 1 dy = 0 水平方向差分 垂直边缘检测
dst = cv.Sobel(shudu,-1,1,0,ksize=3)
cv.imshow("sobel",dst)
#dx = 0 dy = 1 垂直方向差分 水平边缘检测
dst2 = cv.Sobel(shudu,-1,0,1,ksize=3)
cv.imshow("sobel2",dst2)
dst3 = cv.Sobel(shudu,-1,1,1,ksize=3)
cv.imshow("sobel3",dst3)
cv.waitKey(0)
cv.destroyAllWindows()

边缘检测效果:
在这里插入图片描述
通过图像效果得出结论:sobel算子不适合同时对水平和垂直方向进行检测

4.Laplacian算子

和sobel算子不同的是,Laplacian算子对图像的检测通过两个因素:对图像的梯度值和梯度的变换大小实现对xy两个方向进行检测:
Laplacian滤波核:
k=[0101−41010]k=\left[\begin{array}{c c c}{0}&{1}&{0}\\ {1}&{-4}&{1}\\ {0}&{1}&{0}\end{array}\right] k=010141010
这是Laplacian算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓展为:
k=[1111−81111]k=\left[\begin{array}{c c c}{1}&{1}&{1}\\ {1}&{-8}&{1}\\ {1}&{1}&{1}\end{array}\right] k=111181111
cv2.Laplacian(src, ddepth)

src:这是输入图像

ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
代码示例:

import cv2 as cv
import numpy as np#读图
shudu = cv.imread("../images/shudu.png",cv.IMREAD_GRAYSCALE)
cv.imshow("shudu",shudu)
#laplacian算子 laplacian = cv.Laplacian(img,ddepth,ksize=None,scale=1,delta=0,border)
dst = cv.Laplacian(shudu,-1,ksize=3)
cv.imshow("laplacian",dst)
cv.waitKey(0)
cv.destroyAllWindows()

检测效果:
在这里插入图片描述

二、.Canny 边缘检测的原理与步骤(高斯滤波、非极大值抑制、双阈值检测)

1高斯滤波

边缘检测本身属于锐化操作,对噪点比较敏感,所以需要进行平滑处理。这里使用的是一个5*5的高斯核对图像进行消除噪声。上一个实验中已经介绍了高斯滤波的具体过程,这里就不再过多叙述不明白的可以查看往期文章OpenCV-图像预处理➁【图像插值方法、边缘填充策略、图像矫正、掩膜应用、水印添加,图像的噪点消除】。

2.非极大值抑制

在上述梯度的数学定义(Mathematical Definition of Gradient)有讲到图像梯度的梯度的幅值(Gradient Magnitude)与梯度的方向(Gradient Direction)那我们边可以通过角度范围对图像的梯度进行边缘方向的确定:

a). 并且如果梯度方向不是0°、45°、90°、135°这种特定角度,那么就要用到插值算法来计算当前像素点在其方向上进行插值的结果了,然后进行比较并判断是否保留该像素点。这里使用的是单线性插值,通过A1和A2两个像素点获得dTmp1与dTmp2处的插值,然后与中心点C进行比较(非极大值抑制)。具体的插值算法请参考图像旋转实验。

b). 得到θ\thetaθ的值之后,就可以对边缘方向进行分类,为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、45°方向、135°方向。并且:

θ\thetaθ值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;

当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;

当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;

当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;

得到每个边缘的方向之后,其实把它们连起来边缘检测就算完了,但是为什么还有这一步与下一步呢?是因为经过第二步得到的边缘不经过处理是没办法使用的,因为高斯滤波的原因,边缘会变得模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作,而非极大值抑制就是一个很好的方法,它会对得到的边缘像素进行一个排除,使边缘尽可能细一点。

在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。

1.双阈值检测

经过非极大值抑制之后,我们还需要设置阈值来进行筛选,当阈值设的太低,就会出现假边缘,而阈值设的太高,一些较弱的边缘就会被丢掉,因此使用了双阈值来进行筛选,推荐高低阈值的比例为2:1到3:1之间,其原理如下图所示:
代码如下(示例):
在这里插入图片描述
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。

2.API实现(cv.Canny)

edges = cv.Canny(image, threshold1, threshold2),即使读到的是彩色图也可以进行处理。

  • image:输入的灰度/二值化图像数据。

  • threshold1:低阈值,用于决定可能的边缘点。

  • threshold2:高阈值,用于决定强边缘点。

代码如下(示例):

import cv2 as cv
import numpy as np
#读图
shudu = cv.imread("../images/demo.png")
cv.imshow("shudu",shudu)
#转灰度图
gary = cv.cvtColor(shudu,cv.COLOR_BGR2GRAY)
# 二值化处理
# binary = cv.adaptiveThreshold(gary,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,9,2)
#使用Canny(Thresholdlow,Thresholdtop)边缘检测
edges = cv.Canny(gary,2,3)
cv.imshow("edges",edges)
cv.waitKey(0)
cv.destroyAllWindows()

该处Canay方法内部逻辑已经对图像进行过高斯滤波降噪,也可以试试高斯加权二值化看看什么效果,最终结果如下:
在这里插入图片描述

三、图像轮廓提取

轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。轮廓是一个闭合的、封闭的形状。

轮廓的作用

  • 形状分析
  • 目标识别
  • 图像分割

1.二值化图像中的轮廓发现(cv2.findContours())

寻找轮廓需要将图像做一个二值化处理,并且根据图像的不同选择不同的二值化方法来将图像中要绘制轮廓的部分置为白色,其余部分置为黑色。也就是说,我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色,之后对图像中的像素进行遍历,当一个白色像素相邻(上下左右及两条对角线)位置有黑色像素存在或者一个黑色像素相邻(上下左右及两条对角线)位置有白色像素存在时,那么该像素点就会被认定为边界像素点,轮廓就是有无数个这样的边界点组成的。

下面具体介绍一下cv2.findContours()函数,其函数原型为:

contours,hierarchy = cv2.findContours(image,mode,method)

  • 返回值:[ 轮廓点坐标 ] 和 [ 层级关系 ]。

  • contours:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。

  • hierarchy:表示轮廓之间的关系。对于第i条轮廓,hierarchy[i][0]hierarchy[i][0]hierarchy[i][0], hierarchy[i][1]hierarchy[i][1]hierarchy[i][1] , hierarchy[i][2]hierarchy[i][2]hierarchy[i][2] ,$ hierarchy[i][3]$分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。

  • image:表示输入的二值化图像。

  • mode:表示轮廓的检索模式。

  • method:轮廓的表示方法。

2.轮廓层级关系(mode)和图像存储方式(method)

mode参数

轮廓查找方式。返回不同的层级关系。

mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。

  1. RETR_EXTERNAL

表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。

2.3.4.会查找所有轮廓,但会有层级关系。

  1. RETR_LIST

表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。

RETR_CCOMP

表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。

RETR_CCOMP 模式下,轮廓被分为两个层级:

  • 层级 0:所有外部轮廓(最外层的边界)。
  • 层级 1:所有内部轮廓(孔洞或嵌套的区域)。

RETR_TREE

表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。

method参数

轮廓存储方法。轮廓近似方法。决定如何简化轮廓点的数量。就是找到轮廓后怎么去存储这些点。

method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。

  • CHAIN_APPROX_NONE表示将所有的轮廓点都进行存储

  • CHAIN_APPROX_SIMPLE表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;

对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。

3.轮廓的绘制(cv2.drawContours)

cv2.drawContours(image, contours, contourIdx, color, thickness)

  • image:原始图像,一般为单通道或三通道的 numpy 数组。
  • contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
  • contourIdx:要绘制的轮廓索引。如果设为 -1,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。
  • color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
  • thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
    示例代码:
import cv2 as cv
import numpy as np#读图
num = cv.imread("../images/num.png")
cv.imshow("shudu",num)
#转灰度图
gary = cv.cvtColor(num,cv.COLOR_BGR2GRAY)
# 二值化处理
_,binary = cv.threshold(gary,125,255,cv.THRESH_BINARY_INV)
cv.imshow("binary",binary)
#轮廓检测
contours,hierarchy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
print(contours, hierarchy)
#绘制轮廓drawContours(img,contours,idx,color,thickness)
cv.drawContours(num,contours,-1,(0,0,255),thickness=2)
cv.imshow("num",num)
cv.waitKey(0)
cv.destroyAllWindows()

执行效果:
在这里插入图片描述

四、凸包与凸性检测

在进行凸包特征检测之前,首先要了解什么是凸包。通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。

一般来说,凸包都是伴随着某类点集存在的,也被称为某个点集的凸包。

对于一个点集来说,如果该点集存在凸包,那么这个点集里面的所有点要么在凸包上,要么在凸包内。

凸包检测常用在物体识别、手势识别、边界检测等领域。

1.凸包的检测方法与几何意义

1.1穷举法

  • 将集中的点进行两两配对,并进行连线,对于每条直线,检查其余所有的点是否处于该直线的同一侧,如果是,那么说明构成该直线的两个点就是凸包点,其余的线依次进行计算,从而获取所有的凸包点。

  • 用向量的思想,点都是有坐标的,连起来就可以构成一个向量。再以其中一个点,连接另一个点,构成另一个向量,让两个向量做外积,就是叉积。也就是std=∣向量a∣∗∣向量b∣∗sin(θ)std=|向量a|*|向量b|*sin(\theta)std=向量a向量bsin(θ),能控制stdstdstd的正负的只能是θ\thetaθ,如果计算出来的stdstdstd的正负都相同,说明这些点都在这条直线的同一侧,那么这两个点就是凸包的边界点。然后换两个点,就是说换一条直线,换一个向量,继续进行检测,直到找到凸包的所有的边界点。

  • 缺点:时间复杂度高,不断使用for循环,耗时。

在这里插入图片描述

1.2QuickHull法

  • 将所有点放在二维坐标系中,找到横坐标最小和最大的两个点P1P_1P1P2P_2P2并连线。此时整个点集被分为两部分,直线上为上包,直线下为下包。
    我们以点集来举例,假如有这么一些点,其分布如下图所示:

在这里插入图片描述
我们还是以上面的点集为例,假设我们知道这些点的坐标,那么我们就可以找出处于最左边和最右边的点,如下图所示:
在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接着将这两个点连接,并将点集分为上半区和下半区,我们以上半区为例:

在这里插入图片描述

找到上面这些点离直线最远的点,其中,这条直线由于有两个点的坐标,所以其表示的直线方程是已知的,并且上面的点的坐标也是已知的,那么我们就可以根据点到直线的距离公式来进行计算哪个点到直线的距离最远,假设直线的方程为:Ax+By+C=0A x+B y+C=0Ax+By+C=0,那么点(x0,y0)(x_{0},y_{0})(x0,y0)到直线的距离公式为:
d=∣Ax0+By0+C∣A2+B2d={\frac{|A x_{0}+B y_{0}+C|}{\sqrt{A^{2}+B^{2}}}} d=A2+B2Ax0+By0+C
然后我们就可以得到距离这条线最远的点,将其与左右两点连起来,并分别命名为y1和y2,如下图所示:

在这里插入图片描述

然后分别根据点的坐标求出y1和y2的直线方程,之后将上半区的每个点的坐标带入下面公式中:
d=Ax0+By0+CA2+B2d={\frac{A x_{0}+B y_{0}+C}{\sqrt{A^{2}+B^{2}}}} d=A2+B2Ax0+By0+C
当d=0时,表明该点在直线上;当d>0时,表明点在直线的上方,在这里就代表该点在上图所围成的三角形的外面,也就意味着该三角形并没有完全包围住上半区的所有点,需要重新寻找凸包点;当d<0时,表明点在直线的下方,在这里就代表该点在上图所围成的三角形的里面,也就代表着这类点就不用管了。

当出现d>0时,我们需要将出现这种结果的两个计算对象:某点和y1或y2这条线标记,并在最后重新计算出现这种现象的点集到y1或y2的距离来获取新的凸包点的坐标。在本例子中,也就是如下图所示的点和y2这条直线:

在这里插入图片描述

2.凸包点的获取(cv2.convexHull)与绘制(cv2.polylines)

获取凸包点

cv2.convexHull(points)

  • points:输入参数,图像的轮廓

绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)

  • image:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。
  • pts:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。
  • isClosed:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。
  • color:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。
  • thickness(可选):线条宽度,默认值为1。

示例代码:

import cv2 as cv
import numpy as np#读图
tu = cv.imread("../images/tu.png")
cv.imshow("shudu",tu)
#转灰度图
gary = cv.cvtColor(tu,cv.COLOR_BGR2GRAY)
# 二值化处理
_,binary = cv.threshold(gary,11,255,cv.THRESH_BINARY)
cv.imshow("binary",binary)
#查找轮廓
contours,hierarchy=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
print(contours)
#获取凸包
hull = cv.convexHull(contours[0])
cv.polylines(tu,[hull],True,(0,0,255),2)
cv.imshow("tu",tu)
cv.waitKey(0)
cv.destroyAllWindows()

执行效果:
在这里插入图片描述

五、轮廓的外接几何特征

  • 图像轮廓特征查找其实就是他的外接轮廓。
  • 应用:
    • 图像分割
    • 形状分析
    • 物体检测与识别
  • 根据轮廓点进行,所以要先找到轮廓。
    • 先灰度化、二值化。目标物体白色,非目标物体黑色,选择合适的儿值化方式。
  • 有了轮廓点就可以找到最上、最下、最左、最右的四个坐标,Xmin、Xmax、Ymin、YmaxX_{min}、X_{max}、Y_{min}、Y_{max}XminXmaxYminYmax。就可以绘制出矩形。

1.外接矩形与最小外接矩形

外接矩形

  • boundingRect(轮廓点)
    

最小外接矩形

  • rect = cv2.minAreaRect(cnt)
    

API调用步骤

  • contours, hierarchy = cv2.findContours(image_np_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

contours为二值图像上查找所有的外部轮廓

  • rect = cv2.minAreaRect(cnt)

传入的cnt参数为contours中的轮廓,可以遍历contours中的所有轮廓,然后计算出每个轮廓的小面积外接矩形

  • rect 是计算轮廓最小面积外接矩形:rect 结构通常包含中心点坐标 (x, y)、宽度 width、高度 height 和旋转角度 angle

cv2.boxPoints(rect).astype(int)

cv2.boxPoints(rect)返回 是一个形状为 4行2列的数组,每一行代表一个点的坐标(x, y),顺序按照逆时针或顺时针方向排列

将最小外接矩形转换为边界框的四个角点,并转换为整数坐标

cv2.drawContours(image, contours, contourIdx, color, thickness)

  • image:原图像,一般为 numpy 数组,通常为灰度或彩色图像。
  • contours:一个包含多个轮廓的列表,可以用上一个api得到的 [box]
  • contourIdx:要绘制的轮廓索引。如果设置为 -1,则绘制所有轮廓。
  • color:轮廓的颜色,可以是 BGR 颜色格式的三元组,例如 (0, 0, 255) 表示红色。
  • thickness:轮廓线的粗细,如果是正数,则绘制实线;如果是 0,则绘制轮廓点;如果是负数,则填充轮廓内部区域。
import cv2 as cv
import numpy as np#读图
num = cv.imread("../images/num.png")
cv.imshow("num",num)
#转灰度图
gary = cv.cvtColor(num,cv.COLOR_BGR2GRAY)
# 二值化处理
_,binary = cv.threshold(gary,190,255,cv.THRESH_BINARY_INV)
cv.imshow("binary",binary)
#查找轮廓
contours,hierarchy=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
#遍历轮廓绘制外接矩形
for conts in contours:x,y,w,h = cv.boundingRect(conts)cv.rectangle(gary,(x,y),(x+w,y+h),(0,0,255),2,cv.LINE_AA)
cv.imshow("gary",gary)
#最小外接矩形
for conts in contours:rect = cv.minAreaRect(conts)box = cv.boxPoints(rect).astype(int)print( box)cv.drawContours(num,[box],-1,(0,0,255),2)cv.imshow("num",num)
cv.waitKey(0)
cv.destroyAllWindows()

执行效果:
在这里插入图片描述

2.最小外接圆

cv2.minEnclosingCircle(points) -> (center, radius)

参数说明:

  • points:输入参数图片轮廓数据

返回值:

  • center:一个包含圆心坐标的二元组 (x, y)
  • radius:浮点数类型,表示计算得到的最小覆盖圆的半径。

cv2.circle(img, center, radius, color, thickness)

  • img:输入图像,通常是一个numpy数组,代表要绘制圆形的图像。
  • center:一个二元组 (x, y),表示圆心的坐标位置。
  • radius:整型或浮点型数值,表示圆的半径长度。
  • color:颜色标识,可以是BGR格式的三元组 (B, G, R),例如 (255, 0, 0) 表示红色。
  • thickness:整数,表示圆边框的宽度。如果设置为 -1,则会填充整个圆。

代码示例:

import cv2 as cv
import numpy as np#读图
num = cv.imread("../images/num.png")
cv.imshow("num",num)
#转灰度图
gary = cv.cvtColor(num,cv.COLOR_BGR2GRAY)
# 二值化处理
_,binary = cv.threshold(gary,190,255,cv.THRESH_BINARY_INV)
cv.imshow("binary",binary)
#查找轮廓
contours,hierarchy=cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
print(contours[0])
#最小外接圆
#根据轮廓点获取圆形的圆心和半径
center,radius = cv.minEnclosingCircle(contours[0])
#根据圆心和半径绘制圆形
cv.circle(num,np.int32(center),np.int32(radius),(0,255,0),2)
cv.imshow("num",num)
cv.waitKey(0)
cv.destroyAllWindows()

绘制效果:
在这里插入图片描述


结语

边缘与轮廓是计算机视觉的“骨架”,理解它们的提取与分析方法,能显著提升图像理解能力。本文从 梯度计算 到 轮廓特征描述,系统介绍了 OpenCV 的核心工具,并提供了可落地的代码示例。

🔍 动手挑战: 尝试用 Canny 边缘检测和轮廓分析,自动测量一张图片中多个物体的长宽比例!

🚀 下期预告: 我们将探讨 霍夫变换与模板匹配,进一步解锁 OpenCV 的高级特征分析能力!

http://www.dtcms.com/a/301427.html

相关文章:

  • 硅基计划3.0 学习总结 肆 二叉树 初版
  • [每周一更]-(第148期):使用 Go 进行网页抓取:Colly 与 Goquery 的对比与思路
  • QT---概览
  • 优化Linux高并发:文件描述符与端口范围的协同调优
  • SPSC无锁环形队列技术(C++)
  • FreeRTOS—空闲任务
  • 【Python系列】Flask 应用中的主动垃圾回收
  • idea打开后project窗口未显示项目名称的解决方案
  • LangGraph快速入门项目部署
  • C++ 中实现 `Task::WhenAll` 和 `Task::WhenAny` 的两种方案
  • 从0搭建YOLO目标检测系统:实战项目+完整流程+界面开发(附源码)
  • jenkins只能运行2个任务,提示:“等待下一个可用的执行器”
  • Redis C++客户端——命令使用
  • 实战演练1:实战演练之命名实体识别
  • Docker 的数据持久化-数据卷
  • (AC)架子鼓
  • 基于Java的KTV点歌系统的设计与实现
  • 【CF】Day112——杂题 (逆向思维 | 二分 + 贪心 | 单调队列优化DP | 二进制 + 前缀和 | 二分图判断 | 暴力枚举)
  • JavaEE--3.多线程
  • python-装饰器
  • 【ST表、倍增】P7167 [eJOI 2020] Fountain (Day1)
  • QT6 源,七章对话框与多窗体(15)多文档 MDI 窗体 QMdiArea 篇一:属性,公共成员函数,信号与槽函数
  • 多智能体架构
  • 《计算机组成原理与汇编语言程序设计》实验报告四 Debug及指令测试
  • setnonblocking函数用途和使用案例
  • 在本地环境中运行 ‘dom-distiller‘ GitHub 库的完整指南
  • OSPF路由协议 多区域
  • 【ESP32】无法找到: “${env:IDF_PATH}/components/“的路径报错问题以及CMAKE构建不成功问题
  • Cursor报错解决【持续更新中】
  • 金融科技中的远程开户、海外个人客户在线开户、企业客户远程开户