安装
全局安装
pip install opencv-python
项目虚拟环境安装
# 进入项目根路径执行
.venv/bin/pip install opencv-python
计算机眼中的图像
一张图片由大小比如(100*100
)决定,说明存在100*100
的像素点,每个像素点存在颜色通道,我们所看到的彩色均由RGB(Red红色、、Green绿色、Blue蓝色)三原色组成,不同的颜色组合在一起就会在视觉上看到新的颜色,比如红色+绿色,看到的就是黄色。
因为RGB三通道模式,对于彩色图片就存在 2 3 2^3 23即八种标准颜色(只考虑0或者255)。
- 纯红色:RGB(255, 0, 0)
- 纯绿色:RGB(0, 255, 0)
- 纯蓝色:RGB(0, 0, 255)
- 黄色(红色+绿色):RGB(255, 255, 0)
- 青色(绿色+蓝色):RGB(0, 255, 255)
- 品红色(红色+蓝色):RGB(255, 0, 255)
- 白色:RGB(255, 255, 255)
- 黑色:RGB(0, 0, 0)
在计算机中我们用[r,g,b]
这样一个定长的一维数组表示一个像素点,其中元素顺序可变,所以存在RGB,BGR模式的说法。于是对于一张图片的表示如下:
图 片 矩 阵 [ [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] ⋯ [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] [ r , g , b ] ⋯ [ r , g , b ] [ 255 , 0 , 0 ] ⋮ ⋮ ⋮ ⋮ [ 255 , 0 , 0 ] [ r , g , b ] ⋯ [ r , g , b ] [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] ⋯ [ 255 , 0 , 0 ] [ 255 , 0 , 0 ] ] 图片矩阵 \left[ \begin{matrix} [255,0,0] & [255,0,0] & \cdots & [255,0,0] & [255,0,0]\\ [255,0,0] & [r,g,b] & \cdots & [r,g,b] & [255,0,0]\\ \vdots & \vdots & & \vdots& \vdots && \\ [255,0,0] & [r,g,b] & \cdots & [r,g,b] & [255,0,0]\\ [255,0,0] & [255,0,0] & \cdots & [255,0,0] & [255,0,0] \end{matrix} \right] 图片矩阵⎣⎢⎢⎢⎢⎢⎡[255,0,0][255,0,0]⋮[255,0,0][255,0,0][255,0,0][r,g,b]⋮[r,g,b][255,0,0]⋯⋯⋯⋯[255,0,0][r,g,b]⋮[r,g,b][255,0,0][255,0,0][255,0,0]⋮[255,0,0][255,0,0]⎦⎥⎥⎥⎥⎥⎤
我们用[r,g,b]表示一个像素点,于是图片矩阵中每一行的像素点表示为:
red_row1 = [[r1,g1,b1],...,[rn,gn,bn]]
上面这样的行存在多少个呢?这就是列,也就是图片高度height的像素点个数。
red_img = [[[r1,g1,b1],...,[rn,gn,bn]]
,
[[r1,g1,b1],...,[rn,gn,bn]]
,...,
[[r1,g1,b1],...,[rn,gn,bn]]
]
最后图片在计算机看到的数据就是这样:
如果我们只查看几个像素点数据,可以这样:
img = cv2.imread("img.png")
# 获取高度两个像素点,宽度3个像素点,即共6个像素点展示
print(img[:2, :3:])
print("="*20)
# 获取高度一个像素点,宽度三个像素点,B通道的数据
b = img[:1, :3, 0]
# 获取高度一个像素点,宽度三个像素点,G通道的数据
g = img[:1, :3, 1]
# 获取高度一个像素点,宽度三个像素点,R通道的数据
r = img[:1, :3, 2]
print(b)
print(g)
print(r)
输出
总结: 计算机中的图片由矩阵像素点(pixel) 表示,其中每一个像素点由一维数组[b,g,r]定长一维数组表示,不同的数值代表不同的颜色,按照三原色通道,彩色图存在三个通道的二维数组。
Note:
- 通道顺序是可变的,不同的排列意味着模式不同[b,g,r]表示BGR模式[r,b,g]表示RBG模式。
- 对于灰度图,因为不需要三个通道表示,因此一个数值就表示一个像素点,所以灰度图单纯是一个二维矩阵描述图片像素点。
OpenCV
读取图片默认是BGR模式
ROI
ROI(Region Of Interest) 感兴趣的区域,可用于截取特定区域图或者特定通道图。
截取特定区域
# 读取图片为三维数组数据
img = cv2.imread("person.jpg")
# 截取高度500像素,宽度1000像素
img = img[0:500, 0:1000]
# 查看图片,按q退出
cv2.imshow('person', img)
if cv2.waitKey() & 0xFF == 'q':
cv2.destroyAllWindows()
效果图
颜色通道提取
img = cv2.imread("person.jpg")
# 返回不同通道的数据,是一个二维数组!!!
b, g, r = cv2.split(img)
# 只保留b通道数据的图片
blue_img = img.copy()
blue_img[:, :, 1] = 0
blue_img[:, :, 2] = 0
# 只保留g通道数据的图片
green_img = img.copy()
green_img[:, :, 0] = 0
green_img[:, :, 2] = 0
# 只保留r通道数据的图片
red_img = img.copy()
red_img[:, :, 0] = 0
red_img[:, :, 1] = 0
效果图
图片融合
# cv2打开图片默认为BGR格式,需要转为RGB格式
target = cv2.cvtColor(cv2.imread(img1), cv2.COLOR_BGR2RGB)
height, width, _ = target.shape
cv2_img2 = cv2.cvtColor(cv2.imread(img2), cv2.COLOR_BGR2RGB)
# 两张图片融合需要保证宽高一致,因此需要重新调整大小
background = cv2.resize(cv2_img2, (width, height))
# 合并图片
merged = cv2.addWeighted(target, 0.6, background, 0.4, 0)
# 展示图片需要RGB再次转为BGR(如果单纯展示图片前面加载图片就不用转了),如果需要图片保存则需要保存为RGB格式
cur_img = cv2.cvtColor(merged,cv2.COLOR_RGB2BGR)
cv2.imshow("merged", cur_img)
# 按q退出,或者指定waiKey()指定时长(毫秒)后自动退出,为0则不退出,按键q退出
if cv2.waitKey(0) & 0xFF == ord('q'):
cv2.destroyAllWindows()
效果图
边界填充
其实就是指定上下左右四个方向应该填充什么颜色。
img = cv2.imread("person.jpg")
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
# 填充的区域大小:上下左右
top, bottom, left, right = (50, 50, 50, 50)
# 不同的填充策略
# 1. 复制法,就是直接把边缘的像素复制
replicate = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REPLICATE)
# 2. 反射法,比如 321|12345|543
reflect_101 = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT_101)
# 3. 反射法101,比如 432|12345|4321
reflect101 = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT101)
# 4. 包装法,比如 345|12345|123
wrap = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_WRAP)
# 5. 常量值填充,通过value指定常量值
constant = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT,value=255)
images = [img, replicate, reflect_101, reflect101, wrap, constant]
titles = ["ORIGINAL", "REPLICATE", "REFLECT_101", "REFLECT101", "WRAP", "CONSTANT"]
# matplot绘图查看结果
rows, cols = 2, 3
for i in range(rows * cols):
plt.subplot(int(f"{rows}{cols}{i+1}")), plt.imshow(images[i], 'gray'), plt.title(titles[i])
plt.axis('off')
plt.xticks([])
plt.yticks([])
plt.show()
效果图
数值计算
img = cv2.imread('person.jpg')
# 每个像素点的数值都+30,如果超过255则%256,比如刚好256则会变成黑色0
img2 = img + 30
# 直接相加,如果超过则固定数值255
img3 = cv2.add(img, img)
图像阈值
ret, dst = cv2.threshold(src, thres, maxval, type)
- ret:这个返回值是实际使用的阈值。如果
type
参数中使用了cv2.THRESH_OTSU
或cv2.THRESH_TRIANGLE
,则ret
是自动计算得到的最佳阈值,而thresh
参数在这种情况下被忽略。如果不使用这些自动阈值计算方法,ret
将与thresh
参数的值相同。 - dst:输出图
- src:输入图
- thresh:阈值数值
- maxval:超出阈值后应该设置的值,或者小于,根据type策略决定
- type:策略类型
cv2.THRESH_BINARY
:如果像素值大于阈值,则像素值被设置为maxval
;否则,像素值被设置为0。cv2.THRESH_BINARY_INV
:这是cv2.THRESH_BINARY
的反向操作。cv2.THRESH_TRUNC
:如果像素值大于阈值,则像素值被设置为阈值;否则,像素值保持不变。cv2.THRESH_TOZERO
:如果像素值大于阈值,则像素值保持不变;否则,像素值被设置为0。cv2.THRESH_TOZERO_INV
:上面的反向操作cv2.THRESH_OTSU
:自动选择最佳阈值的方法。cv2.THRESH_TRIANGLE
:OpenCV 4.x中引入的一个选项,它使用了一种基于图像直方图的三角形方法来寻找最佳全局阈值。
img = cv2.imread('person.jpg')
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
# 不同阈值设置得到的结果图
ret, dst1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
ret, dst2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
ret, dst3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
ret, dst4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
ret, dst5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)
images = [img, dst1, dst2, dst3, dst4, dst5]
titles = ["ORIGINAL", "BINARY", "BINARY_INV", "TRUNC", "TOZERO", "TOZERO_INV"]
explains = ["原图", "大于则为阈值,否则为0", "小于则为阈值,否则为0", "大于阈值则设置为阈值", "小于阈值则设置为0",
"大于阈值则设置为0"]
rows = 2
cols = 3
# plt绘制图
for i in range(rows * cols):
plt.subplot(int(f"{rows}{cols}{i + 1}")), plt.imshow(images[i]), plt.title(titles[i])
# 在图片下方添加注释
plt.text(x=0.5, y=-0.1, s=explains[i], fontsize=10,
ha='center', va='top', transform=plt.gca().transAxes)
plt.axis('off')
plt.xticks([]), plt.yticks([])
plt.show()
效果图
图像处理
1. 均值滤波
将像素点周围的数加起来计算平均值,我们设置的大小则为滤波核,或者叫卷积核,单词为kernel,在参数里边为ksize。
这么计算的结果更像是把所有的像素点按照卷积核大小进行了一个平滑处理,当卷积核越大,影响的面积就越大,响应的平均后的值越均匀,给人的视觉效果就是更模糊。
# 卷积3x3对应下图中间
ret1 = cv2.blur(noisy, (3, 3))
# 卷积4x4对应下图左上
ret1 = cv2.blur(noisy, (3, 3))
# 第一叫均值滤波,第二个叫方框滤波,结果完全是一样的
ret1 = cv2.blur(noisy, (3, 3))
# 参数-1输出值像素深度保持一致,深度值得的是表示像素的位数,比如8位,16位,32位
ret2 = cv2.boxFilter(noisy, -1, (3, 3), normalize=True)
效果图
2. 高斯滤波
高斯滤波区别均值滤波的核心点在于权重概念,离像素点越远的位置权重比越小。
高斯滤波的核心是正态分布,然后计算权重得出的,高斯滤波处理的图片会比均值滤波更清晰因为距离越远受响应程度越小。
img = cv2.imread('person_noisy.png')
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
ret1 = cv2.blur(img, (25, 25))
# 最后一个参数为sigma=0,标准差设置0让cv2自己计算合理的标准差
ret2 = cv2.GaussianBlur(img, (25, 25), 0)
plt.figure(figsize=(12,4))
plt.subplot(131), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(ret1), plt.title("均值滤波(25,25)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(133), plt.imshow(ret2), plt.title("高斯滤波(25,25)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.tight_layout()
plt.show()
效果图
3. 中值滤波
中值滤波计算方式最简单,将滤波核里边的数据取出来排序后取中间值,作为代替的值。
对于上面这种特殊例子,255永远是最大值,因此直接被过滤掉了。
img = cv2.imread('person_noisy.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
ret1 = cv2.blur(img, (25, 25))
# 最后一个参数为sigma=0,标准差设置0让cv2自己计算合理的标准差
ret2 = cv2.GaussianBlur(img, (25, 25), 0)
# 参数比较特殊,实际25等价上面的元组(25, 25)
ret3 = cv2.medianBlur(img, 25)
plt.subplot(221), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(222), plt.imshow(ret1), plt.title("均值滤波(25,25)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(223), plt.imshow(ret2), plt.title("高斯滤波(25,25)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(224), plt.imshow(ret3), plt.title("中值滤波(25,25)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.tight_layout()
plt.show()
效果图
形态学
1. 腐蚀操作
所谓腐蚀操作,是对图像处理效果的描述,原理则是根据给定的卷积核结构元(structuring element)遍历图像,然后取最小值赋值给目标像素点,最后得到一个处理后的结果矩阵。
计算原理遵循两个点:
- 卷积核完全处在图像内,确定的中心点就是目标点(是否需要腐蚀)
- 如果在这个卷积核区域内,只要存在最小值通常为二值图,也即是黑色0,则腐蚀中心点(也就是赋值最小值。)
示意图如下
其中图中框选区域就是卷积核结构元,就是一个3*3
的矩阵,可见,只要白色区域不能完全覆盖卷积核,则中心点被设置为背景色黑色0。
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((90, 90), dtype=np.uint8)
ret = cv2.erode(img, kernel, iterations=1)
# 错误的使用,这实际上是利用卷积结构为一维数组,数据元素为5进行卷积运算,也就是说,1*2两个像素点大小的卷积核!!!
# ret = cv2.erode(img, (5,5), iterations=9)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("腐蚀前(原图)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("腐蚀后)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
Note: 这里有个需要特别指明的点,在使用上,卷积核是一个矩阵结构,只不过里边的数都是1,如果错误使用,比如我这样👇
ret = cv2.erode(img, (5,5), iterations=9)
实际上是指定卷积核为1*2
像素的一个长方形,里边元素是5,也就是对每个1*2
大小的像素块的值乘5
,导致结果并不会判断最小值0,也就是下面的结果,腐蚀效果看不到,实际上就腐蚀了大小为2的很小一个像素点。
错误的结果图:
总结:所谓腐蚀操作,**就是利用卷积核(也叫结构元)进行移动,只要所到之处没有被白色完全包裹,就会腐蚀中心点,**因此对于上面规整的黑白图,卷积核为10,实际就是白色区域减少10个白色像素点,如果设置20则减少20个白色像素点。
另外,所谓迭代次数,就是在操作后的基础上再次移动的意思。如果卷积核设置足够大,可以一次腐蚀操作直接全部腐蚀完。
如果卷积核大小完全覆盖白色区域,且在图片内,则一次腐蚀完毕!
2. 膨胀操作
如果理解了腐蚀操作,那么膨胀操作就很好理解了,完全是一个相反的操作,腐蚀操作是赋值卷积核内最小的数值,膨胀操作则恰恰相反,赋值最大的值也就是视觉上的白色。
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# 生成卷积核矩阵
kernel = np.ones((110, 110), dtype=np.uint8)
# 膨胀操作
ret = cv2.dilate(img, kernel, iterations=1)
# 输出结果图
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("膨胀前(原图)"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("膨胀后"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
3. 开运算和闭运算
3.1 开运算
开运算(先腐蚀,再膨胀)。
这么看可能会认为这是无效的操作,看下面的图👇
可以看到,如果原图的白色很细小,不影响整体的腐蚀,那么开运算就可以达到去除杂质的效果。
img = cv2.imread('open.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((90, 90), dtype=np.uint8)
# 开运算
ret = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("开运算"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
3.2 闭运算
闭运算(先膨胀,再腐蚀)。
闭运算并不能做到类似开运算的结果,相反他会让原来的图形轮廓变大(不同的图闭运算效果也不同)
img = cv2.imread('open.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((90,90), dtype=np.uint8)
# 闭运算
ret = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("闭运算"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
4. 梯度运算
梯度运算(膨胀-腐蚀)。
img = cv2.imread('open.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((5,5), dtype=np.uint8)
# 梯度运算
ret = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("梯度运算运算"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
5. 礼帽和黑帽
5.1 礼帽
礼帽(原图-开运算结果)。
img = cv2.imread('open.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((95, 95), dtype=np.uint8)
# 礼帽运算(原图-开运算结果)
ret = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("礼帽"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
5.2 黑帽
黑帽(闭运算结果-原图)。
img = cv2.imread('open.png', cv2.IMREAD_GRAYSCALE)
kernel = np.ones((95, 95), dtype=np.uint8)
# 黑帽运算(闭运算结果-原图)
ret = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("黑帽"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
图形梯度
注意: 这里的梯度,指得是图像强度或颜色的方向变化。 他是用来衡量变化幅度的,比如黑色到白色,直接变化255,就是很大的一个梯度,可以理解为楼梯的陡峭程度;而形态学中的梯度是一种运算操作。
一个像素点左边跟右边的差值就是梯度,这个就是x轴方向的梯度,上边跟下边的差值,就是y轴方向的梯度。而得出这个差异值的计算方法,就是算子,OpenCV提供了三种算子:Sobel,Scharr和Laplacian,高大上的名词就叫梯度滤波器或高通滤波器。
这个本质跟形态操作是一样的,只是设置的卷积矩阵值不同。
我们通过设置卷积矩阵里边元素的数值,做到右边-左边
即差异值的结果作为当前像素的梯度表示。
中 心 点 梯 度 = [ 0 0 255 0 0 255 0 0 255 ] ∗ 卷 积 矩 阵 [ − 1 0 1 − 2 0 2 − 1 0 1 ] = 255 ∗ 1 + 255 ∗ 2 + 255 ∗ 1 = 255 中心点梯度= \begin{bmatrix} 0&0&255\\ 0&0&255\\ 0&0&255 \end{bmatrix}* 卷积矩阵 \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1\\ \end{bmatrix} = 255*1 + 255 * 2 + 255*1 = 255 中心点梯度=⎣⎡000000255255255⎦⎤∗卷积矩阵⎣⎡−1−2−1000121⎦⎤=255∗1+255∗2+255∗1=255
动态效果图
1. Sobel算子
当我们设置卷积矩阵为下面的时候,就是Sobel算子。
S o b e l [ − 1 0 1 − 2 0 2 − 1 0 1 ] Sobel \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} Sobel⎣⎡−1−2−1000121⎦⎤
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# Sobel算子,这里深度指定为-1,x轴计算
ret = cv2.Sobel(img, -1, 1, 0)
# 转换通道,仅用于展示用
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("Sobel算子结果"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.show()
效果图
这里存在两个问题:
- 没有计算y轴的梯度
- 右边的梯度被忽略了
关于右边的梯度问题,这是因为我们设置的深度为-1,什么是深度? 我们一直默认颜色数值表示是0~255就是因为默认是uint8数据类型为无符号的8位,因此当黑色-白色
为负数时,被截断为0,同理当计算超过255时则被截断为255。
但这显然不是我们期望的结果,因为我们想要整张图的梯度而无关正数和负数,因此我们需要将深度改为更多位数的表示,比如32F,64F,即带符号的32位表示,64位表示。同时对梯度结果取绝对值,排除负数。
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# Sobel算子,这里深度指定深度为无符号64位,x轴计算
ret = cv2.Sobel(img, cv2.CV_64F, 1, 0)
# 将梯度结果取绝对值
ret = cv2.convertScaleAbs(ret)
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
ret = cv2.cvtColor(ret, cv2.COLOR_GRAY2RGB)
plt.subplot(121), plt.imshow(img), plt.title("原图"), plt.axis('off'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ret), plt.title("Sobel算子结果"), plt.axis('off'), plt.xticks([]), plt.yticks([])
效果图
对y轴的梯度计算同理,我们直接看最后的使用
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# Sobel算子,这里深度指定深度为无符号64位,x轴计算
x_gradient = cv2.Sobel(img, cv2.CV_64F, 1, 0)
x_gradient = cv2.convertScaleAbs(x_gradient)
# Sobel算子,这里深度指定深度为无符号64位,x轴计算
y_gradient = cv2.Sobel(img, cv2.CV_64F, 0, 1)
y_gradient = cv2.convertScaleAbs(y_gradient)
# Sobel算子,这里深度指定深度为无符号64位,x,y轴同时计算
xy_gradient = cv2.Sobel(img, cv2.CV_64F, 1, 1)
xy_gradient = cv2.convertScaleAbs(xy_gradient)
# x轴和y轴的融合结果(x轴权重0.5,y轴权重0.5),标量值设置为0 ==> dst=src1⋅α+src2⋅β+γ
x_add_y_gradient = cv2.addWeighted(x_gradient, 0.5, y_gradient, 0.5, 0)
# 图片展示代码略
效果图
可见,同时对xy轴计算梯度的效果并不好,因此最好分别计算x轴和y轴最后加权平均。
2. Scharr算子
有了前面的基础,这里就很简单了,当我们设置卷积矩阵为下面的时候,就是Scharr算子。
S c h a r r [ − 3 0 3 − 10 0 10 − 3 0 3 ] Scharr \begin{bmatrix} -3&0&3\\ -10&0&10\\ -3&0&3 \end{bmatrix} Scharr⎣⎡−3−10−30003103⎦⎤
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# Scharr算子,这里深度指定深度为无符号64位,x轴计算
x_gradient = cv2.Scharr(img, cv2.CV_64F, 1, 0)
x_gradient = cv2.convertScaleAbs(x_gradient)
# Sobel算子,这里深度指定深度为无符号64位,x轴计算
y_gradient = cv2.Scharr(img, cv2.CV_64F, 0, 1)
y_gradient = cv2.convertScaleAbs(y_gradient)
# 如果Sobel指定ksize=-1就是等价Scharr的用法
# cv2.Sobel(src,cv2.CV_64F,1,0,ksize=-1)
# x轴和y轴的融合结果(x轴权重0.5,y轴权重0.5),标量值设置为0 ==> dst=src1⋅α+src2⋅β+γ
x_add_y_gradient = cv2.addWeighted(x_gradient, 0.5, y_gradient, 0.5, 0)
3. Laplacian算子
当卷积核为下面的数值时,则为Laplacian算子。
L a p l a c i a n [ 0 1 0 1 − 4 1 0 1 0 ] Laplacian \begin{bmatrix} 0&1&0\\ 1&-4&1\\ 0&1&0 \end{bmatrix} Laplacian⎣⎡0101−41010⎦⎤
4. 三种算子的比较
img = cv2.imread('person.jpg', cv2.IMREAD_GRAYSCALE)
# Sobel算子
x_gradient = cv2.Sobel(img, cv2.CV_64F, 1, 0)
x_gradient = cv2.convertScaleAbs(x_gradient)
y_gradient = cv2.Sobel(img, cv2.CV_64F, 0, 1)
y_gradient = cv2.convertScaleAbs(y_gradient)
sobel_gradient = cv2.addWeighted(x_gradient, 0.5, y_gradient, 0.5, 0)
# Scharr算子, Sobel函数指定ksize=-1就是Scharr
x_gradient = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=-1)
x_gradient = cv2.convertScaleAbs(x_gradient)
y_gradient = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=-1)
y_gradient = cv2.convertScaleAbs(y_gradient)
scharr_gradient = cv2.addWeighted(x_gradient, 0.5, y_gradient, 0.5, 0)
# Laplacian算子
laplacian_gradient = cv2.Laplacian(img, cv2.CV_64F)
laplacian_gradient = cv2.convertScaleAbs(laplacian_gradient)
# 图片展示代码略
效果图
总结: Sobel算子主要用于边缘检测,Scharr算子同理但是他更能捕捉细节,二者对噪音点的抵抗都还可以,Laplacian算子噪音点对识别的影响较大,但是能够提供更清晰的边缘效果。
参考链接
[1]:B站视频&opencv从入门到实战
[2]:官网Docs4.10.0
[3]:中文文档
[4]:原来卷积是这么计算的