OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

这篇具有很好参考价值的文章主要介绍了OpenCV基础操作(5)图像平滑、形态学转换、图像梯度。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

一、图像平滑

1、2D卷积

我们可以对 2D 图像实施低通滤波(LPF),高通滤波(HPF)等。
LPF 帮助我们去除噪音,模糊图像。HPF 帮助我们找到图像的边缘。

OpenCV 提供的函数 cv.filter2D() 可以让我们对一幅图像进行卷积操作。

'''
下面我们将对一幅图像使用平均滤波器(kernel核中的参数和为1,所有参数值相同),

将核放在图像的一个像素 A 上,求与核对应的图像上 25(5x5)个像素的和,在取平均数,用这个平均数替代像素 A 的值。
重复以上操作直到将图像的每一个像素值都更新一遍。
'''

img = cv.imread('open_cv_logo.png')
kernel = np.ones((5,5),np.float32) / 25
print(kernel)
[[0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]
 [0.04 0.04 0.04 0.04 0.04]]
dst = cv.filter2D(img,-1,kernel)


plt.subplot(121),plt.imshow(img),plt.title('Original'),plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging'),plt.xticks([]), plt.yticks([])
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

img = cv.imread('./data/xiaoren.png')

plt.figure(figsize=(10,5))

kernel = np.array(
    [
        [1,2,1],
        [0,-8,0],
        [1,2,1]
    ]
)


dst = cv.filter2D(img,-1,kernel)


plt.subplot(121),plt.imshow(cv.cvtColor(img,cv.COLOR_BGR2RGB)),plt.title('Original'),plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(cv.cvtColor(dst,cv.COLOR_BGR2RGB)),plt.title('Define kernel'),plt.xticks([]), plt.yticks([])
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

2、图像模糊

使用低通滤波器可以达到图像模糊的目的。这对与去除噪音很有帮助。

其实就是去除图像中的高频成分(比如:噪音,边界)。所以边界也会被模糊一点。(当然,也有一些模糊技术不会模糊掉边界)。
OpenCV 提供了四种模糊技术。

'''
1、平均

这是由一个归一化卷积框完成的。用卷积框覆盖区域所有像素的平均值来代替中心元素。
可以使用函数 cv2.blur() 和 cv2.boxFilter() 来完这个任务。
我们需要设定卷积框的宽和高。

下面是一个 3x3 的归一化卷积框:
K = [
        [1,1,1],
        [1,1,1],
        [1,1,1]
    ] / 9

如果你不想使用归一化卷积框,你应该使用 cv2.boxFilter(),这时要传入参数 normalize=False。
'''
img = cv.imread('./data/xiaoren.png')
blur = cv.blur(img,ksize=(11,11))

plt.subplot(121),plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB)),plt.title('Original'),plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(cv.cvtColor(blur, cv.COLOR_BGR2RGB)),plt.title('Blurred'),plt.xticks([]), plt.yticks([])
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度


'''
2、高斯模糊


把卷积核换成高斯核(简单来说,方框不变,将原来每个方框的值是相等的,现在里面的值是符合高斯分布的,方框中心的值最大,其余方框根据
距离中心元素的距离递减,构成一个高斯小山包。原来的求平均数现在变成求加权平均数,全就是方框里的值)。

实现的函数是 cv2.GaussianBlur()。
我们需要指定高斯核的宽和高(必须是奇数)。以及高斯函数沿 X,Y 方向的标准差。

如果我们只指定了 X 方向的的标准差,Y 方向也会取相同值。如果两个标准差都是 0,那么函数会根据核函数的大小自己计算。
高斯滤波可以有效的从图像中去除高斯噪音。
'''

plt.figure(figsize=(20,10))

img = cv.imread('./data/koala.png')
#0 是指根据窗口大小(5,5)来计算高斯函数标准差
blur = cv.GaussianBlur(img, ksize=(5,5), sigmaX=10.0, sigmaY=10.0)

plt.subplot(121),plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB)),plt.title('Original'),plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(cv.cvtColor(blur, cv.COLOR_BGR2RGB)),plt.title('Blurred'),plt.xticks([]), plt.yticks([])
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
3、中值模糊

顾名思义就是用与卷积框对应像素的中值来替代中心像素的值。这个滤波器经常用来去除椒盐噪声。
(即将卷积域内的所有像素按照从小到大排序,然后获取中间值作为卷积的输出。)


前面的滤波器都是用计算得到的一个新值来取代中心像素的值,而中值滤波是用中心像素周围(也可以使他本身)的值来取代他。
他能有效的去除噪声。卷积核的大小也应该是一个奇数。
'''
# 加载图像
img = cv.imread('./data/xiaoren.png')

# 加噪声数据
noisy_img = np.random.normal(10, 10, (img.shape[0], img.shape[1], img.shape[2]))
noisy_img = np.clip(noisy_img, 0, 255).astype(np.uint8)
img = img + noisy_img

# 转换为灰度图像
img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 做一个中值过滤
dst = cv.medianBlur(img, ksize=5)

# 可视化
plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('Original')

plt.subplot(122)
plt.imshow(dst, 'gray')
plt.title('medianBlur')
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
4、双边滤波


函数 cv2.bilateralFilter() 能在保持边界清晰的情况下有效的去除噪音。

但是这种操作与其他滤波器相比会比较慢。
我们已经知道高斯滤波器是求中心点邻近区域像素的高斯加权平均值。
这种高斯滤波器只考虑像素之间的空间关系,而不会考虑像素值之间的关系(像素的相似度)。
所以这种方法不会考虑一个像素是否位于边界。因此边界也不会模糊掉,而这正不是我们想要。


双边滤波在同时使用空间高斯权重和灰度值相似性高斯权重。

空间高斯函数确保只有邻近区域的像素对中心点有影响,灰度值相似性高斯函数确保只有与中心像素灰度值相近的才会被用来做模糊运算。
所以这种方法会确保边界不会被模糊掉,因为边界处的灰度值变化比较大。
'''
# 双边滤波: 中间的纹理删除,保留边缘信息
# 加载图像
img = cv.imread('./data/xiaoren.png')

# 做一个双边滤波
dst = cv.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('bilateralFilter')
plt.show()

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

二、形态学转换

主要包括腐蚀、扩张、打开、关闭等操作;主要操作是基于kernel核的操作
常见的核主要有:矩阵、十字架、椭圆结构的kernel

kernel1 = cv.getStructuringElement(cv.MORPH_RECT, ksize=(5,5))
print("矩形kernel:\n{}".format(kernel1))

kernel2 = cv.getStructuringElement(cv.MORPH_CROSS, ksize=(5,5))
print("十字架kernel:\n{}".format(kernel2))

kernel3 = cv.getStructuringElement(cv.MORPH_ELLIPSE, ksize=(5,5))
print("椭圆kernel:\n{}".format(kernel3))
矩形kernel:
[[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]]
十字架kernel:
[[0 0 1 0 0]
 [0 0 1 0 0]
 [1 1 1 1 1]
 [0 0 1 0 0]
 [0 0 1 0 0]]
椭圆kernel:
[[0 0 1 0 0]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [0 0 1 0 0]]

1、腐蚀

腐蚀的意思是将边缘的像素点进行一些去除的操作;
腐蚀的操作过程就是让kernel核在图像上进行滑动,当内核中的所有像素被视为1时,原始图像中对应位置的像素设置为1,否则设置为0.

  • 其主要效果是:可以在图像中减少前景图像(白色区域)的厚度,有助于减少白色噪音,可以用于分离两个连接的对象
  • 一般应用与只有黑白像素的灰度情况
'''
    第一种方式
'''
kernel = cv.getStructuringElement(cv.MORPH_ERODE, (5,5))
dst = cv.morphologyEx(img, cv.MORPH_ERODE, kernel)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('erode')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
    第二种方式
'''
img = cv.imread('./data/j.png',0)


# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5),np.uint8)
# b. 腐蚀操作
dst = cv.erode(img,kernel,iterations=1,borderType=cv.BORDER_REFLECT)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('erode')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

2、扩张、膨胀

和腐蚀的操作相反,其功能是增加图像的白色区域的值

只要在kernel中所有像素中有可以视为1的像素值,那么就将原始图像中对应位置的像素值设置为1,否则设置为0。

通常情况下,在去除噪音后,可以通过扩张在恢复图像的目标区域信息。

'''
    第一种方式
'''
kernel = cv.getStructuringElement(cv.MORPH_DILATE, (5,5))
dst = cv.morphologyEx(img, cv.MORPH_DILATE, kernel)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('dilate')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
    第二种方式
'''
img = cv.imread('./data/j.png',0)


# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5),np.uint8)
# b. 膨胀操作
dst = cv.dilate(img,kernel,iterations=1,borderType=cv.BORDER_REFLECT)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('dilate')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

3、Open

Open其实指的就是先做一次腐蚀,然后再做一次扩张操作,一般用于去除噪音数据。

# 加载图像
img = cv.imread('./data/j.png', 0)

# 加载噪音数据(白色噪音)
rows, cols = img.shape
for i in range(100):
    x = np.random.randint(cols)
    y = np.random.randint(rows)
    img[y,x] = 255

'''
    第一种方式
'''
# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5),np.uint8)
# b. 先进行腐蚀操作
dst1 = cv.erode(img,kernel,iterations=1,borderType=cv.BORDER_REFLECT)
# c. 后进行膨胀操作
dst2 = cv.dilate(dst1,kernel,iterations=1,borderType=cv.BORDER_REFLECT)


# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst2, cv.COLOR_BGR2RGB))
plt.title('open')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
    第二种方式
'''
# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5), np.uint8)
# b. Open操作
dst = cv.morphologyEx(img, op=cv.MORPH_OPEN, kernel=kernel, iterations=1)


# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('open')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

4、Closing

Closing其实指的就是先做一次扩张,再做一次腐蚀
对前景图像中的如果包含黑色点,有去除的效果。

# 加载图像
img = cv.imread('./data/j.png', 0)

# 加载噪音数据
rows, cols = img.shape
# 加白色点
for i in range(100):
    x = np.random.randint(cols)
    y = np.random.randint(rows)
    img[y,x] = 255

# 加黑色点
for i in range(1000):
    x = np.random.randint(cols)
    y = np.random.randint(rows)
    img[y,x] = 0
'''
    第一种方式
'''

# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5), np.uint8)
# b. 再膨胀
dst = cv.dilate(img, kernel, iterations=1)

# c. 先腐蚀
dst = cv.erode(dst, kernel, iterations=1)



# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('open')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
    第二种方式
'''

# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5), np.uint8)
# b. Closing操作
dst = cv.morphologyEx(img, op=cv.MORPH_CLOSE, kernel=kernel, iterations=1)

# c. 可视化
# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('open')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

5、Morphological Gradient(形态梯度)

在膨胀和腐蚀图像之间获取一个差集,也就是Gradient=Dilate - Erode;

该操作的作用能够提取边缘特征信息。

img = cv.imread('./data/j.png', 0)

# a. 定义一个核(全部设置为1表示对核中5*5区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((5,5), np.uint8)

# b. 形态梯度(dilate - erode)
dst = cv.morphologyEx(img, op=cv.MORPH_GRADIENT, kernel=kernel, iterations=1)

# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original')

plt.subplot(122)
plt.imshow(cv.cvtColor(dst, cv.COLOR_BGR2RGB))
plt.title('Gradient')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

6、Top Hat

在原始图像和Open操作的图像上做一个差集,也就是Top Hat=image - Open;

该操作的主要作用是可以提取一些非交叉点的信息。一般不用。

img = cv.imread('./data/j.png', 0)

# a. 定义一个核(全部设置为1表示对核中9*9区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((9,9), np.uint8)

# b1. Open(先腐蚀,再扩展)
dst1 = cv.morphologyEx(img, op=cv.MORPH_OPEN, kernel=kernel, iterations=1)
# b2. Top Hat(src - open)
dst2 = cv.morphologyEx(img, op=cv.MORPH_TOPHAT, kernel=kernel, iterations=1)



# 可视化
plt.subplot(131)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(132)
plt.imshow(dst1, 'gray')
plt.title('Open')

plt.subplot(133)
plt.imshow(dst2, 'gray')
plt.title('TopHat')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

7、Black Hat

在Close操作的图像和原始图像上做一个差集,也就是Black Hat = Close -image;

该操作的主要作用是可以提取一些交叉点附件的位置特征信息。一般不用。

# a. 定义一个核(全部设置为1表示对核中9*9区域的所有像素均进行考虑,设置为0表示不考虑)
kernel = np.ones((9,9), np.uint8)

# b1. Close(先膨胀,再腐蚀)
dst1 = cv.morphologyEx(img, op=cv.MORPH_CLOSE, kernel=kernel, iterations=1)
# b2. Black Hat(close - src)
dst2 = cv.morphologyEx(img, op=cv.MORPH_BLACKHAT, kernel=kernel, iterations=1)
# dst2 = dst1 - img

# c. 可视化
plt.subplot(131)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(132)
plt.imshow(dst1, 'gray')
plt.title('Close')

plt.subplot(133)
plt.imshow(dst2, 'gray')
plt.title('BlackHat')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

三、图像梯度

除了前面介绍的普通滤波/卷积操作外,在图像空域上而言,还存在一些比较重要的特征信息,

比如:边缘(Edge)特征信息;
边缘信息指的就是像素值明显变化的区域,具有非常丰富的语义信息,常用于物体识别等领域

通过对图像梯度的操作,可以发现图像的边缘信息
在OpenCV中提供了三种类型的高通滤波器,常见处理方式:Sobel、Scharr以及Laplacian导数

1、Sobel

主要就是梯度和高斯平滑的结合,在求解梯度之前,首先进行一个高斯平滑的操作。

# 加载图像
img = cv.imread('./data/xiaoren.png', 0)

# 画几条线条
rows, cols = img.shape
cv.line(img, pt1=(0,rows//3), pt2=(cols,rows//3), color=0, thickness=5)
cv.line(img, pt1=(0,2*rows//3), pt2=(cols,2*rows//3), color=0, thickness=5)
cv.line(img, pt1=(cols//3,0), pt2=(cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(2*cols//3,0), pt2=(2*cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(0,0), pt2=(cols,rows), color=0, thickness=1)
print("")

# x方向的的Sobel过滤,ksize:一般取值为3,5,7;
# 第二个参数:ddepth,给定输出的数据类型的取值范围,默认为unit8的,取值为[0,255],如果给定-1,表示输出的数据类型和输入一致。
sobelx = cv.Sobel(img, 6, dx=1, dy=0, ksize=5)
sobely = cv.Sobel(img, cv.CV_64F, dx=0, dy=1, ksize=5)

sobelx2 = cv.Sobel(img, cv.CV_64F, dx=2, dy=0, ksize=5)
sobely2 = cv.Sobel(img, cv.CV_64F, dx=0, dy=2, ksize=5)

sobel = cv.Sobel(img, cv.CV_64F, dx=1, dy=1, ksize=5)

sobelx_y = cv.Sobel(sobelx, cv.CV_64F, dx=0, dy=1, ksize=5)
sobely_x = cv.Sobel(sobely, cv.CV_64F, dx=1, dy=0, ksize=5)


# c. 可视化
plt.figure(figsize=(20,10))

plt.subplot(241)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(242)
plt.imshow(sobelx, 'gray')
plt.title('sobelx')

plt.subplot(243)
plt.imshow(sobely, 'gray')
plt.title('sobely')

plt.subplot(244)
plt.imshow(sobelx2, 'gray')
plt.title('sobelx2')

plt.subplot(245)
plt.imshow(sobely2, 'gray')
plt.title('sobely2')

plt.subplot(246)
plt.imshow(sobel, 'gray')
plt.title('sobel')

plt.subplot(247)
plt.imshow(sobelx_y, 'gray')
plt.title('sobelx_y')

plt.subplot(248)
plt.imshow(sobely_x, 'gray')
plt.title('sobely_x')

plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
自定义kernel,实现sobel
'''
kernel = np.asarray([
    [-1, -2, -1],
    [0, 0, 0],
    [1, 2, 1]
])
# 做一个卷积操作
# 第二个参数为:ddepth,一般为-1,表示不限制,默认值即可。
sobely = cv.filter2D(img, 6, kernel)
sobelx = cv.filter2D(img, 6, kernel.T)

plt.figure(figsize=(10,5))
# 可视化
plt.subplot(131)
plt.imshow(img, 'gray')
plt.title('Original')

plt.subplot(132)
plt.imshow(sobelx, 'gray')
plt.title('sobelx')

plt.subplot(133)
plt.imshow(sobely, 'gray')
plt.title('sobely')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
sobelx
=
    [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ]
=   [-1 0 1](水平梯度) X [1 2 1].T(高斯平滑)


sobely
=
    [
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1]
    ]
=   [1 2 1](高斯平滑) X [-1 0 1].T(垂直梯度)


'''
# kernel = np.asarray([
#     [-1, -2, -1],
#     [0, 0, 0],
#     [1, 2, 1]
# ])
kernel1 = np.asarray([[1,2,1]])
kernel2 = np.asarray([[-1],[0],[1]])

# 做一个卷积操作
# 第二个参数为:ddepth,一般为-1,表示不限制,默认值即可。
# sobelx = cv.filter2D(img, 6, kernel.T)
# 先高斯平滑
a = cv.filter2D(img, 6, kernel1.T)
# 再水平梯度
sobelx = cv.filter2D(a, 6, kernel2.T)

# sobely = cv.filter2D(img, 6, kernel)
# 先高斯平滑
a = cv.filter2D(img, 6, kernel1)
# 再垂直梯度
sobely = cv.filter2D(a, 6, kernel2)

plt.figure(figsize=(10,5))
# 可视化
plt.subplot(131)
plt.imshow(img, 'gray')
plt.title('Original')

plt.subplot(132)
plt.imshow(sobelx, 'gray')
plt.title('sobelx')

plt.subplot(133)
plt.imshow(sobely, 'gray')
plt.title('sobely')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

2、Scharr

Scharr可以认为是一种特殊的Sobel方式, 实际上就是一种特殊的kernel

# 加载图像
img = cv.imread('./data/xiaoren.png', 0)


# 画几条线条
rows, cols = img.shape
cv.line(img, pt1=(0,rows//3), pt2=(cols,rows//3), color=0, thickness=5)
cv.line(img, pt1=(0,2*rows//3), pt2=(cols,2*rows//3), color=0, thickness=5)
cv.line(img, pt1=(cols//3,0), pt2=(cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(2*cols//3,0), pt2=(2*cols//3,rows), color=0, thickness=5)
print("")


# Scharr中,dx和dy必须有一个为0,一个为1
scharr_x = cv.Scharr(img, cv.CV_64F, dx = 1, dy = 0)
scharr_y = cv.Scharr(img, cv.CV_64F, dx = 0, dy = 1)


scharr_x_y = cv.Scharr(scharr_x, cv.CV_64F, dx = 0, dy = 1)
scharr_y_x = cv.Scharr(scharr_y, cv.CV_64F, dx = 1, dy = 0)


# c. 可视化
plt.figure(figsize=(20,10))

plt.subplot(231)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(232)
plt.imshow(scharr_x, 'gray')
plt.title('scharr_x')

plt.subplot(233)
plt.imshow(scharr_y, 'gray')
plt.title('scharr_y')

plt.subplot(234)
plt.imshow(scharr_x_y, 'gray')
plt.title('scharr_x_y')

plt.subplot(235)
plt.imshow(scharr_y_x, 'gray')
plt.title('scharr_y_x')

plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

3、Laplacian

使用拉普拉斯算子进行边缘提取

# 加载图像
img = cv.imread('./data/xiaoren.png', 0)

# 画几条线条
rows, cols = img.shape
cv.line(img, pt1=(0,rows//3), pt2=(cols,rows//3), color=0, thickness=5)
cv.line(img, pt1=(0,2*rows//3), pt2=(cols,2*rows//3), color=0, thickness=5)
cv.line(img, pt1=(cols//3,0), pt2=(cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(2*cols//3,0), pt2=(2*cols//3,rows), color=0, thickness=5)
print("")

# ksize设置为3
ksize = 3

sobel_x = cv.Sobel(img, cv.CV_64F, dx=1, dy=0, ksize=ksize)
sobel_y = cv.Sobel(img, cv.CV_64F, dx=0, dy=1, ksize=ksize)

laplacian = cv.Laplacian(img, cv.CV_64F, ksize=ksize)
# 对laplacian取绝对值,并且准换为uint8格式
laplacian_v2 = np.uint8(np.absolute(laplacian))

scharr_x = cv.Scharr(img, cv.CV_64F, dx=1, dy=0)
scharr_y = cv.Scharr(img, cv.CV_64F, dx=0, dy=1)


# c. 可视化
plt.figure(figsize=(20,10))

plt.subplot(241)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(242)
plt.imshow(sobel_x, 'gray')
plt.title('sobel_x')

plt.subplot(243)
plt.imshow(sobel_y, 'gray')
plt.title('sobel_y')

plt.subplot(244)
plt.imshow(laplacian, 'gray')
plt.title('laplacian')

plt.subplot(245)
plt.imshow(laplacian_v2, 'gray')
plt.title('laplacian_v2')

plt.subplot(246)
plt.imshow(scharr_x, 'gray')
plt.title('scharr_x')

plt.subplot(247)
plt.imshow(scharr_y, 'gray')
plt.title('scharr_y')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''

在Sobel检测中,depth对于结果的影响,当输出的depth设置为比较低的数据格式,那么当梯度值计算为负值的时候,就会将其重置为0,从而导致失真。
在Laplacian检测中,该问题不大。

'''
# 构建一个图像
# 构建黑底白框的图像
img = np.zeros((300,300), np.uint8)
img[100:200,100:200] = 255
# 构建白底黑框的图像
# img = np.ones((300,300), np.uint8) * 255
# img[100:200,100:200] = 0

ksize = 5

# 做Sobel的操作
dst1 = cv.Sobel(img, cv.CV_8U, 1, 0, ksize=ksize)
dst2 = cv.Sobel(img, cv.CV_64F, 1, 0, ksize=ksize)
dst3 = np.uint8(np.absolute(dst2))



# 做Laplacian的操作
# dst1 = cv.Laplacian(img, cv.CV_8U, ksize=ksize)
# dst2 = cv.Laplacian(img, cv.CV_64F, ksize=ksize)
# dst3 = np.uint8(np.absolute(dst2))

# c. 可视化
plt.figure(figsize=(10,5))

plt.subplot(221)
plt.imshow(img, 'gray')
plt.title('img')

plt.subplot(222)
plt.imshow(dst1, 'gray')
plt.title('dst1')

plt.subplot(223)
plt.imshow(dst2, 'gray')
plt.title('dst2')

plt.subplot(224)
plt.imshow(dst3, 'gray')
plt.title('dst3')

plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

4、Canday算法

Canny算法是一种比Sobel和Laplacian效果更好的一种边缘检测算法;在Canny算法中,主要包括以下几个阶段:

  • Noise Reduction:降噪,使用5*5的kernel做Gaussian filter降噪;
  • Finding Intensity Gradient of the Image:求图像像素的梯度值;
  • Non-maximum Suppression:删除可能不构成边缘的像素,即在渐变方向上相邻区域的像素梯度值是否是最大值,如果不是,则进行删除。
  • Hysteresis Thresholding:基于阈值来判断是否属于边;大于maxval的一定属于边,小于minval的一定不属于边,在这个中间的可能属于边的边缘。
# 加载图像
img = cv.imread('./data/xiaoren.png', 0)

# 画几条线条
rows, cols = img.shape
cv.line(img, pt1=(0,rows//3), pt2=(cols,rows//3), color=0, thickness=5)
cv.line(img, pt1=(0,2*rows//3), pt2=(cols,2*rows//3), color=0, thickness=5)
cv.line(img, pt1=(cols//3,0), pt2=(cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(2*cols//3,0), pt2=(2*cols//3,rows), color=0, thickness=5)
cv.line(img, pt1=(0,0), pt2=(cols,rows), color=0, thickness=1)
print("")


# 做一个Canny边缘检测(OpenCV中是不包含高斯去燥的)
# a. 高斯去燥
blur = cv.GaussianBlur(img, (5,5),0)
# b. Canny边缘检测
edges = cv.Canny(blur,threshold1=10, threshold2=250)

# 可视化
plt.figure(figsize=(20,10))
plt.subplot(131)
plt.imshow(img,cmap = 'gray')
plt.title('Original Image')

plt.subplot(132)
plt.imshow(blur,cmap = 'gray')
plt.title('Gaussian Blur Image')

plt.subplot(133)
plt.imshow(edges,cmap = 'gray')
plt.title('Canny Edge Image')
plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

5、轮廓信息

  • 轮廓信息可以简单的理解为图像曲线的连接点信息,在目标检测以及识别中有一定的作用。
  • 轮廓信息的查找最好是基于灰度图像或者边缘特征图像,因为基于这样的图像比较容易找连接点信息;

NOTE:在OpenCV中,查找轮廓是在黑色背景中查找白色图像的轮廓信息。

# 加载图像
img = cv.imread('./data/xiaoren.png')

# 将图像转换为灰度图像
img1 = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 做一个图像反转(0 -> 255, 255 -> 0)
img1 = cv.bitwise_not(img1)

# 做一个二值化
ret, thresh = cv.threshold(img1, 127, 255, cv.THRESH_BINARY)

# 发现轮廓信息
# 第一个参数是原始图像,第二个参数是轮廓的检索模型,第三个参数是轮廓的近似方法
# 第一个返回值是轮廓,第二个参数值为层次信息
# CHAIN_APPROX_SIMPLE指的是对于一条直线上的点而言,仅仅保留端点信息,而CHAIN_APPROX_NONE保留所有点
# contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
print("总的轮廓数目:{}".format(len(contours)))
print('hierarchy.shape = ', hierarchy.shape)

# 在图像中绘制图像
# 当contourIdx为-1的时候,表示绘制所有轮廓,当为大于等于0的时候,表示仅仅绘制某一个轮廓
# 这里的返回值img3和img是同一个对象,在当前版本中
# img3 = cv.drawContours(img, contours, contourIdx=-1, color=(0, 0, 255), thickness=2)
max_idx = np.argmax([len(t) for t in contours])
# print(max_idx)
img3 = cv.drawContours(img, contours, contourIdx=max_idx, color=(0, 0, 255), thickness=2)

# 可视化
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original Image')

plt.subplot(122)
plt.imshow(thresh,cmap = 'gray')
plt.title('thresh')

plt.show()
总的轮廓数目:283
hierarchy.shape =  (1, 283, 4)

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

6、轮廓信息说明

# 构建黑底白框的图像
img = np.zeros((300,300), np.uint8)
img[10:290,10:290] = 255
img[50:200, 50:200] = 0
img[55:100, 55:100] = 255
img[120:190, 120:150] = 255
img[130:160, 130:145] = 0
img[210:250, 210:250] = 0
img[250:270, 150:180] = 0
img[205:220, 205:220] = 0

# 可视化
plt.imshow(img, 'gray')
plt.title('img')

plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

ret, thresh = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
img3 = cv.drawContours(img, contours, contourIdx=-1, color=128, thickness=2)

print("总的轮廓数目:{}".format(len(contours)))
print('hierarchy.shape = ', hierarchy.shape)
hierarchy
# 是一个[1, n, 4]格式,n为轮廓的数目,这个中间保存的是轮廓包含信息

# 每个轮廓的层次信息是一个4维的向量值,
# 第一个值表示当前轮廓的上一个同层级的轮廓下标,
# 第二值表示当前轮廓的下一个同层级的轮廓下标,
# 第三个表示当前轮廓的第一个子轮廓的下标,
# 第四个就表示当前轮廓的父轮廓的下标
总的轮廓数目:7
hierarchy.shape =  (1, 7, 4)





array([[[-1, -1,  1, -1],
        [ 2, -1, -1,  0],
        [ 3,  1, -1,  0],
        [-1,  2,  4,  0],
        [ 6, -1,  5,  3],
        [-1, -1, -1,  4],
        [-1,  4, -1,  3]]], dtype=int32)

7、轮廓属性

获取得到轮廓坐标后,就可以基于轮廓来计算面积、周长等属性。

# 加载图像
img = cv.imread('./data/xiaoren.png')

# 将图像转换为灰度图像
img1 = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 做一个图像反转(0 -> 255, 255 -> 0)
img1 = cv.bitwise_not(img1)

# 做一个二值化
ret, thresh = cv.threshold(img1, 127, 255, cv.THRESH_BINARY)

# 发现轮廓信息
# 第一个参数是原始图像,第二个参数是轮廓的检索模型,第三个参数是轮廓的近似方法
# 第一个返回值是轮廓,第二个参数值为层次信息
# CHAIN_APPROX_SIMPLE指的是对于一条直线上的点而言,仅仅保留端点信息,而CHAIN_APPROX_NONE保留所有点
# contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)

idx = np.argmax([len(t) for t in contours])
cnt = contours[idx]
print(cnt.shape)
print(cnt[:2,:,:])
# 绘制轮廓
cv.drawContours(img, contours, contourIdx=idx, color=(0, 0, 255), thickness=2)

# 计算面积
area = cv.contourArea(cnt)
# 计算周长
perimeter = cv.arcLength(cnt, closed=True)
print("面积为:{}, 周长为:{}".format(area, perimeter))


'''
1、获取最大的矩形边缘框, 返回值为矩形框的左上角的坐标以及宽度和高度
'''
x,y,w,h = cv.boundingRect(cnt)
# 绘图
cv.rectangle(img, pt1=(x,y), pt2=(x+w,y+h), color=(255,0,0), thickness=2)


'''
2、绘制最小矩形(所有边缘在矩形内)、得到矩形的点(左下角、左上角、右上角、右下角<顺序不一定>)、绘图
'''
# minAreaRect:求得一个包含点集cnt的最小面积的矩形,这个矩形可以有一点的旋转偏转的,输出为矩形的四个坐标点
# rect为三元组,
#       第一个元素为旋转中心点的坐标
#       第二个元素为矩形的高度和宽度
#       第三个元素为旋转大小,正数表示顺时针选择,负数表示逆时针旋转
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int64(box)
cv.drawContours(img, [box], 0, (0, 255, 0), 2)

'''
3、绘制最小的圆(所有边缘在圆内)
'''
(x,y), radius = cv.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv.circle(img, center, radius, (0, 0, 255), 5)


'''
4、绘制最小的椭圆(所有边缘不一定均在圆内)
'''
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img, ellipse, (0, 255, 0), 5)


# 可视化
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original Image')

plt.subplot(122)
plt.imshow(thresh,cmap = 'gray')
plt.title('thresh')

plt.show()
(1050, 1, 2)
[[[306 220]]

 [[306 229]]]
面积为:65960.5, 周长为:2487.3636897802353

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

'''
5、旋转后绘制最小椭圆
'''
# 加载图像
img = cv.imread('./data/xiaoren.png')

# 旋转

rect = [(302, 420), (317.06085205078125, 372.9076232910156), 18]
M = cv.getRotationMatrix2D(center=rect[0], angle=rect[-1], scale=1)
img = cv.warpAffine(img, M, (cols, rows), borderValue=[255,255,255])

# 将图像转换为灰度图像
img1 = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# 做一个图像反转(0 -> 255, 255 -> 0)
img1 = cv.bitwise_not(img1)

# 做一个二值化
ret, thresh = cv.threshold(img1, 127, 255, cv.THRESH_BINARY)
# 发现轮廓信息
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)

idx = np.argmax([len(t) for t in contours])
cnt = contours[idx]

# 绘制轮廓
cv.drawContours(img, contours, contourIdx=idx, color=(0, 0, 255), thickness=2)

# 绘制最小矩形(所有边缘在矩形内)、得到矩形的点(左下角、左上角、右上角、右下角<顺序不一定>)、绘图
# minAreaRect:求得一个包含点集cnt的最小面积的矩形,这个矩形可以有一点的旋转偏转的,输出为矩形的四个坐标点
# rect为三元组,第一个元素为旋转中心点的坐标
# rect为三元组,第二个元素为矩形的高度和宽度
# rect为三元组,第三个元素为旋转大小,正数表示顺时针选择,负数表示逆时针旋转
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int64(box)
cv.drawContours(img, [box], 0, (0, 255, 0), 2)
print(rect)


# 可视化
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.title('Original Image')

plt.subplot(122)
plt.imshow(thresh,cmap = 'gray')
plt.title('thresh')

plt.show()
((300.05328369140625, 390.378662109375), (290.5243225097656, 397.2838134765625), 88.93909454345703)

OpenCV基础操作(5)图像平滑、形态学转换、图像梯度

8、直方图

'''
OpenCV中的直方图的主要功能是可以查看图像的像素信息以及提取直方图中各个区间的像素值的数目作为当前图像的特征属性进行机器学习模型
'''

# 加载图像
img = cv.imread('./data/koala.png', 0)

# 两种方式基本结果基本类似
# 1、基于OpenCV的API计算直方图
hist1 = cv.calcHist([img], channels=[0], mask=None, histSize=[256], ranges=[0,256])


# 2、基于NumPy计算直方图
hist2, bins = np.histogram(img.ravel(), 256, [0, 256])


# 和np.histogram一样的计算方式,但是效率快
hist3 = np.bincount(img.ravel(), minlength=256)

# 可视化
plt.figure(figsize=(20,10))
plt.subplot(231)
plt.imshow(img, 'gray')
plt.title('Original Image')


plt.subplot(232)
# 可以直接使用matpliab中的hist API直接画直方图
plt.plot(hist1)
plt.title('hist1')

plt.subplot(233)
plt.plot(hist2)
plt.title('hist2')


plt.subplot(234)
plt.plot(hist3)
plt.title('hist3')

# 可以直接使用matpliab中的hist API直接画直方图
plt.subplot(235)
plt.hist(img.ravel(), 256, [0, 256])
plt.title('plt.hist')

plt.show()


OpenCV基础操作(5)图像平滑、形态学转换、图像梯度
文章来源地址https://www.toymoban.com/news/detail-471300.html

到了这里,关于OpenCV基础操作(5)图像平滑、形态学转换、图像梯度的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Python-OpenCV中的图像处理-形态学转换

    形态学操作:腐蚀,膨胀,开运算,闭运算,形态学梯度,礼帽,黑帽等 主要涉及函数:cv2.erode(), cv2.dilate(), cv2.morphologyEx() 原理:形态学操作是根据图像形状进行的简单操作。一般情况下对二值化图像进行的操作。需要输入两个参数,一个是原始图像,第二个被称为结构化

    2024年02月13日
    浏览(60)
  • Opencv | 图像卷积与形态学变换操作

    在每个图像位置(x,y)上进行基于邻域的函数计算,其中函数参数被称为卷积核 (kernel) kernel核的尺寸通常为奇数,一般为: 3 ∗ 3 、 5 ∗ 5 、 7 ∗ 7 3*3、5*5、7*7 3 ∗ 3 、 5 ∗ 5 、 7 ∗ 7 不同功能需要定义不同的函数,其中功能可以有: 图像增强:           平滑 / 去

    2024年04月23日
    浏览(43)
  • OpenCV基本图像处理操作(一)——图像基本操作与形态学操作

    图像显示 转hsv图像 颜色表示为三个组成部分:色调(Hue)、饱和度(Saturation)和亮度(Value)。常用于图像处理中,因为它允许调整颜色的感知特性,如色彩和亮度,这些在RGB颜色模型中不那么直观。 HSV模型特别适用于任务如图像分割和对象追踪,因为它可以更好地处理光

    2024年04月22日
    浏览(89)
  • OpenCV图像处理学习十,图像的形态学操作——膨胀腐蚀

    一.形态学操作概念 图像形态学操作是指基于形状的一系列图像处理操作的合集,主要是基于集合论基础上的形态学数学对图像进行处理。 形态学有四个基本操作:腐蚀、膨胀、开操作、闭操作,膨胀与腐蚀是图像处理中最常用的形态学操作手段。 二.形态学操作-膨胀 跟卷积

    2024年02月05日
    浏览(56)
  • 图像形态学-阈值的概念、功能及操作(threshold()函数))【C++的OpenCV 第九课-OpenCV图像常用操作(六)】

    首先,顾名思义,“ 阈 ”就是范围或者 限制 ,所以,“阈值”就是 某个限制的值 (该值具有一定的数学含义,即“ 临界值 ”,例如车辆限高杆的高度就是一种阈值,不可超越;亦或者1.1米以下儿童不收费,超过1.1就要收费。) 其次,图形学中的阈值,往往指某个你想要

    2024年02月03日
    浏览(49)
  • 使用opencv c++完成图像中水果分割(分水岭、形态学操作、通道处理)单独标记每个水果

    2023.4.16日更新 1. 利用一阶矩增加了草莓等水果的质心绘制。 2. 绘制出了生长方向。 原为本人机器人视觉作业。参考文章http://t.csdn.cn/eQ0qp(目测是上一届的学长) 要求:在网络上寻找水果重叠在一起的图片、经过一系列图像处理,完成每个水果的分割,并单独标记出来。 导

    2024年02月04日
    浏览(86)
  • 图像的形态学开操作(开运算)和闭操作(闭运算)的概念和作用,并用OpenCV的函数morphologyEx()实现对图像的开闭操作

    大家看这篇博文前可以先看一看下面这篇博文,下面这篇博文是这篇博文的基础: 详解图像形态学操作之图形的腐蚀和膨胀的概念和运算过程,并利用OpenCV的函数erode()和函数dilate()对图像进行腐蚀和膨胀操作 图像形态学腐蚀可以将细小的噪声区域去除,但是会将图像主要区域

    2024年02月06日
    浏览(61)
  • OpenCv之图像形态学

    目录 一、形态学  二、图像全局二值化  三、自适应阈值二值化 四、腐蚀操作 五、获取形态学卷积核 六、膨胀操作 七、开运算 八、闭运算 定义: 指一系列处理图像形状特征的图像处理技术 形态学的基本思想是利用一种特殊的结构元(本质上就是卷积核)来测量或提取输入图

    2024年02月16日
    浏览(42)
  • OpenCv之图像形态学(二)

    目录 一、形态学梯度 二、顶帽操作 三、黑帽操作 梯度=原图 - 腐蚀 腐蚀之后原图边缘变小,原图 - 腐蚀 就可以得到腐蚀掉的部分,即边缘 案例代码如下: 运行结果如下: 顶帽 = 原图 - 开运算 开运算的效果是去除图形外的噪点,原图 - 开运算就得到了去掉的噪点 案例代码如

    2024年02月16日
    浏览(46)
  • 我在Vscode学OpenCV 图像处理一(阈值处理、形态学操作【连通性,腐蚀和膨胀,开闭运算,礼帽和黑帽,内核】)

    例如,设定阈值为127,然后:  将图像内所有像素值大于 127 的像素点的值设为 255。  将图像内所有像素值小于或等于 127 的像素点的值设为 0。 cv2.threshold() 和 cv2.adaptiveThreshold() 是 OpenCV 中用于实现阈值处理的两个函数,它们之间有以下区别: 1.1.1. cv2.threshold(): 这个函数

    2024年02月05日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包