目录
一、图像梯度处理
图像梯度处理是计算机视觉和图像处理中的一项基本技术,通过计算像素强度的变化来识别图像中的边缘。检测图像中的轮廓。梯度反映了图像像素强度的变化率和方向。
1、边缘提取
边缘是图像中像素值发生剧烈变化的区域,数学上对应于一阶导数的极值点或二阶导数的过零点。边缘提取的核心是检测这些变化的位置。
卷积是图像处理中最核心的数学运算之一,用于实现滤波、边缘检测、模糊、锐化等多种操作。
滤波是应用卷积来实现的,卷积的关键就是卷积核,
1.1垂直边缘
反映图像在水平方向(x轴)的强度突变(如左侧暗右侧亮的边界)。
$$
k1=\left[\begin{array}{c c c}{{-1}}&{{0}}&{{1}}\\ {{-2}}&{{0}}&{{2}}\\ {{-1}}&{{0}}&{{1}}\end{array}\right]
$$
这个核是用来提取图片中的垂直边缘的,怎么做到的呢?看下图:
当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。
示例:
import cv2 as cv import numpy as np #读取图片 shu = cv.imread('./images/shudu.png',cv.IMREAD_GRAYSCALE) cv.imshow("shu",shu) #定义卷积核 #垂直边缘提取 kernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]],dtype=np.float32) #卷积 dst = cv.filter2D(shu,-1,kernel) cv.imshow("dst",dst) cv.waitKey(0) cv.destroyAllWindows()
1.2水平边缘
反映图像在垂直方向(y轴)的强度突变(如上方暗下方亮的边界)。
同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就称为图像的梯度计算:
$$
k2=\left[\begin{array}{c c c}{{-1}}&{{-2}}&{{-1}}\\ {{0}}&{{0}}&{{0}}\\ {{1}}&{{2}}&{{1}}\end{array}\right]
$$
kernel.T
的作用是 对垂直边缘检测核 kernel
进行转置(Transpose),从而将其转换为水平边缘检测核。
示例:
import cv2 as cv import numpy as np #读取图片 shu = cv.imread('./images/shudu.png',cv.IMREAD_GRAYSCALE) cv.imshow("shu",shu) #定义卷积核 #水平边缘提取 kernel= np.array([[-1,-2,-1],[0,0,0],[1,2,1]]) dst = cv.filter2D(shu,-1,kernel) cv.imshow("dst",dst) cv.waitKey(0) cv.destroyAllWindows() 或者: import cv2 as cv import numpy as np #读取图片 shu = cv.imread('./images/shudu.png',cv.IMREAD_GRAYSCALE) cv.imshow("shu",shu) #定义卷积核 #垂直边缘提取 kernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]],dtype=np.float32) #卷积 dst = cv.filter2D(shu,-1,kernel) cv.imshow("dst",dst) #水平边缘提取 kernel.T dst2 = cv.filter2D(shu,-1,kernel.T) cv.imshow("dst2",dst2) cv.waitKey(0) cv.destroyAllWindows()
cv2.filter2D(src, ddepth, kernel)
filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许自定义卷积核(kernel)来实现各种图像处理效果,如平滑、锐化、边缘检测等
-
src
: 输入图像,一般为numpy
数组。 -
ddepth
: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。 -
kernel
: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权和。 -
先用数组模拟一下
import cv2 as cv import numpy as np # 模拟一张图像,灰度图 img=np.array([[100,102,109,110,98,20,19,18,21,22], [109,101,98,108,102,20,21,19,20,21], [109,102,105,108,98,20,22,19,19,18], [109,98,102,108,102,20,23,19,20,22], [109,102,105,108,98,20,22,19,20,18], [100,102,108,110,98,20,19,18,21,22], [109,101,98,108,102,20,22,19,20,21], [109,102,108,108,98,20,22,19,19,18], ],dtype=np.float32) # 定义卷积核, kernel=np.array([[-1,0,1], [-2,0,2], [-1,0,1]],dtype=np.float32) # 二维卷积操作 img2=cv.filter2D(img,-1,kernel) # 打印卷积后的图 print(img2)
2、Sobel算子(常用)
sobel算子: 一阶微分算子,结合高斯平滑和差分,抗噪性较好
-
平滑效果减少噪声干扰,但边缘可能变粗。
-
对对角线边缘响应较弱(因核的旋转对称性不足)。
-
常用于实时系统(计算效率高)。
上面的两个卷积核都叫做Sobel算子,只是方向不同,它先在垂直方向计算梯度:
$$
G_{x}=k_{1}\times s r c
$$
再在水平方向计算梯度:
$$
G_{y}=k_{2}\times s r c
$$
最后求出总梯度:
$$
G={\sqrt{G x^{2}+G y^{2}}}
$$
在梯度处理方式这个组件中,当参数filter_method选择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=\left[{\begin{array}{r r r}{-1}&{0}&{1}\\ {-1}&{0}&{1}\\ {-1}&{0}&{1}\end{array}}\right]
$$
还有比Sobel更好用的Scharr算子,大家可以了解下:
$$
k=\left[{\begin{array}{r r r}{-3}&{0}&{3}\\ {-10}&{0}&{10}\\ {-3}&{0}&{3}\end{array}}\right]
$$
这些算法都是一阶边缘检测的代表,网上也有算子之间的对比资料。
示例:
import cv2 as cv # 读取图片 shu = cv.imread('./images/shudu.png',cv.IMREAD_GRAYSCALE) cv.imshow("shu",shu) #sobel算子 # dx=1 dy=0 水平方向差分,提取垂直边缘 dst = cv.Sobel(shu,-1,1,0,ksize=3) cv.imshow("dst",dst) # dy=1 dx=0 垂直方向差分,提取水平边缘 dst2 = cv.Sobel(shu,-1,0,1,ksize=3) cv.imshow("dst2",dst2) cv.waitKey(0) cv.destroyAllWindows()
3、Laplacian算子
Laplacian 算子是一种基于二阶导数的边缘检测算子,用于突出图像中的快速强度变化区域(如边缘、角点、噪声等)。与一阶算子(如 Sobel)不同,Laplacian 直接计算梯度的散度,对图像中的极值点(如边缘的零交叉点)更敏感。
高数中用一阶导数求极值,在这些极值的地方,二阶导数为0,所以也可以通过求二阶导计算梯度:
$$
d s t={\frac{\partial^{2}f}{\partial x^{2}}}+{\frac{\partial^{2}f}{\partial y^{2}}}
$$
一维的一阶和二阶差分公式分别为:
$$
{\frac{\partial f}{\partial x}}=f(x+1)-f(x)
$$
$$
{\frac{\partial^{2}f}{\partial x^{2}}}=f(x+1)+f(x-1)-2f(x)
$$
提取前面的系数,那么一维的Laplacian滤波核是:
$$
k=[1~~-2~~~1]
$$
而对于二维函数f(x,y),两个方向的二阶差分分别是:
$$
{\frac{\partial^{2}f}{\partial x^{2}}}=f(x+1,y)+f(x-1,y)-2f(x,y)
$$
$$
{\frac{\partial^{2}f}{\partial y^{2}}}=f(x,y+1)+f(x,y-1)-2f(x,y)
$$
合在一起就是:
$$
V^{2}f(x,y)=f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1)-4f(x,y)
$$
同样提取前面的系数,那么二维的Laplacian滤波核就是:
$$
k=\left[\begin{array}{c c c}{0}&{1}&{0}\\ {1}&{-4}&{1}\\ {0}&{1}&{0}\end{array}\right]
$$
这就是Laplacian算子的图像卷积模板,有些资料中在此基础上考虑斜对角情况,将卷积核拓展为:
$$
k=\left[\begin{array}{c c c}{1}&{1}&{1}\\ {1}&{-8}&{1}\\ {1}&{1}&{1}\end{array}\right]
$$
cv2.Laplacian(src, ddepth)
src:这是输入图像
ddepth:这个参数代表输出图像的深度,即输出图像的数据类型。在 OpenCV 中,-1 表示输出图像的深度与输入图像相同。
示例:
import cv2 as cv #读取图片 shu = cv.imread('./images/shudu.png',cv.IMREAD_GRAYSCALE) cv.imshow("shu",shu) #Laplacian算子 dst = cv.Laplacian(shu,-1,ksize=3) cv.imshow("dst",dst) cv.waitKey(0) cv.destroyAllWindows()
二、图像边缘检测
1、高斯滤波
目的:平滑图像,减少噪声对梯度计算的干扰。去除噪点 操作:使用高斯核进行卷积,权重按距离中心点的标准差(σ)分布。
import cv2 as cv lv=cv.imread("./images/1.jpg") #转为灰度图 gray = cv.cvtColor(lv,cv.COLOR_BGR2GRAY) #高斯滤波 dst = cv.GaussianBlur(gray,(5,5),1) cv.imshow("blurred",dst) cv.waitKey(0) cv.destroyAllWindows()
2、计算图像的梯度与方向
目的:检测图像中强度变化的方向和幅度。 操作:使用 Sobel 算子计算水平和垂直梯度(Gx, Gy),再合成梯度幅值和方向。
这里使用了sobel算子来计算图像的梯度值,在上一章节中,我们了解到sobel算子其实就是一个核值固定的卷积核,如下所示:
$$
sobel(水平方向)=\left[\begin{array}{c c c}{{-1}}&{{0}}&{{1}}\\ {{-2}}&{{0}}&{{2}}\\ {{-1}}&{{0}}&{{1}}\end{array}\right]
$$
$$
sobel(垂直方向)=\left[\begin{array}{c c c}{{-1}}&{{-2}}&{{-1}}\\ {{0}}&{{0}}&{{0}}\\ {{1}}&{{2}}&{{1}}\end{array}\right]
$$
首先使用sobel算子计算中心像素点的两个方向上的梯度G_{x}和G_{y},然后就能够得到其具体的梯度值:
$$
G={\sqrt{G_{x}{}^{2}+G_{y}{}^{2}}}
$$
也可以使用G=|G_{x}+G_{y}|来代替。在OpenCV中,默认使用G=|G_{x}+G_{y}|来计算梯度值。
然后我们根据如下公式可以得到一个角度值
{\frac{G_{\mathrm{y}}}{G_{x}}}=\tan\,(\theta)
$$
\theta=\arctan\,({\frac{G_{\mathrm{y}}}{G_{x}}})
$$
这个角度值其实是当前边缘的梯度的方向。通过这个公式我们就可以计算出图片中所有的像素点的梯度值与梯度方向,然后根据梯度方向获取边缘的方向。
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°边缘;
3、非极大值抑制
非极大值抑制(NMS)是边缘检测(如Canny算法)中的关键步骤,其目的是细化边缘,确保检测到的边缘是单像素宽度。当前像素的梯度幅值 G 必须大于或等于沿梯度方向的两个邻域像素值,否则被抑制(置为0)。
-
核心思想:沿梯度方向比较当前像素与邻域像素的梯度幅值,仅保留局部极大值,抑制其他非极大值点。
-
数学本质:一种局部极大值搜索算法,基于梯度方向选择比较对象。
作用 | 说明 |
---|---|
边缘细化 | 将宽边缘变为单像素宽度,避免边缘模糊。 |
去除冗余响应 | 抑制梯度幅值非最大的像素,减少重复检测。 |
提升边缘定位精度 | 确保边缘位置精确到像素级,避免“双边缘”现象。 |
得到每个边缘的方向之后,其实把它们连起来边缘检测就算完了,但是为什么还有这一步与下一步呢?是因为经过第二步得到的边缘不经过处理是没办法使用的,因为高斯滤波的原因,边缘会变得模糊,导致经过第二步后得到的边缘像素点非常多,因此我们需要对其进行一些过滤操作,而非极大值抑制就是一个很好的方法,它会对得到的边缘像素进行一个排除,使边缘尽可能细一点。
在该步骤中,我们需要检查每个像素点的梯度方向上的相邻像素,并保留梯度值最大的像素,将其他像素抑制为零。假设当前像素点为(x,y),其梯度方向是0°,梯度值为G(x,y),那么我们就需要比较G(x,y)与两个相邻像素的梯度值:G(x-1,y)和G(x+1,y)。如果G(x,y)是三个值里面最大的,就保留该像素值,否则将其抑制为零。
4、双阈值筛选
双阈值筛选是Canny边缘检测的最后关键步骤,用于区分真实边缘、弱边缘和噪声,通过高低阈值组合和边缘跟踪,生成清晰的二值边缘图。
阈值类型 | 作用 | 取值建议 |
---|---|---|
高阈值 | 高于此值的像素判定为强边缘(确信边缘) | 通常取图像梯度幅值最大值的15%~20% |
低阈值 | 低于此值的像素直接丢弃(视为噪声或背景) | 通常为高阈值的40%~50% |
中间区域 | 介于低阈值和高阈值之间的像素标记为弱边缘(可能属于边缘,需进一步验证) |
-
去除噪声:低阈值过滤微小梯度变化(如噪声)。
-
保留真实边缘:高阈值确保强边缘不被漏检。
-
边缘连接:通过弱边缘与强边缘的连通性,补充断裂的边缘。
-
当某一像素位置的幅值超过最高阈值时,该像素必是边缘像素;当幅值低于最低像素时,该像素必不是边缘像素;幅值处于最高像素与最低像素之间时,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素,否则就不会被认为是边缘。也就是说,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C就是边缘。
edges = cv2.Canny(image, threshold1, threshold2)
-
image
:输入的灰度/二值化图像数据。 -
threshold1
:低阈值,用于决定可能的边缘点。 -
threshold2
:高阈值,用于决定强边缘点。
示例:
import cv2 as cv #读图 shu = cv.imread('./images/shudu.png') cv.imshow("shu",shu) #灰度化处理 gray = cv.cvtColor(shu,cv.COLOR_BGR2GRAY) #二值化处理 _,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY) #使用canny边缘检测 edges = cv.Canny(binary,30,70) cv.imshow("edges",edges) #显示效果 cv.waitKey(0) cv.destroyAllWindows()
三、绘制图像轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。轮廓是一个闭合的、封闭的形状。
-
轮廓的作用:
-
形状分析
-
目标识别
-
图像分割
-
函数 | 作用 |
---|---|
cv2.findContours() | 从二值图像中查找轮廓 |
cv2.drawContours() | 在图像上绘制轮廓 |
cv2.approxPolyDP() | 轮廓多边形近似(简化轮廓点) |
1、寻找轮廓
在OpenCV中,使用cv2.findContours()来进行寻找轮廓。
寻找轮廓需要将图像做一个二值化处理,并且根据图像的不同选择不同的二值化方法来将图像中要绘制轮廓的部分置为白色,其余部分置为黑色。也就是说,我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色,如下图所示。
之后,对图像中的像素进行遍历,当一个白色像素相邻(上下左右及两条对角线)位置有黑色像素存在或者一个黑色像素相邻(上下左右及两条对角线)位置有白色像素存在时,那么该像素点就会被认定为边界像素点,轮廓就是有无数个这样的边界点组成的。
下面具体介绍一下cv2.findContours()函数,其函数原型为:
contours,hierarchy = cv2.findContours(image,mode,method)
-
返回值:[ 轮廓点坐标 ] 和 [ 层级关系 ]。
-
contours:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。
-
hierarchy:表示轮廓之间的关系。对于第i条轮廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。
-
image:表示输入的二值化图像。
-
mode:表示轮廓的检索模式。
-
method:轮廓的表示方法。
import cv2 as cv #读图 img = cv.imread('./images/num.png') #灰度化 gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) #二值化 反阈值法 _,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY_INV) #查找轮廓 **contours,hierarchy = cv2.findContours(image,mode,method) contours,hierarchy = cv.findContours(binary,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) print(len(contours)) cv.waitKey(0) cv.destroyAllWindows()
1、mode参数
包括:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE
1.RETR_EXTERNAL
表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
-
RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
在 RETR_CCOMP
模式下,轮廓被分为两个层级:
-
层级 0:所有外部轮廓(最外层的边界)。
-
层级 1:所有内部轮廓(孔洞或嵌套的区域)。
RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
2、method参数
轮廓存储方法。轮廓近似方法。决定如何简化轮廓点的数量。就是找到轮廓后怎么去存储这些点。
method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
1. CHAIN_APPROX_NONE
-
作用:保存轮廓上的所有点,不进行任何近似。
-
特点:
-
存储完整:保留轮廓的每一个像素位置。
-
内存占用高:尤其对长轮廓或复杂形状。
-
-
适用场景:
-
需要精确的轮廓点坐标(如高精度测量)。
-
后续操作依赖完整轮廓点(如点集分析)
-
2. CHAIN_APPROX_SIMPLE
-
作用:压缩水平、垂直、对角线方向的冗余点,仅保留端点。
-
特点:
-
内存高效:删除直线上的中间点。
-
保持几何形状:对矩形、多边形等简单形状效果显著。
-
-
适用场景:
-
常规物体检测(如矩形框、多边形识别)。
-
需要减少数据量的实时应用。
-
3. CHAIN_APPROX_TC89_L1
/ TC89_KCOS
-
作用:使用Teh-Chin链式近似算法(L1或KCOS准则)进一步简化轮廓。
-
算法差异:
-
TC89_L1
:基于L1距离(曼哈顿距离)近似。 -
TC89_KCOS
:基于余弦相似度近似,更平滑。
-
-
特点:
-
高度压缩:比
SIMPLE
更激进,适合复杂曲线。 -
可能损失细节:对锯齿状边缘不友好。
-
-
适用场景:
-
自然物体轮廓(如圆形、不规则形状)。
-
对存储空间极度敏感的场景。
-
存储方式 | 内存占用 | 精度 | 典型应用场景 | |
---|---|---|---|---|
CHAIN_APPROX_NONE | 保留所有点 | 高 | 最高 | 高精度测量、医学图像分析 |
CHAIN_APPROX_SIMPLE | 压缩直线冗余点 | 中 | 中等 | 物体检测、工业质检 |
CHAIN_APPROX_TC89_* | 算法优化近似 | 低 | 较低 | 自然物体识别、移动端应用 |
对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。
2、绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
-
image:原始图像,一般为单通道或三通道的 numpy 数组。
-
contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
-
contourIdx:要绘制的轮廓索引。如果设为
-1
,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。 -
color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
-
thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
示例;
import cv2 as cv #读图 img = cv.imread('./images/num.png') #灰度化 gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) #二值化 反阈值法 _,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY_INV) #查找轮廓 **contours,hierarchy = cv2.findContours(image,mode,method) contours,hierarchy = cv.findContours(binary,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) print(len(contours)) cv.waitKey(0) cv.destroyAllWindows()
四、凸包特征检测
凸包(Convex Hull)是计算机视觉中用于描述物体形状的重要工具,它表示能完全包含目标轮廓的最小凸多边形。凸包检测广泛应用于形状分析、手势识别、物体分类等领域。
一般来说,凸包都是伴随着某类点集存在的,也被称为某个点集的凸包。
对于一个点集来说,如果该点集存在凸包,那么这个点集里面的所有点要么在凸包上,要么在凸包内。
凸包检测常用在物体识别、手势识别、边界检测等领域。
-
穷举法
-
QuickHull法
1. 穷举法 (Brute Force)
基本思想
穷举法通过检查所有可能的点对组合,判断它们是否构成凸包的边界。
算法步骤
-
遍历所有点对:对于点集S中的每对点(p, q)
-
检查凸包边界条件:
-
所有其他点都在线段pq的同一侧
-
或所有其他点都位于线段pq上
-
-
收集边界点:满足条件的点对(p,q)即为凸包边界
时间复杂度
-
检查每对点:O(n²)
-
每对点检查其他点:O(n)
-
总复杂度:O(n³)
优点 | 缺点 |
---|---|
实现简单 | 时间复杂度高(O(n³)) |
无需特殊数据结构 | 不适用于大型点集 |
教学价值高 | 产生重复点 |
2. QuickHull法
基本思想
QuickHull算法采用分治策略,灵感来自快速排序:
-
找到横坐标最小和横坐标最大的两个点P1和P2构成初始分界线
-
递归处理分界线两侧的点集
-
合并结果形成凸包
算法步骤
-
寻找极点:
-
找x最小点A和x最大点B
-
将点集分为AB上侧和AB下侧两个子集
-
-
递归处理:
-
以上包为例,找到上包中的点距离该直线最远的点P_3,连线并寻找直线P1P3左侧的点和P2P3右侧的点,然后重复本步骤,直到找不到为止。对下包也是这样操作。
-
-
合并结果:
hull = [A] + findHull(upper, A, B) + [B] + findHull(lower, B, A)
-
A 和 B:初始的两个极点(通常是 x 坐标最小和最大的点)
-
upper:位于线段 AB 上方的点集
-
lower:位于线段 AB 下方的点集
-
findHull():递归求解凸包的函数
-
优点 | 缺点 |
---|---|
平均效率高 | 最坏情况O(n²) |
适合非均匀分布 | 递归深度问题 |
易于实现 | 浮点精度敏感 |
示例:
import cv2 as cv #读图 tu = cv.imread("./images/tu.png") #灰度化 gray = cv.cvtColor(tu,cv.COLOR_BGR2GRAY) #二值化 _,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY) #查找轮廓 conts,th = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_NONE) #获取凸包点 hull = cv.convexHull(conts[0]) #print(hull) #绘制凸包 cv.polylines(tu,[hull],True,(0,255,0),1) cv.imshow("tu",tu) cv.waitKey(0) cv.destroyAllWindows()