OpenCV和图像处理(三)

本文以下OpenCV都简写成"cv2"的形式,所有img都默认为一张图片
关联文章:
OpenCV和图像处理(一)

OpenCV和图像处理(二)

OpenCV和图像处理(四)

OpenCV和图像处理(五)

OpenCV和图像处理(六)

七、边缘与轮廓
1、图像梯度
1-1:Scharr 滤波器

Scharr 滤波器:Sobel 滤波器的改良版。
S x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] S y = [ − 3 − 10 − 3 0 0 0 3 10 3 ] 计 算 梯 度 幅 值 和 方 向 S = S x 2 + S y 2 θ = a r c t a n ( S y S x ) S_x=\left[ \begin{matrix} -3&0&3\\ -10&0&10\\ -3&0&3 \end{matrix} \right] \quad S_y=\left[ \begin{matrix} -3&-10&-3\\ 0&0&0\\ 3&10&3 \end{matrix} \right]\\ 计算梯度幅值和方向\\ S=\sqrt{S_x^2+S_y^2}\quadθ=arctan(\frac{S_y}{S_x}) Sx=31030003103Sy=30310010303S=Sx2+Sy2 θ=arctan(SxSy)
很明显,Scharr 滤波器把 Sobel 滤波器的参数"增强"了,所以效果会更显著一些。

此外,为了加速计算,可以简化幅值:
∣ S ∣ = ∣ S x ∣ + ∣ S y ∣ |S|=|S_x|+|S_y| S=Sx+Sy
在数据相对小的时候,误差也不大,但是计算比平方和再开根号就快多了。

​ 特别地,Sx在处理横向的梯度的时候,也在纵向方向上有类似"高斯"的功能;Sy则反过来。所以

Scharr 滤波器能有"低通模糊"的功效。

 # 可以对比上文 Sobel 滤波器,注意这里没有ksize参数。
scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
1-2:Laplacian 滤波器

Laplacian 梯度滤波器是求图像的二阶差分, 二阶差分在亮的一边是负的,在暗的一边是正的。常数部分为零。可以用来确定边的准确位置,以及像素在亮的一侧还是暗的一侧。可假设其离散实现类似于二阶Sobel导数。事实的确如此,OpenCV在计算拉普拉斯算子时直接使用Sobel算子。
L ( I m g ) = ∂ 2 I m g ∂ x 2 + ∂ 2 I m g ∂ y 2 L(Img)=\frac{∂^2Img}{∂x^2}+\frac{∂^2Img}{∂y^2} L(Img)=x22Img+y22Img
上面是微分的公式,先看一下一维一阶差分和二阶差分的公式:
∂ f ∂ x i = f ( x i + 1 ) − f ( x i ) ∂ 2 f ∂ x i 2 = f ( x i + 1 ) + f ( x i − 1 ) − 2 f ( x i ) \frac{∂f}{∂x_i}=f(x_{i+1})-f(x_i)\\ \frac{∂^2f}{∂x_i^2}=f(x_{i+1})+f(x_{i-1})-2f(x_i) xif=f(xi+1)f(xi)xi22f=f(xi+1)+f(xi1)2f(xi)
由于图像是二维的,所以(x,y)两个方向上的二阶差分分别为:
∂ 2 f ∂ x i 2 = f ( x i + 1 , y i ) + f ( x i − 1 , y i ) − 2 f ( x i , y i ) ∂ 2 f ∂ y i 2 = f ( x i , y i + 1 ) + f ( x i , y i − 1 ) − 2 f ( x i , y i ) \frac{∂^2f}{∂x_i^2}=f(x_{i+1},y_i)+f(x_{i-1},y_i)-2f(x_i,y_i)\\ \frac{∂^2f}{∂y_i^2}=f(x_i,y_{i+1})+f(x_i,y_{i-1})-2f(x_i,y_i) xi22f=f(xi+1,yi)+f(xi1,yi)2f(xi,yi)yi22f=f(xi,yi+1)+f(xi,yi1)2f(xi,yi)
所以 Laplacian 算子的差分形式为:
▽ 2 f ( x i , y i ) = f ( x i + 1 , y i ) + f ( x i − 1 , y i ) + f ( x i , y i + 1 ) + f ( x i , y i − 1 ) − 4 f ( x i , y i ) ▽^2f(x_i,y_i)=f(x_{i+1},y_i)+f(x_{i-1},y_i)+ f(x_i,y_{i+1})+f(x_i,y_{i-1})-4f(x_i,y_i) 2f(xi,yi)=f(xi+1,yi)+f(xi1,yi)+f(xi,yi+1)+f(xi,yi1)4f(xi,yi)
写成滤波器形式(3x3)为:
L a p l a c i a n 算 子 k e r n e l = [ 0 1 0 1 − 4 1 0 1 0 ] Laplacian算子kernel= \left[ \begin{matrix} 0&1&0\\ 1&-4&1\\ 0&1&0 \end{matrix} \right] Laplaciankernel=010141010

# 可以对比上文 Sobel 滤波器,这里可以只需要两个参数,并且没有xy分量了
laplacian = cv2.Laplacian(img, cv2.CV_64F)
2、Canny边缘提取算法

Canny算法首先需要经过高斯模糊去噪,减少不重要特征,然后再调用Canny方法。

img = cv2.GaussianBlur(img, (5, 5), 0)
canny = cv2.Canny(img, threshold1, threshold2)
r'''
注意到Canny有两个阈值属性:tr1和tr2
其中较大的tr2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,
可能是断断续续的。所以这时候用较小的第一个阈值tr1用于将这些间断的边缘连接起来。
函数返回的是二值图,包含检测出的边缘
'''
2-1:非极大值抑制

这是一种边缘稀疏技术,作用在于"瘦"边。可以想象成我找了很多很多的结果,然后通过非极大值抑制,找到了相对最好的结果,而其他一般的结果我就丢掉了,达到"瘦"和"稀疏"的效果。具体如下:

1.将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。

2.如果当前像素梯度强度与另外两个像素相比最大,则保留为边缘点;否则该像素点将被抑制。

2-2:双阈值边缘连接处理

这个是判断哪些些边缘需要保留,哪些边缘需要丢弃的。cv2.Canny中两个阈值就是梯度大小。假设有个坐标系,横轴代表像素[0-255],纵轴代表梯度大小,则两个阈值(maxVal,minVal)就相当于两条平行横轴的直线,然后连接每一个像素当前的梯度值,就形成了一条一条的梯度曲线。然后根据以下规则判断:

1.保留梯度曲线大于maxVal的部分对应的边缘。

2.如果曲线全部大于minVal,且有部分在maxVal和minVal之间,但是两头都大于maxVal,即"出头了",则保留全部曲线对应的边缘。

3、轮廓

轮廓和边缘不同,边缘厚度一般为1,即不能再薄了。可能是很多线条构成的,更有可能是断开的。而轮廓肯定是一些闭合的线条,更具有层次感。

3-1:轮廓查找和绘制
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
r'''
首先,该操作会修改原图,不想改原图需要拷贝一份
三个输入参数:输入的(二值)图像,轮廓检索方式,轮廓近似方法
=====================================================================================
1.轮廓检索方式
cv2.RETR_EXTERNAL	只检测外轮廓
cv2.RETR_LIST		检测的轮廓不建立等级关系
cv2.RETR_CCOMP		建立两个等级的轮廓,上面一层为外边界,里面一层为内孔的边界信息
cv2.RETR_TREE		建立一个等级树结构的轮廓
-------------------------------------------------------------------------------------
2.轮廓近似办法
cv2.CHAIN_APPROX_NONE		存储所有边界点
cv2.CHAIN_APPROX_SIMPLE		压缩垂直、水平、对角方向,只保留端点
cv2.CHAIN_APPROX_TX89_L1	使用teh-Chini近似算法
cv2.CHAIN_APPROX_TC89_KCOS	使用teh-Chini近似算法
=====================================================================================
返回的是轮廓的点contours和轮廓直接的从属关系hierarchy
'''

img_contour = cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
r'''
参数说明:
    img		    表示输入的需要画的图片
    contours	表示轮廓值
    -1		    表示轮廓的索引,-1为全部都画
    (0, 0, 255)  表示颜色(这是红色)
    3            表示线条粗细,-1表示用颜色填充整个轮廓
'''
3-2:面积、周长和重心
ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
M = cv2.moments(contours[0])  # 矩,参数是一个数组,此时选择一组轮廓传入,返回一个字典

cx, cy = int(M['m10'] / M['m00']), int(M['m01'] / M['m00']) # 这是公式
print("重心:", cx, cy)

area = cv2.contourArea(contours[0]) # 传入轮廓,返回面积。(上面的M['m00']的值也是面积)
print("面积:", area)

perimeter = cv2.arcLength(contours[0], True) # 传入轮廓,返回周长。参数2表示轮廓是否封闭
print("周长:", perimeter)

关于图像的"矩",这里简单介绍一下,具体可以见 @zhouzongzong 的文章:图像中矩的概念

普通矩的计算:
m p q = ∫ − ∞ + ∞ x p y q f ( x , y ) d x d y p , q = 0 , 1 , 2... m_{pq}=\int_{-∞}^{+∞}{x^py^qf(x,y)}dxdy\quad p,q=0,1,2... mpq=+xpyqf(x,y)dxdyp,q=0,1,2...
图像是二维信号,将上面公式离散化:
m p q = ∑ x ∑ y x p y q f ( x , y ) 0 阶 矩 ( m 00 ) : 目 标 区 域 的 面 积 ( A r e a ) 1 阶 矩 ( m 01 , m 10 ) : 目 标 区 域 的 质 心 ( C e n t r o i d ) 2 阶 矩 ( m 20 , m 02 , m 11 ) : 即 惯 性 矩 , 可 计 算 目 标 图 像 的 方 向 3 阶 矩 ( m 30 , m 03 , m 12 , m 21 ) : 目 标 区 域 的 方 位 和 斜 度 , 反 应 目 标 的 扭 曲 . . . . . . m_{pq}=\sum_{x}\sum_{y}{x^py^qf(x,y)}\\ 0阶矩(m_{00}):目标区域的面积(Area)\\ 1阶矩(m_{01},m_{10}):目标区域的质心(Centroid)\\ 2阶矩(m_{20},m_{02},m_{11}):即惯性矩,可计算目标图像的方向\\ 3阶矩(m_{30},m_{03},m_{12},m_{21}):目标区域的方位和斜度,反应目标的扭曲\\ ...... mpq=xyxpyqf(x,y)0(m00):(Area)1(m01,m10):(Centroid)2(m20,m02,m11):3(m30,m03,m12,m21):......
中心矩:构造平移不变性。以目标区域的质心为中心构建中心矩,那么矩的计算时永远是目标区域中的点相对于目标区域的质心,而与目标区域的位置无关,即中心矩具备了平移不变性。

3-3:轮廓近似

得到一个大体的轮廓,需要定义一个精度参数。

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
epsilon = 30 # 设定一个精度
approx = cv2.approxPolyDP(contours[0],epsilon,True)
r'''
第一个参数是传入一个轮廓
第二个参数是精度,它是从原始轮廓到近似轮廓的最大距离,是一个准确度参数
第三个参数表示轮廓是否封闭
'''
img_contour= cv2.drawContours(img, [approx], -1, (0, 255, 0), 3)
3-4:凸包和凸性检测

和轮廓类似,但不同

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
hull = cv2.convexHull(contours[0]) # 获取当前图片轮廓的最小凸集
r'''
参数解释:[一般用到第一个参数就行]
Points:是传入的轮廓;
Hull:是输出,通常我们避免它;
Clockwise:方向标志。如果是true,则顺时针输出凸包,否则逆时针方式输出;
ReturnPoints:默认true时,返回hull点的坐标;如果是false,则返回与hull点对应的轮廓点的索引。
'''

print(cv2.isContourConvex(contours[0]), cv2.isContourConvex(hull))
r'''
1.输出当前图片轮廓是否是凸的
2.输出当前图片轮廓的最小凸集是否是凸的[肯定是True]
'''
img_contour = cv2.drawContours(img, [hull], -1, (0, 0, 255), 3)

关于边界检测,常用的有:边界矩形、最小矩形、最小外切圆等。[可用来检测碰撞]

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
# 边界矩形
x, y, w, h = cv2.boundingRect(contours[0])
img_contour = cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

# 最小矩形
rect = cv2.minAreaRect(contours[0])
box = cv2.boxPoints(rect)
box = np.int0(box)
img_contour = cv2.drawContours(img, [box], 0, (0, 0, 255), 2)

# 最小外切圆
(x, y), radius = cv2.minEnclosingCircle(contours[0])
center = (int(x), int(y))
radius = int(radius)
img_contour = cv2.circle(img, center, radius, (255, 0, 0), 2)

图像的最小椭圆和拟合直线:可用于方向性的判断。

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
# 椭圆拟合
ellipse = cv2.fitEllipse(contours[0])
r'''
返回的ellipse是椭圆的[中心坐标,短轴长轴(也就是2b,2a),旋转角度]
'''
cv2.ellipse(img, ellipse, (255, 0, 0), 2)
r'''
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness, lineType, shift),可见这个方法参数是很多的,在此说明一下:
====================================================================================
    img:需要绘图的图像
    ---------------------------------------------------------------
    center:椭圆中心点坐标,int
    axes:椭圆尺寸(即长短轴),int
    angle:旋转角度(顺时针方向),可以为小数
    ---------------------[ellipse参数包含了上面三个参数]------------
    startAngle:绘制的起始角度(顺时针方向)
    endAngle:绘制的终止角度(如绘制整个椭圆是0,360;绘制下半椭圆就是0,180)
    color:线条颜色(BGR)
    thickness:线条粗细(默认值=1)
    lineType:线条类型(默认值=8),本案例没设置
    shift:圆心坐标点和数轴的精度(默认值=0),本案例没设置
====================================================================================
但是本文案例没有写这么多参数,经过本人测试,cv2.ellipse(img, ellipse, (255, 0, 0), 2)
自动将ellipse解包成了center, axes, angle参数并且没有报数据类型错误[全是float]
还将axes参数自动/2了,起始终止也默认设为0和360。可以说是简化了参数吧。
'''

# 直线拟合
h, w, _ = img.shape
[vx, vy, x, y] = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01)
r'''
points:一组轮廓
distType:距离类型==>
-----------------------------------------------------------------------
    cv2.DIST_USER:	 User defined distance
    cv2.DIST_L1: 	 distance = |x1-x2| + |y1-y2|
    cv2.DIST_L2: 	 欧式距离,此时与最小二乘法相同
    cv2.DIST_C:		 distance = max(|x1-x2|,|y1-y2|)
    cv2.DIST_L12:	 L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1))
    cv2.DIST_FAIR:	 distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998
    cv2.DIST_WELSCH: distance = c2/2(1-exp(-(x/c)2)), c = 2.9846
    cv2.DIST_HUBER:	 distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345
-----------------------------------------------------------------------
param:距离参数,跟所选的距离类型有关,值可以设置为0。
reps、aeps:用于表示拟合直线所需要的径向和角度精度,通常情况下两个值均被设定为0.01
输出值:
对于二维直线,输出output为4维,前两维代表拟合出的直线的方向,后两位代表直线上的一点。
[即通常说的点斜式直线]
'''
lefty = int((-x * vy / vx) + y)
righty = int(((w - x) * vy / vx) + y)
cv2.line(img, (w - 1, righty), (0, lefty), (0, 0, 255), 2)

最后来说一些轮廓的重要性质:

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
cnt = contours[0]

注意:下面 cnt 都是从上面两步得来的某一个轮廓

  1. 边界矩形的宽高比
    A s p e c t _ R a t i o = W i d t h H e i g h t Aspect\_Ratio=\frac{Width}{Height} Aspect_Ratio=HeightWidth

    x, y, w, h = cv2.boundingRect(cnt)
    aspect_ratio = float(w) / h
    
  2. 轮廓面积与边界矩形面积的比
    E x t e n t = O b j e c t _ A r e a B o u n d i n g _ R e c t a n g l e _ A r e a Extent=\frac{Object\_Area}{Bounding\_Rectangle\_Area} Extent=Bounding_Rectangle_AreaObject_Area

    area = cv2.contourArea(cnt)
    x, y, w, h = cv2.boundingRect(cnt)
    rect_area = w * h
    extent = float(area) / rect_area
    
  3. 轮廓面积与凸包面积的比
    S o l i d i t y = C o n t o u r _ A r e a C o n v e x _ H u l l _ A r e a Solidity=\frac{Contour\_Area}{Convex\_Hull\_Area} Solidity=Convex_Hull_AreaContour_Area

    area = cv2.contourArea(cnt)
    hull = cv2.convexHull(cnt)
    hull_area = cv2.contourArea(hull)
    extent = float(area) / hull_area
    
  4. 与轮廓面积相等的圆的直径
    E q u i v a l e n t _ D i a m e t e r = 4 × C o n t o u r _ A r e a π Equivalent\_Diameter=\sqrt{\frac{4×Contour\_Area}{π}} Equivalent_Diameter=π4×Contour_Area

    area = cv2.contourArea(cnt)
    equi_diameter = np.sqrt(4 * area / np.pi)
    
  5. 图像的方向

返回椭圆长轴和短轴的长度

(x,y),(MA,ma),angle = cv2.fitEllipse(cnt) # 详见上文"图像的最小椭圆"
3-5:对象掩码

用于获取构成对象的所有像素点,即 3-1:轮廓查找和绘制 中最后参数取-1的情况

ret, thresh = cv2.threshold(imggray, 127, 255, cv2.THRESH_BINARY) # 得到二值图
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)#得到轮廓
mask = np.zeros(imggray.shape, np.uint8) # 通过numpy生成一个和原图一样的、全为0的张量
cv2.drawContours(mask, [contours[0]], 0, 100, -1)
r'''
这里是把一组轮廓传入,并画在mask上,最后参数为-1表示是填充。
'''
pixelpoints = np.transpose(np.nonzero(mask))

# 最大值和最小值以及它们的位置
# 使用相同的掩码求一个对象的平均颜色或平均灰度  ??? 
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imggray, mask=mask)


3-6:形状匹配

函数 cv2.matchShape() 可以帮我们比较两个形状或轮廓的相似度。如果返回值越小,匹配越好。它是根据 Hu 矩来计算的。Hu 矩是归一化中心矩的线性组合,之所以这样做是为了能够获取代表图像的某个特征的矩函数,这些矩函数对某些变化如缩放,旋转,镜像映射具有不变形 。

img1 = cv2.imread('1.jpg', 0)
img2 = cv2.imread('2.jpg', 0)

ret, thresh1 = cv2.threshold(img1, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh1, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cnt1 = contours[0] # 得到图1的一条轮廓

ret, thresh2 = cv2.threshold(img2, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
cnt2 = contours[0] # 得到图2的一条轮廓

ret = cv2.matchShapes(cnt1, cnt2, 1, 0.0) # 进行匹配
print(ret) # 输出匹配度[0表示完全相同,1表示完全不同]
r'''
前两个参数分别是两个图的轮廓,第三个参数是匹配模式:
-----------------------------------------------------------------------------------
    CONTOURS_MATCH_I1 => 模式1
    CONTOURS_MATCH_I2 => 模式2
    CONTOURS_MATCH_I3 => 模式3
-----------------------------------------------------------------------------------
第四个参数是保留参数,目前无用,但是必须得写一个数:int
'''

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值