OpenCV系列教程二:基本图像增强(数值运算)、滤波器(去噪、边缘检测)
文章目录
- 一、基本图像增强(数值运算)
- 1.1 加法 (cv2.add)
- 1.1.1 图像与标量相加(调节亮度)
- 1.1.2 图像与图像相加(两个图像shape要相同)
- 1.1.3 图像的加权加法(渐变切换)
- 1.2 乘法/对比度(cv2.multiply)
- 1.3 使用np.clip处理溢出
- 1.4 图像阈值(cv2.threshold)
- 1.4.1 阈值处理基础
- 1.4.2 固定阈值处理函数cv.threshold
- 1.4.3 全局阈值处理 Otsu 方法(可跳过本节)
- 1.4.4 自适应阈值处理函数cv.adaptiveThreshold
- 1.4.5 应用:乐谱阅读器
- 1.5 位运算
- 1.6 图像的叠加:制作coca-cola彩色Logo
- 二、滤波器(Filter)
- 2.1 简介
- 2.1.1 滤波器原理
- 2.1.2 卷积函数filter2D
- 2.1.3 滤波器分类
- 2.2 降噪
- 2.2.1 方盒滤波器
- 2.2.2 均值滤波器
- 2.2.3 高斯滤波器
- 2.2.3.1 高斯函数
- 2.2.3.2 高斯滤波器
- 2.2.4 中值滤波器
- 2.2.5 双边滤波器
- 2.2.5.1 工作原理
- 2.2.5.2 bilateralFilter
- 2.3 边缘检测
- 2.3.1 sobel算子
- 2.3.2 Scharr算子
- 2.3.3 Laplacian算子
- 2.3.4 Canny
- 2.3.5 总结
- 《OpenCV系列课程一:图像处理入门(读写、拆分合并、变换、注释)、视频处理》
一、基本图像增强(数值运算)
图像处理技术利用数学运算获得不同的结果。通常,我们使用一些基本操作可以得到图像的简单增强。在本章中,我们将介绍:
- 算术运算,如加法、乘法
- 阈值和屏蔽(masking)
- 按位运算,如OR、AND、XOR
# Import libraries
import cv2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
%matplotlib inline
from IPython.display import Image
下面用opencv读取一张新西兰海岸照
img_bgr = cv2.imread("New_Zealand_Coast.jpg",cv2.IMREAD_COLOR)
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
# Display 18x18 pixel image.
Image(filename='New_Zealand_Coast.jpg')
1.1 加法 (cv2.add)
函数 cv2.add()用于图像的加法运算,其语法为dst=cv2.add(src1, src2 [, dst[, mask[, dtype]])
scr1, scr2
:进行加法运算的图像,或一张图像与一个 numpy array 标量mask
:掩模图像,8位灰度格式;掩模图像数值为 0 的像素,输出图像对应像素的各通道值也为 0(被mask位置像素输出为0)。可选项,默认值为 None。dtype
:图像数组的深度,即每个像素值的位数,可选项
需要注意的是,OpenCV 加法和 numpy 加法之间有区别:cv2.add() 是饱和运算(相加后如大于 255 则结果为 255),而 Numpy 加法是模运算。
1.1.1 图像与标量相加(调节亮度)
本节讨论图像加法的简单操作——图像与标量相加,这会导致图像亮度的增加或减少,因为我们最终会对每个像素值增加或减少相同的值。(亮度会全局地增加/减少)
matrix = np.ones(img_rgb.shape, dtype = "uint8") * 50
img_rgb_brighter = cv2.add(img_rgb, matrix)
img_rgb_darker = cv2.subtract(img_rgb, matrix)
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(img_rgb_darker); plt.title("Darker");
plt.subplot(132); plt.imshow(img_rgb); plt.title("Original");
plt.subplot(133); plt.imshow(img_rgb_brighter);plt.title("Brighter");
另外,图像也可以与常数相加。下面进行常数相加和标量相加的对比:
Value =70 # 常数
Scalar = np.ones((1, 3), dtype="float") * Value # 标量
imgAddV = cv2.add(img_bgr , Value) # OpenCV 加法: 图像 + 常数
imgAddS = cv2.add(img_bgr , Scalar) # OpenCV 加法: 图像 + 标量
print("Shape of scalar", Scalar)
for i in range(1, 6):
x, y = i*10, i*10
print("(x,y)={},{}, img_bgr:{}, imgAddV:{}, imgAddS:{}"
.format(x,y,img_bgr [x,y],imgAddV[x,y],imgAddS[x,y]))
# 打印图像中的6个点,可以看到相加后像素值的变化
Shape of scalar [[70. 70. 70.]]
(x,y)=10,10, img_bgr:[184 179 170], imgAddV:[254 179 170], imgAddS:[254 249 240]
(x,y)=20,20, img_bgr:[185 179 172], imgAddV:[255 179 172], imgAddS:[255 249 242]
(x,y)=30,30, img_bgr:[189 182 173], imgAddV:[255 182 173], imgAddS:[255 252 243]
(x,y)=40,40, img_bgr:[187 181 174], imgAddV:[255 181 174], imgAddS:[255 251 244]
(x,y)=50,50, img_bgr:[193 188 179], imgAddV:[255 188 179], imgAddS:[255 255 249]
plt.figure(figsize=[18,5])
plt.subplot(131), plt.title("1. img1"), plt.axis('off')
plt.imshow(cv2.cvtColor(img_bgr , cv2.COLOR_BGR2RGB)) # 显示 img_bgr(RGB)
plt.subplot(132), plt.title("2. img + constant"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddV, cv2.COLOR_BGR2RGB)) # 显示 imgAddV(RGB)
plt.subplot(133), plt.title("3. img + scalar"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddS, cv2.COLOR_BGR2RGB)) # 显示 imgAddS(RGB)
plt.show()
- 将图像与一个常数 value 相加,只是将 B 通道即蓝色分量与常数相加,而 G、R 通道的数值不变,因此图像发蓝。
- 将图像与一个标量 scalar 相加,“标量” 是指一个 1x3 的 numpy 数组,此时 B/G/R 通道分别与数组中对应的常数相加,因此图像发白。(数组中各元素可不相同)
1.1.2 图像与图像相加(两个图像shape要相同)
img1 = cv2.imread("../images/imgB1.jpg") # 读取彩色图像(BGR)
img2 = cv2.imread("../images/imgB3.jpg") # 读取彩色图像(BGR)
imgAddCV = cv2.add(img1, img2) # OpenCV 加法: 饱和运算
imgAddNP = img1 + img2 # Numpy 加法: 模运算
plt.subplot(221), plt.title("1. img1"), plt.axis('off')
plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)) # 显示 img1(RGB)
plt.subplot(222), plt.title("2. img2"), plt.axis('off')
plt.imshow(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)) # 显示 img2(RGB)
plt.subplot(223), plt.title("3. cv2.add(img1, img2)"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddCV, cv2.COLOR_BGR2RGB)) # 显示 imgAddCV(RGB)
plt.subplot(224), plt.title("4. img1 + img2"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddNP, cv2.COLOR_BGR2RGB)) # 显示 imgAddNP(RGB)
plt.show()
图 3 是 cv2.add()
饱和加法的结果,图 4 是 numpy
取模加法的结果。饱和加法以 255 为上限,所有像素只会变的更白(大于原值);取模加法以 255 为模,会导致部分像素变黑 (小于原值)。因此,一般情况下应使用 cv2.add
进行饱和加法操作,不宜使用 numpy 取模加法。
1.1.3 图像的加权加法(渐变切换)
函数 cv2.addWeight() 用于图像的加权加法运算,可以实现图像的叠加和混合。其语法为:
dst=cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]])
简单理解就是:
d
s
t
=
s
r
c
1
∗
a
l
p
h
a
+
s
r
c
2
∗
b
e
t
a
+
g
a
m
m
a
dst = src1 * alpha + src2 * beta + gamma
dst=src1∗alpha+src2∗beta+gamma,推荐取 beta=1-alpha, gamma=0
。
alpha/beta
:第一、二张图像 的权重,通常取为 0~1 之间的浮点数gamma
: 灰度系数,图像校正的偏移量,用于调节亮度dtype
:输出图像的深度,即每个像素值的位数,可选项,default=src1.depth()
img1 = cv2.imread("../images/imgGaia.tif") # 读取图像 imgGaia
img2 = cv2.imread("../images/imgLena.tif") # 读取图像 imgLena
imgAddW1 = cv2.addWeighted(img1, 0.2, img2, 0.8, 0) # 加权相加, a=0.2, b=0.8
imgAddW2 = cv2.addWeighted(img1, 0.5, img2, 0.5, 0) # 加权相加, a=0.5, b=0.5
imgAddW3 = cv2.addWeighted(img1, 0.8, img2, 0.2, 0) # 加权相加, a=0.8, b=0.2
plt.subplot(131), plt.title("1. a=0.2, b=0.8"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddW1, cv2.COLOR_BGR2RGB)) # 显示 img1(RGB)
plt.subplot(132), plt.title("2. a=0.5, b=0.5"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddW2, cv2.COLOR_BGR2RGB)) # 显示 imgAddV(RGB)
plt.subplot(133), plt.title("3. a=0.8, b=0.2"), plt.axis('off')
plt.imshow(cv2.cvtColor(imgAddW3, cv2.COLOR_BGR2RGB)) # 显示 imgAddS(RGB)
plt.show()
不同尺寸图像的相加,可先将二者调整到同一尺寸,方法见本章4.6节。
1.2 乘法/对比度(cv2.multiply)
对比度是图像像素值的差异,将像素值与常数相乘可以使差值变大或变小。
matrix1 = np.ones(img_rgb.shape) * 0.5
matrix2 = np.ones(img_rgb.shape) * 1.5
# 先将img_rgb转为浮点数类型进行乘法计算,计算完成后,我们再将结果转换回np.uint8类型
# 因为图像数据通常是以8位无符号整数形式存储的。
img_rgb_darker = np.uint8(cv2.multiply(np.float64(img_rgb), matrix1))
img_rgb_brighter = np.uint8(cv2.multiply(np.float64(img_rgb), matrix2))
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(img_rgb_darker); plt.title("Lower Contrast");
plt.subplot(132); plt.imshow(img_rgb); plt.title("Original");
plt.subplot(133); plt.imshow(img_rgb_brighter);plt.title("Higher Contrast");
右边图会发现图像的某些区域看到奇怪的颜色,这是因为相乘后某些像素值>255,已经溢出,这该如何处理呢?
1.3 使用np.clip处理溢出
np.clip
函数用于裁剪数组中的元素,使其值位于指定的范围之内,其语法为:
np.clip(a, a_min, a_max)
下面是一个简单的示例:
img_rgb_higher = np.uint8(np.clip(cv2.multiply(np.float64(img_rgb), matrix2),0,255))
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(img_rgb_lower); plt.title("Lower Contrast");
plt.subplot(132); plt.imshow(img_rgb); plt.title("Original");
plt.subplot(133); plt.imshow(img_rgb_higher);plt.title("Higher Contrast");
1.4 图像阈值(cv2.threshold)
1.4.1 阈值处理基础
阈值就是临界值。根据阈值可将图像划分为多个区域,或提取图像中的目标物体,是最基本的阈值处理方法。图像阈值处理简单、直观,计算速度快,是图像处理的基础操作,在图像分割中处于核心地位。
例如,图像由暗色背景上的亮目标组成,目标像素和背景像素的灰度值组合构成两种主要模式,可以通过设定适当的阈值 T,将图像的像素划分为两类:灰度值大于 T 的像素集是目标,小于 T 的像素集是背景。
当 T 是应用于整幅图像的常数,称为全局阈值处理;当 T 对于整幅图像发生变化时,称为可变阈值处理。有时,对应于图像中任一点的 T 值取决于该点的邻域的限制,称为局部阈值处理。
1.4.2 固定阈值处理函数cv.threshold
OpenCV 提供了函数 cv.threshold 可以对图像进行阈值处理。此函数可以将灰度图像转换为二值图像(将灰度大于阈值的像素点置为255,小于阈值的像素点置为0,得到二值图像,可以突出图像轮廓,把目标从背景中分割出来)。或用于消除噪声,即过滤出值太小或太大的像素。
二值图像在图像处理中有很多用例,最常见的用例之一是创建掩码。图像蒙版允许我们对图像的特定部分进行处理,使其他部分保持完整。
cv2.threshold
函数语法为:
retval, dst = cv2.threshold( src, thresh, maxval, type[, dst] )
dst
:阈值变化后的图像,与src具有相同大小和类型以及通道数的输出数组。src
:输入数组(多通道,8位或32位浮点,文档确实这么写的)。thresh
:阈值。retval
:阈值,浮点型maxval
:用于THRESH_BINARY
和THRESH_MINARY_INV
阈值类型的最大值,一般取 255。type
:阈值类型(请参见阈值类型)。- cv2.THRESH_BINARY:大于阈值时置 255,否则置 0
- cv2.THRESH_BINARY_INV:大于阈值时置 0,否则置 255
- cv2.THRESH_TRUNC:大于阈值时置为阈值 thresh,否则不变(保持原色)
- cv2.THRESH_TOZERO:大于阈值时不变(保持原色),否则置 0
- cv2.THRESH_TOZERO_INV:大于阈值时置 0,否则不变(保持原色)
- cv2.THRESH_OTSU:使用 OTSU 算法选择阈值
- 确切地说,只有 type 为
cv2.THRESH_BINARY
或cv2.THRESH_BINARY_INV
时输出为二值图像,其它变换类型时进行阈值处理但并不是二值处理。
特殊值THRESH_OTSU或THRESH_TRIANGLE可以与上述值之一组合。在这些情况下,函数使用Otsu或Triangle算法确定最佳阈值,并使用它代替指定的阈值。Otsu和Triangle方法仅用于8位单通道图像。
当图像中存在高斯噪声时,通常难以通过全局阈值将图像的边界完全分开。如果图像的边界是在局部对比下出现的,不同位置的阈值也不同,使用全局阈值的效果将会很差。如果图像的直方图存在明显边界,容易找到图像的分割阈值;但如果图像直方图分界不明显,则很难找到合适的阈值,甚至可能无法找到固定的阈值有效地分割图像。
img_read = cv2.imread("building-windows.jpg", 0) # 灰度图
retval, img_thresh = cv2.threshold(img_read, 100, 255, cv2.THRESH_BINARY)
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(121); plt.imshow(img_read, cmap="gray"); plt.title("Original");
plt.subplot(122); plt.imshow(img_thresh, cmap="gray"); plt.title("Thresholded");
print(retval,img_thresh.shape)
(572, 800) 100.0 (572, 800)
1.4.3 全局阈值处理 Otsu 方法(可跳过本节)
当图像中的目标和背景的灰度分布较为明显时,可以对整个图像使用固定阈值进行全局阈值处理。 为了获得适当的全局阈值,可以基于灰度直方图进行迭代计算(详见【youcans 的 OpenCV 例程200篇】159. 图像分割之全局阈值处理),另一种改进算法是OTSU 方法(又称大津算法)。使用最大化类间方差(intra-class variance)作为评价准则,基于对图像直方图的计算,可以给出类间最优分离的最优阈值。
任取一个灰度值 T,可以将图像分割为两个集合 F 和 B,集合 F、B 的像素数的占比分别为 pF、pB,集合 F、B 的灰度值均值分别为 mF、mB,图像灰度值为 m,定义类间方差为:
I
C
V
=
p
F
∗
(
m
F
−
m
)
2
+
p
B
∗
(
m
B
−
m
)
2
ICV = p_F * (m_F - m)^2 + p_B * (m_B - m)^2
ICV=pF∗(mF−m)2+pB∗(mB−m)2
使类间方差 ICV 最大化的灰度值 T 就是最优阈值。因此,只要遍历所有的灰度值,就可以得到使 ICV 最大的最优阈值 T。
OpenCV 提供了函数 cv.threshold 可以对图像进行阈值处理,将参数 type 设为 cv.THRESH_OTSU
,就可以使用使用 OTSU 算法进行最优阈值分割。
img = cv2.imread("../images/Fig1039a.tif", flags=0)
deltaT = 1 # 预定义值
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
grayScale = range(256) # 灰度级 [0,255]
totalPixels = img.shape[0] * img.shape[1] # 像素总数
totalGray = np.dot(histCV[:,0], grayScale) # 内积, 总和灰度值
T = round(totalGray/totalPixels) # 平均灰度
while True:
numC1, sumC1 = 0, 0
for i in range(T): # 计算 C1: (0,T) 平均灰度
numC1 += histCV[i,0] # C1 像素数量
sumC1 += histCV[i,0] * i # C1 灰度值总和
numC2, sumC2 = (totalPixels-numC1), (totalGray-sumC1) # C2 像素数量, 灰度值总和
T1 = round(sumC1/numC1) # C1 平均灰度
T2 = round(sumC2/numC2) # C2 平均灰度
Tnew = round((T1+T2)/2) # 计算新的阈值
print("T={}, m1={}, m2={}, Tnew={}".format(T, T1, T2, Tnew))
if abs(T-Tnew) < deltaT: # 等价于 T==Tnew
break
else:
T = Tnew
# 阈值处理
ret1, imgBin = cv2.threshold(img, T, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=T
ret2, imgOtsu = cv2.threshold(img, T, 255, cv2.THRESH_OTSU) # 阈值分割, thresh=T
print(ret1, ret2)
plt.figure(figsize=(7,7))
plt.subplot(221), plt.axis('off'), plt.title("Origin"), plt.imshow(img, 'gray')
plt.subplot(222, yticks=[]), plt.title("Gray Hist") # 直方图
histNP, bins = np.histogram(img.flatten(), bins=255, range=[0, 255], density=True)
plt.bar(bins[:-1], histNP[:])
plt.subplot(223), plt.title("global binary(T={})".format(T)), plt.axis('off')
plt.imshow(imgBin, 'gray')
plt.subplot(224), plt.title("OTSU binary(T={})".format(round(ret2))), plt.axis('off')
plt.imshow(imgOtsu, 'gray')
plt.tight_layout()
plt.show()
全局阈值处理还有一些其它改进方法,比如处理前先对图像进行平滑、基于边缘信息改进全局阈值处理等等。
1.4.4 自适应阈值处理函数cv.adaptiveThreshold
噪声和非均匀光照等因素对阈值处理的影响很大,例如光照复杂时 Otsu
算法等全局阈值分割方法的效果往往不太理想,需要使用可变阈值处理。
可变阈值是指对于图像中的每个像素点或像素块有不同的阈值,如果该像素点大于其对应的阈值则认为是前景。可变阈值处理的基本方法,是对图像中的每个点,根据其邻域的性质计算阈值。标准差和均值是对比度和平均灰度的描述,在局部阈值处理中非常有效。
局部阈值分割可以根据图像的局部特征进行处理,与图像像素位置、灰度值及邻域特征值有关。
cv.adaptiveThreshold为自适应阈值的二值化处理函数,可以通过比较像素点与周围像素点的关系动态调整阈值。函数语法:
dst = cv.adaptiveThreshold( src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst] )
dst
:与src大小和类型相同的目标图像。src
:8位单通道图像。maxValue
:为满足条件的像素指定的非零值,详见阈值类型说明。adaptiveMethod
:要使用的自适应阈值算法- cv.ADAPTIVE_THRESH_MEAN_C:阈值是邻域的均值
- cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域的高斯核加权均值
- 其它请参考AdaptiveThresholdTypes。
thresholdType
:阈值类型,必须是THRESH_BINARY或THRESH_MINARY_INV。cv2.THRESH_BINARY
:大于阈值时置 maxValue,否则置 0cv2.THRESH_BINARY_INV
:大于阈值时置 0,否则置 maxValue
blockSize
:用于计算像素阈值的像素邻域的尺寸,可以取3、5、7,依此类推。C
: 偏移量,从平均值或加权平均值中减去该常数。
1.4.5 应用:乐谱阅读器
假设您想构建一个可以读取(解码)乐谱的应用程序。这类似于文本文档的光学字符识别(OCR,其目标是识别文本字符)。
在这两种应用程序中,处理管道的第一步是隔离文档图像中的重要信息(将其与背景分离)。这项任务可以通过阈值技术来完成。让我们看一个例子:
# 正常读取图像
img_read = cv2.imread("Piano_Sheet_Music.png", 0)
# 全局阈值1
retval, img_thresh_gbl_1 = cv2.threshold(img_read,50, 255, cv2.THRESH_BINARY)
# 全局阈值2
retval, img_thresh_gbl_2 = cv2.threshold(img_read,130, 255, cv2.THRESH_BINARY)
# 自适应阈值
img_thresh_adp = cv2.adaptiveThreshold(img_read, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 7)
# Show the images
plt.figure(figsize=[18,15])
plt.subplot(221); plt.imshow(img_read, cmap="gray"); plt.title("Original");
plt.subplot(222); plt.imshow(img_thresh_gbl_1,cmap="gray"); plt.title("Thresholded (global: 50)");
plt.subplot(223); plt.imshow(img_thresh_gbl_2,cmap="gray"); plt.title("Thresholded (global: 130)");
plt.subplot(224); plt.imshow(img_thresh_adp, cmap="gray"); plt.title("Thresholded (adaptive)");
1.5 位运算
在OpenCV中,位运算通常指的是对图像的像素值进行位操作,即将每个十进制的像素值看作 8 位二进制数,按位对每一对像素值执行对应的位运算,最后将结果转换回十进制像素值。常见位运算包括:
-
cv2.bitwise_and
: result ( i , j ) = src1 ( i , j ) & src2 ( i , j ) \text{result}(i, j) = \text{src1}(i, j) \, \& \, \text{src2}(i, j) result(i,j)=src1(i,j)&src2(i,j)- 按位 “与” 操作, 对应位都为1的情况下,结果为1,否则是 0。(黑色和任何颜色做与操作都是黑色,白色与任何颜色进行与操作的结果都是那个颜色,图像与自身做与操作,结果不变)
- 效果: 主要用于图像的区域掩码。例如,在图像中通过与一个掩码图像的与运算可以保留特定的区域,而其他地方为黑色。
-
bitwise_or
: result ( i , j ) = src1 ( i , j ) ∣ src2 ( i , j ) \text{result}(i, j) = \text{src1}(i, j) \, | \, \text{src2}(i, j) result(i,j)=src1(i,j)∣src2(i,j)- 按位 “或” 操作,对应位只要有一个为1,结果就为1,否则为0。
- 效果: 将两个图像合并,重叠的部分会亮起,非重叠的部分也会被保留下来。
-
bitwise_xor
: result ( i , j ) = src1 ( i , j ) ⊕ src2 ( i , j ) \text{result}(i, j) = \text{src1}(i, j) \, \oplus \, \text{src2}(i, j) result(i,j)=src1(i,j)⊕src2(i,j)- 按位 “异或” 操作,对应位不同为1,相同为0。
- 效果: 只保留两个图像中不同的部分,重叠的部分将变为黑色。
-
bitwise_not
: result ( i , j ) = ¬ src ( i , j ) \text{result}(i, j) = \neg \text{src}(i, j) result(i,j)=¬src(i,j)- 按位 “非” 操作(即按位取反),所有的 0 变成 1,所有的 1 变成 0。
- 效果: 反转图像颜色,黑色变成白色,白色变成黑色。
以and位运算举例,其语法为:
cv2.bitwise_and(src1, src2, dst=None, mask=None)
src1,src2
: 这两个参数是要进行位运算的源图像。dst
: 可选,存储位运算结果的图像。如果未指定,将创建一个与src1相同大小的图像。mask
:可选,一个单通道的8位灰度图像(每个元素的值要么是0要么是255),用作掩码。当你提供了 mask,OpenCV 只会在 掩码中像素值为为 255(白色)的区域内对 src1 和 src2 进行按位 “与” 操作,mask为0的区域直接填充0。
也可以理解为
src1,src2
先做与运算,再与mask
做与运算。那么mask是白色的部分结果不变,黑色部分保留黑色(黑色和任何颜色与运算结果都是黑色)。
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from IPython.display import Image
# 创建两个图像
img1=np.full((300, 300,3), 255, dtype=np.uint8)
img2 = np.full((300, 300,3), 255, dtype=np.uint8)
# 在img1上绘制一个白色矩形
cv2.rectangle(img1, (50, 50), (250, 250), (255, 0, 0), -1)
# 在img2上绘制一个白色圆
cv2.circle(img2, (150, 150), 100, (0, 0, 0), -1)
# 创建一个掩码(mask),掩码上绘制一个白色矩形
mask = np.zeros((300, 300), dtype=np.uint8)
cv2.rectangle(mask, (100, 100), (200, 200), 255, -1)
# 使用mask进行按位与操作
result= cv2.bitwise_and(img1, img2)
result_mask = cv2.bitwise_and(img1, img2, mask=mask)
# 显示图像
plt.subplot(131); plt.imshow(img1,cmap="gray"); plt.title("img1");
plt.subplot(132); plt.imshow(img2,cmap="gray"); plt.title("img2");
plt.subplot(133); plt.imshow(mask,cmap="gray"); plt.title("mask");
plt.subplot(121); plt.imshow(result,cmap="gray"); plt.title("result");
plt.subplot(122); plt.imshow(result_mask,cmap="gray"); plt.title("result_mask");
位运算应用场景
- 图像合并:通过 bitwise_and、bitwise_or 等操作,可以根据掩码合并图像的特定区域。
- 图像遮罩:通过位运算可以使用遮罩图像来选择性显示部分区域。
- 二值化操作:可以在处理二值图像时使用这些位运算进行更复杂的图像处理。
- 背景去除:通过掩码和位运算,能将背景移除,只保留感兴趣的前景区域。
下面举例说明,先正常读取一张矩形图和一张圆形图:
img_rec = cv2.imread("rectangle.jpg", 0)
img_cir = cv2.imread("circle.jpg", 0)
plt.figure(figsize=[20,5])
plt.subplot(121);plt.imshow(img_rec,cmap='gray')
plt.subplot(122);plt.imshow(img_cir,cmap='gray')
print(img_rec.shape,img_cir.shape)
(200, 499) (200, 499)
下面依次进行and、or、xor、not操作
img_and = cv2.bitwise_and(img_rec, img_cir, mask = None) # 与操作中一方是黑就显示黑
img_or= cv2.bitwise_or(img_rec, img_cir, mask = None) # 或操作中一方是白就显示白
img_xor= cv2.bitwise_xor(img_rec, img_cir, mask = None)
img_not=cv2.bitwise_not(img_rec, img_cir, mask = None)
#plt.imshow(result,cmap='gray')
plt.figure(figsize=[10,5])
plt.subplot(221); plt.imshow(img_and,cmap="gray"); plt.title("AND");
plt.subplot(222); plt.imshow(img_or,cmap="gray"); plt.title("OR");
plt.subplot(223); plt.imshow(img_xor,cmap="gray"); plt.title("XOR");
plt.subplot(224); plt.imshow(img_not,cmap="gray"); plt.title("NOT");
1.6 图像的叠加:制作coca-cola彩色Logo
两张图像直接进行加法运算后图像的颜色会改变,通过加权加法实现图像混合后图像的透明度会改变,都不能实现图像的叠加。
实现图像的叠加,需要综合运用图像阈值处理、图像掩模、位操作和图像加法的操作。下面展示如何用背景图像填充可口可乐Logo的白色字母。
Image(filename='Logo_Manipulation.png')
- 读取logo图片和背景图片,调整后者尺寸使二者尺寸相同
- 对前景图片(logo)进行二值化处理,生成黑白掩模图像 mask及其反转掩模图像 mask_Inv
- 以黑白掩模 mask作为掩模,对背景图像进行位操作,得到叠加背景图片Add_Background(只得到彩色logo)
- 以反转掩模 mask_Inv作为掩模,对前景图像进行位操作,得到叠加前景图像Foreground;(只得到除logo之外的区域)
- 二者通过 cv2.add 加法运算,得到叠加图像
1.读取Logo图片
img_bgr = cv2.imread("coca-cola-logo.png")
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
logo_w,logo_h = img_rgb.shape[0],img_rgb.shape[1]
print(img_rgb.shape)
(700, 700, 3)
- 读取背景图片
img_background_bgr = cv2.imread("checkerboard_color.png")
img_background_rgb = cv2.cvtColor(img_background_bgr, cv2.COLOR_BGR2RGB)
# 调整图片宽度,并保持高宽比不变
aspect_ratio = logo_w / img_background_rgb.shape[1]
dim = (logo_w, int(img_background_rgb.shape[0] * aspect_ratio))
# 背景图resize到和Logo一样大小
img_background_rgb = cv2.resize(img_background_rgb, dim, interpolation=cv2.INTER_AREA)
print(img_background_rgb.shape)
(700, 700, 3)
- 创建掩码mask
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) # RGB转灰度图
# 使用全局阈值创建Logo二值mask
retval, img_mask = cv2.threshold(img_gray,127,255,cv2.THRESH_BINARY)
print(img_mask.shape)
- 反转mask
img_mask_inv = cv2.bitwise_not(img_mask)
plt.figure(figsize=[8,8])
plt.subplot(221); plt.imshow(img_rgb); plt.title("RGB");
plt.subplot(222); plt.imshow(img_background_rgb); plt.title("Background");
plt.subplot(223); plt.imshow(img_mask,cmap="gray"); plt.title("Mask");
plt.subplot(224); plt.imshow(img_mask_inv,cmap="gray"); plt.title("Mask_inv");
- mask图像加上背景
# 在logo字母上加上彩色背景
img_background = cv2.bitwise_and(img_background_rgb, img_background_rgb, mask=img_mask)
plt.imshow(img_background)
- 将前景与图像分开
# img_mask_inv白色部分红
img_foreground = cv2.bitwise_and(img_rgb, img_rgb, mask=img_mask_inv)
plt.imshow(img_foreground)
- 前景与背景相加得到最终结果
result = cv2.add(img_background,img_foreground)
plt.imshow(result)
cv2.imwrite("logo_final.png", result[:,:,::-1])
plt.figure(figsize=[15,5])
plt.subplot(141); plt.imshow(img_background); plt.title("Add_Background");
plt.subplot(142); plt.imshow(img_foreground); plt.title("Foreground");
plt.subplot(143); plt.imshow(result,cmap="gray"); plt.title("Result");
二、滤波器(Filter)
2.1 简介
2.1.1 滤波器原理
在 OpenCV 中,滤波器是一种对图像进行处理的工具。通过对图像进行卷积操作,来增强图像的某些特征或去除噪声(平滑、锐化、去噪等)。滤波器的原理主要是通过一个卷积核(kernel)对图像进行卷积,从而改变图像像素值。
滤波器可以直接理解为卷积神经网络(CNN)中的卷积核(kernel),两者在核心原理上是相同的,都是通过卷积操作来处理图像信息,提取特征,但它们的使用方式和目的有所不同:
Filter
:传统滤波器的参数是预定义的,专应用于特定任务。如图像处理中的去噪、平滑、锐化等,滤波器的设计依赖于人类的先验知识,例如高斯滤波器用于去噪,Sobel滤波器用于边缘检测。Kernel
:卷积神经网络中的卷积核参数则通过反向传播算法和梯度下降优化自动学习的,能够在不同的任务中学习到特定的、复杂的特征。Filter
多用于低层次的图像处理,如平滑、锐化、边缘检测等,它通常应用于图像的像素级操作。Kernel
则可以在不同层次的特征提取中应用。例如在CNN的前层,它可能类似于传统滤波器,提取简单的边缘和纹理信息;而在更深的层次,它可以提取更抽象的特征,帮助进行高层次的任务,如物体识别、语义分割等。
可以认为,CNN中的卷积核是传统滤波器概念在深度学习中的自然扩展和进化版本。
2.1.2 卷积函数filter2D
filter2D
是 OpenCV 中用于对图像进行二维卷积操作的一个函数,它允许使用自定义的滤波器(卷积核)对图像进行各种滤波处理,其语法为:
dst = cv2.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])
-
src
::输入图像(可以是灰度图或彩色图像)。 -
ddepth
:输出图像的深度,即像素数据类型,可以是以下类型之一:CV_8U
:无符号8位整数。CV_16U
:无符号16位整数。CV_16S
:有符号16位整数。CV_32F
:32位浮点数。CV_64F
:64位浮点数。- 若设置为
-1
,则输出图像与输入图像的深度相同。
-
kernel
:卷积核(滤波器)。这是一个大小为(m, n)
的二维矩阵,用于卷积操作。 -
.
dst
(可选)::输出图像的存储位置。如果不提供,函数会自动创建一个与输入图像大小和类型相同的图像。 -
anchor
(可选):卷积核的锚点。默认为(-1, -1)
,表示锚点位于卷积核的中心。你可以将其设置为其他值来改变卷积核的相对位置。 -
delta
(可选):用于在卷积结果中添加一个常量偏移。默认值为0
。 -
borderType
(可选):边界填充方法,用于处理图像边界问题。常见的边界类型包括:cv2.BORDER_CONSTANT
: 填充常数值。cv2.BORDER_REPLICATE
: 重复最近的边界像素。cv2.BORDER_REFLECT
: 反射边界。cv2.BORDER_WRAP
: 环绕边界。cv2.BORDER_DEFAULT
: 使用默认的边界策略。
以下是使用 filter2D
进行图像平滑的一个示例:
import cv2
import numpy as np
#导入图片
img = cv2.imread('./dog.jpeg')
kernel = np.ones((5, 5), np.float32) / 25 # 5×5的均值滤波器
dst = cv2.filter2D(img, -1, kernel) # ddepth = -1 表示图片的数据类型不变
# 很明显卷积之后的图片模糊了.
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
在这个示例中,使用了一个 3x3
的均值滤波器来平滑图像。原始图片中的每个点都被平均了一下, 所以图像变模糊了.。你可以根据需求设计不同的卷积核,例如用于边缘检测、锐化等。常见卷积核:
-
均值滤波器(用于平滑图像):
kernel = np.ones((3, 3), np.float32) / 9
-
锐化滤波器(用于图像锐化):
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
-
Sobel 滤波器(用于边缘检测):
kernel = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
2.1.3 滤波器分类
功能 | 滤波器类型 | 描述 |
---|---|---|
去噪 | 均值滤波器、高斯滤波器、中值滤波器 | 用于去除图像中的随机噪声 |
边缘检测 | 高通滤波器(Sobel、Canny 边缘检测) | 用于检测图像中的边缘信息 |
图像增强 | 锐化滤波器 | 用于提高图像的细节对比度,增强图像细节 |
图像平滑 | 低通滤波器(高斯模糊) | 通过减少高频噪声实现图像平滑 |
保边去噪 | 双边滤波器 | 在去除噪声的同时保留图像的边缘细节 |
根据以上应用场景,滤波器可以分为以下几种主要类型:
滤波器类型 | 作用 | 原理 | 常见类型 |
---|---|---|---|
低通滤波器(LPF) | 平滑图像,去除噪声和细节 | 抑制高频成分(即快速变化的像素) 保留低频成分 (即平滑部分) | 方盒滤波(Box Filter):平均周围像素值来平滑图像 均值滤波器(Mean Filter):计算周围像素的平均值 高斯滤波器(Gaussian Filter):使用高斯函数对像素进行加权平均。 |
高通滤波器(HPF) | 增强图像的边缘或细节 | 抑制低频成分,突出高频成分 | Sobel滤波器:用于检测边缘。 Laplacian滤波器: 用于增强图像细节 |
中值滤波器(Median Filter) | 去除椒盐噪声 (salt-and-pepper noise) | 替换当前像素值为邻域内像素的中值,去除孤立噪声点 | 无 |
双边滤波器(Bilateral Filter) | 保留边缘的情况下平滑图像 | 结合空间邻域与像素值相似度,保留边缘 | 无 |
自适应滤波器(Adaptive Filter) | 处理不均匀噪声 | 根据局部统计特性调整滤波器参数 | 无 |
2.2 降噪
2.2.1 方盒滤波器
方盒滤波器(boxFilter
) 的滤波器核(卷积核)是一种均匀的矩阵,每个元素的值相同。这样计算时,每个像素的值由周围的像素值的平均值决定,因此能够去除噪声,但图像的边缘可能会变得模糊。其语法为:
boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]])
src
:输入图像ddepth
:输出图像的深度ksize
:滤波器的大小normalize
:是否对计算结果进行归一化。
例如一个3×3的方盒滤波器,未归一化时卷积核是:
[1, 1, 1]
[1, 1, 1]
[1, 1, 1]
这种情况下,卷积的结果是该局部区域内所有像素的简单累加和,不做平均处理。这样操作的结果会可能超出正常的像素范围(0-255),但对于某些特殊应用场景,这样做可能有特定用途。
归一化时的卷积核是:
[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]
[1/9, 1/9, 1/9]
这种情况下,最终得到的结果会是区域内像素的平均值,方盒滤波器等价于均值滤波器。
# 方盒滤波
import cv2
import numpy as np
img = cv2.imread('dog.jpeg')
# 不用手动创建卷积核, 只需要告诉方盒滤波, 卷积核的大小是多少.
dst = cv2.boxFilter(img, -1, (5, 5), normalize=True)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
2.2.2 均值滤波器
blur
是 boxFilter
的简化版,即归一化的方盒滤波,可快速实现均值模糊效果,其语法为:
blur(src, ksize[, dst[, anchor[, borderType]]])
# 均值滤波
img = cv2.imread('dog.jpeg')
dst = cv2.blur(img, (5, 5))
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
2.2.3 高斯滤波器
2.2.3.1 高斯函数
高斯分布(正态分布)是一种常见的概率分布,用来描述数据在均值附近集中、且两端逐渐减少的分布形式。它的概率密度函数正是一个一维的高斯函数,定义如下:
G
(
x
)
=
1
2
π
σ
exp
(
−
(
x
−
μ
)
2
2
σ
2
)
G(x) = \frac{1}{\sqrt{2\pi}\sigma} \exp\left( -\frac{(x - \mu)^2}{2\sigma^2} \right)
G(x)=2πσ1exp(−2σ2(x−μ)2)
x
是输入变量(如时间、空间坐标等)。μ
是高斯函数的均值,表示钟形曲线的中心位置(通常是0)。σ
是标准差,决定了钟形曲线的宽度,σ
越大,曲线越平滑。exp()
表示指数函数。
在图像处理中,我们常用的是二维高斯函数,用于高斯滤波,公式如下:
G ( x , y ) = 1 2 π σ 2 exp ( − ( x − μ x ) 2 + ( y − μ y ) 2 2 σ 2 ) G(x, y) = \frac{1}{2\pi\sigma^2} \exp\left( -\frac{(x - \mu_x)^2 + (y - \mu_y)^2}{2\sigma^2} \right) G(x,y)=2πσ21exp(−2σ2(x−μx)2+(y−μy)2)
(x, y)
是二维空间中的坐标。μ_x
和μ_y
是高斯函数的中心点坐标。σ
是标准差,控制滤波器的范围和影响。
2.2.3.2 高斯滤波器
使用符合高斯分布的卷积核对图像进行卷积,以平滑图像。高斯模糊通过邻域像素的加权平均来减少噪声,其中靠近中心的像素权重大,远离中心的像素权重小。其本质是对图像中的每一个像素点,通过其周围像素的加权平均值来进行模糊处理,通常适用于减少图像中的随机噪声,其特点是:
- 可以有效降低图像中的高频噪声或减少图像中的随机点
- 对所有像素均一处理,因此边缘也会被模糊
- 计算速度快,常用于一般的噪声去除和图像预处理
高斯滤波的重点就是如何计算符合高斯分布的卷积核(高斯模板)。假设卷积核尺寸为3×3
,中心点的坐标是(0,0)
,则9个格子的坐标如下图左侧所示。我们假设
σ
=
1.5
\sigma=1.5
σ=1.5,代入二维高斯函数公式,可计算出整个卷积核为:
- 计算
(0,0)
坐标点,对应的值: 1 / ( 2 ∗ n p . p i ∗ 1.5 ∗ ∗ 2 ) = 0.00707355 1 / (2 * np.pi * 1.5**2)=0.00707355 1/(2∗np.pi∗1.5∗∗2)=0.00707355- 计算
(-1, 1)
坐标点对应的值 1 / ( 2 ∗ n p . p i ∗ 1.5 ∗ ∗ 2 ) ∗ n p . e x p ( − ( 2 / ( 2 ∗ 1.5 ∗ ∗ 2 ) ) ) = 0.0453542 1 / (2 * np.pi * 1.5**2)* np.exp(-(2/(2*1.5**2)))=0.0453542 1/(2∗np.pi∗1.5∗∗2)∗np.exp(−(2/(2∗1.5∗∗2)))=0.0453542
我们可以观察到越靠近中心,数值越大;越边缘的数值越小,符合高斯分布的特点。
通过高斯函数计算出来的是概率密度函数,而这9个点的权重总和等于0.4787147。为了确保这九个点加起来和为1,上面9个值还要分别除以0.4787147,得到最终的高斯模板。有些高斯模板都是整数值,这是对模板中每个数除上左上角的值,,然后取整得到的结果。
卷积核确定了之后,就可以进行卷积计算了。将下面这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。
在OpenCV中,我们使用GaussianBlur
进行高斯滤波计算,通过应用高斯滤波器(Gaussian filter)来平滑图像,减少噪声和细节,使图像看起来更平滑。其语法为:
GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
src
:输入图像,可以是彩色图像(BGR)或灰度图像。ksize
:高斯核的大小,必须是正的奇数,例如(5,5)。sigmaX
:X方向上的标准差(控制模糊程度)。当sigmaX
值为 0 时,OpenCV 会根据ksize
自动计算。sigmaY
(可选):Y方向上的标准差。如果这个值为 0,函数将使用sigmaX
的值。borderType
(可选):边界模式,决定如何处理图像边界。默认值为cv2.BORDER_DEFAULT
。
import cv2
# 读取图像
image = cv2.imread('lena.jpg')
# 使用 GaussianBlur
blurred_image = cv2.GaussianBlur(image, (5, 5), 1)
# 显示图像
cv2.imshow('Blurred Image', blurred_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
如果对经典的gaussian.png进行处理,效果如下,原图中很细小的白点被消除了:
2.2.4 中值滤波器
中值滤波器(medianBlur)的原理就是对每个卷积窗口中的所有像素值进行排序,然后取中间值作为卷积计算结果。这种操作在去除椒盐噪声等突变噪声(salt-and-pepper noise)方面特别有效。
在含有椒盐噪声的图像中,像素值要么非常高(白点),要么非常低(黑点)。中值滤波器能够很好地将这些异常点替换为邻域中更接近的值,从而有效去除这种噪声。
假设我们有一个 3x3 的图像区域(卷积窗口),如下所示:
src=array([[74, 67, 71],
[74, 69, 71],
[66, 68, 73]], dtype=uint8)
如果我们用一个 3x3 的中值滤波器来处理这个窗口,先将这个区域的所有元素按数值大小进行排序:
[66, 67, 68, 69, 71, 71, 73, 74, 74]
选择排序后的中间值,即第 5 个元素值71就是此区域中心点的卷积计算结果。
在opencv中,我们使用cv2.medianBlur
进行中值滤波计算,其语法为:
dst = cv2.medianBlur(src, ksize)
src
: 输入图像ksize
: 滤波器的大小,必须是正奇数(如 3, 5, 7 等)。窗口的尺寸越大,滤波效果越明显,但图像可能会变得更模糊。
下面是中值滤波的处理效果(ksize=5
):
2.2.5 双边滤波器
这是OpenCV中的一张经典图片,可以看到照片中的女人,帽子是浅黄,帽子上的装饰是蓝色,头发是棕色,脸的颜色也不一样。这样不同的部分都有一个轮廓线,相交的地方颜色变化就非常大。如果在灰度图上看,差异更大,边界左右的像素很可能一个是白色一个是黑色(灰度距离)。高斯滤波器只考虑像素之间的空间信息,所以处理后容易模糊掉物体的边界。
2.2.5.1 工作原理
双边滤波是结合空域和色域的滤波器。同时考虑像素的空间距离和像素强度差异。在滤波过程中,离中心像素越近的像素会权重越大,而颜色和中心像素差异越大的像素权重越小,这样边界处颜色不容易被混合,所以双边滤波可以在保持边缘清晰的情况下,去除图像中的噪声,适合高质量图像处理。
可以简单理解为不对边界进行模糊。
- 空间距离:当前点于中心点的欧氏距离,空间域高斯函数形式如下,用于根据像素的物理距离来加权计算,保证距离中心像素越远的像素权重越小。
G σ s ( x i , x ) = exp ( − ∥ x i − x ∥ 2 2 σ s 2 ) G_{\sigma_s}(x_i, x) = \exp\left(-\frac{\|x_i - x\|^2}{2\sigma_s^2}\right) Gσs(xi,x)=exp(−2σs2∥xi−x∥2)
- G σ s ( x i , x ) G_{\sigma_s}(x_i, x) Gσs(xi,x):表示邻域中像素位置 x i x_i xi 相对于中心像素 x x x 的空间距离权重;
- ∥ x i − x ∥ \|x_i - x\| ∥xi−x∥:表示像素 x i x_i xi 和中心像素 x x x 之间的欧几里得距离(通常为二维距离,考虑的是像素位置的坐标差异);
- σ s \sigma_s σs:控制空间权重的标准差,值越大,意味着更大范围内的像素影响越大。
- 灰度距离:当前点灰度于中心点灰度差的绝对值,其高斯函数形式如下。该函数用于根据像素的灰度值差异来加权计算,确保与中心点灰度差异越大的像素权重越小,从而保留边缘。
G σ r ( I ( x i ) , I ( x ) ) = exp ( − ( I ( x i ) − I ( x ) ) 2 2 σ r 2 ) G_{\sigma_r}(I(x_i), I(x)) = \exp\left(-\frac{(I(x_i) - I(x))^2}{2\sigma_r^2}\right) Gσr(I(xi),I(x))=exp(−2σr2(I(xi)−I(x))2)
- G σ r ( I ( x i ) , I ( x ) ) G_{\sigma_r}(I(x_i), I(x)) Gσr(I(xi),I(x)):表示邻域中像素 x i x_i xi 的灰度值 I ( x i ) I(x_i) I(xi) 相对于中心像素 I ( x ) I(x) I(x) 的灰度差异权重;
- I ( x i ) − I ( x ) I(x_i) - I(x) I(xi)−I(x):表示像素 x i x_i xi 和中心像素 x x x 之间的灰度值差异;
- σ r \sigma_r σr:控制灰度值权重的标准差,值越大,意味着更大灰度差异的像素也可以有较高的权重。
使用灰度距离而不是RGB色空间的距离,能够简化计算、提高效率,并且更符合人类视觉对图像的感知需求。由于亮度是影响图像结构和轮廓的主要因素,灰度值已足够用于保持边缘细节,同时减少噪声。
双边滤波时,最终的权重 W ( x i , x ) W(x_i, x) W(xi,x) 是这两个高斯函数的乘积:
W ( x i , x ) = G σ s ( x i , x ) ⋅ G σ r ( I ( x i ) , I ( x ) ) W(x_i, x) = G_{\sigma_s}(x_i, x) \cdot G_{\sigma_r}(I(x_i), I(x)) W(xi,x)=Gσs(xi,x)⋅Gσr(I(xi),I(x))
处理后的像素值为:
I
′
(
x
)
=
1
W
p
∑
x
i
∈
Ω
I
(
x
i
)
W
(
x
i
,
x
)
I'(x) = \frac{1}{W_p} \sum_{x_i \in \Omega} I(x_i) W(x_i, x)
I′(x)=Wp1xi∈Ω∑I(xi)W(xi,x)
- I ′ ( x ) I'(x) I′(x) :滤波后的位置 x x x 处的像素值;
- I ( x i ) I(x_i) I(xi) :邻域 Ω \Omega Ω 中位置 x i x_i xi 处的像素值;
- W p W_p Wp :权重的归一化系数,确保滤波后的像素值不超出范围。
该权重结合了空间距离和灰度差异,使得双边滤波可以同时对图像进行平滑和边缘保留处理。
- σ s \sigma_s σs:控制空间距离的影响范围,值越大,滤波的模糊效果越强。
- σ r \sigma_r σr:控制灰度差异的敏感性,值越大,保留边缘的能力减弱,值越小,边缘保留能力增强。
- 由于总的权重是两个权重的乘积,所以其速度与比一般的滤波慢很多(计算复杂度为核大小的平方)
上图可以理解为和
p
灰度近似的区域使用高斯滤波,和p
灰度显著差异的q
区域,权重会很小,几乎被忽略。
2.2.5.2 bilateralFilter
OpenCV中使用bilateralFilter
进行双边滤波处理,其语法为:
bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])
src
:输入图像。d
:卷积核大小sigmaColor
:在颜色空间中的滤波器 σ \sigma σ 值。sigmaSpace
:在坐标空间中的滤波器 σ \sigma σ 值。
import cv2
import numpy as np
img = cv2.imread('./lena.png')
dst = cv2.bilateralFilter(img, 7, 20, 50)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
右图是处理之后的结果,可以看到脸部变得更加平滑,有美颜的效果。而如果用双边滤波处理椒盐噪声,就没有效果。因为这些噪声点和周围像素的灰度值差异很大,而差异大的部分双边滤波是不处理的。
2.3 边缘检测
2.3.1 sobel算子
边缘是像素值发生跃迁的位置,是图像的显著特征之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。例如,当图像中一侧较亮,另一侧较暗时,人眼会很容易将这一过渡区域识别为边缘,也就是像素的灰度值快速变化的地方(梯度大的地方)。
由于图像是以像素为单位的离散数据,无法直接应用连续的微分运算,所以需要采用差分运算来计算图像中像素灰度的变化。差分算子通过以下几种基本方式近似图像中某一方向的梯度(导数):
- 一阶前向差分:当前像素与下一个像素的差值。
f ′ ( x ) ≈ f ( x + 1 ) − f ( x ) ] f'(x) \approx f(x+1) - f(x) ] f′(x)≈f(x+1)−f(x)]
- 一阶后向差分:当前像素与前一个像素的差值。
f ′ ( x ) ≈ f ( x ) − f ( x − 1 ) ] f'(x) \approx f(x) - f(x-1) ] f′(x)≈f(x)−f(x−1)]
- 中心差分:用当前像素的前后两个像素的差值来计算当前像素点的导数。 这种方式对称性更好、误差更小。
f ′ ( x ) ≈ f ( x + 1 ) − f ( x − 1 ) 2 ] f'(x) \approx \frac{f(x+1) - f(x-1)}{2} ] f′(x)≈2f(x+1)−f(x−1)]
Sobel 算子是一种基于离散差分的边缘检测算子,它通过计算图像亮度值在水平和垂直方向的近似梯度,来检测图像中的边缘。其核心思想是应用两个3x3
的卷积核,分别计算图像在水平方向(x方向)和垂直方向(y方向)的梯度。梯度越大,说明像素在该方向的变化越大,边缘信号越强。
-
水平方向卷积核 G x G_x Gx:
x
轴差分模式,检测图像在水平方向的亮度变化。G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} Gx= −1−2−1000121
-
垂直方向卷积核 G y G_y Gy:
y
轴差分模式,检测图像在垂直方向的亮度变化。G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} Gy= −101−202−101
计算之后,我们可以得到水平方向梯度 G x G_x Gx和垂直方向梯度: G y G_y Gy。结合这两个方向的梯度,得到该像素点的梯度大小,反映了边缘的强度: G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2 或者采用绝对值相加近似计算: G = ∣ G x ∣ + ∣ G y ∣ G = |G_x| + |G_y| G=∣Gx∣+∣Gy∣
梯度的方向(该点边缘的方向)可以通过计算 G x G_x Gx 和 G y G_y Gy 的比值得到:
θ = arctan ( G y G x ) \theta = \arctan\left(\frac{G_y}{G_x}\right) θ=arctan(GxGy)
在实际应用中,Sobel算子广泛用于边缘检测、图像锐化等任务。你可以通过 OpenCV 提供的cv2.Sobel
函数来进行计算。
Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
src
:输入图像,通常是一个灰度图像。ddepth
:输出图像的深度(数据类型),常用值为cv2.CV_64F
,也可以使用cv2.CV_8U
,cv2.CV_16U
,或者直接写-1
(表示和原图一致)。dx
: x 方向上的导数阶数。dx=1
表示在 x 方向上计算一阶导数,dx=0
则不在 x 方向上计算。dy
: y 方向上的导数阶数。dy=1
表示在 y 方向上计算一阶导数,dy=0
则不在 y 方向上计算。ksize
: 卷积核大小,必须为奇数(1, 3, 5, 7等)。ksize
越大,结果越平滑,但同时细节也可能丢失。ksize=-1
时,Sobel 算子就是 Scharr 算子。scale
: 可选参数,缩放导数的比例系数,默认为 1。delta
: 可选参数,表示添加到结果的值,默认为 0。borderType
: 用于指定如何处理边界像素,默认为cv2.BORDER_DEFAULT
。
下面是一个简单的示例:
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('./chess.png')#
dx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5) # 水平方向梯度,只有垂直方向的边缘
dy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5) # 垂直方向梯度,只有水平方向的边缘
# 如果使用dst = cv2.magnitude(dx, dy),dst亮度会更高。
dst = cv2.add(dx, dy) # 使用addWeighted也可以
plt.figure(figsize=[20,5])
plt.subplot(141);plt.imshow(img,cmap='gray');plt.title("img");
plt.subplot(142);plt.imshow(dx,cmap='gray');plt.title("dx");
plt.subplot(143);plt.imshow(dy,cmap='gray');plt.title("dy");
plt.subplot(144);plt.imshow(dst,cmap='gray');plt.title("dst");
如果设ddepth=-1
而非cv2.CV_64F
,效果如下:
2.3.2 Scharr算子
Sobel
算子使用的卷积核较为简单,但它并不是精确的导数计算,仅仅是一个近似。对于边缘检测任务,导数的近似可能不足以捕捉图像中的高频变化,尤其是在边缘强度变化剧烈的地方。当内核较小时(比如ksize=3
),这种近似可能会放大误差,使得图像中的细节丢失或边缘模糊。
为了解决这一问题,OpenCV 提供了沙尔( Scharr
)算子,它对内核大小为 3 时进行了优化,特别在检测图像中的细小边缘时,计算结果更精确,并且运算速度与 Sobel 算子相当。
Scharr
算子计算过程与Sobel
算子类似,但卷积核系数更大,放大了像素变换的情况,增强了对图像细节的捕捉能力。
- 水平方向: G x = [ 3 0 − 3 10 0 − 10 3 0 − 3 ] G_x = \begin{bmatrix} 3 & 0 & -3 \\ 10 & 0 & -10 \\ 3 & 0 & -3 \end{bmatrix} Gx= 3103000−3−10−3
- 垂直方向: G y = [ 3 10 3 0 0 0 − 3 − 10 − 3 ] G_y = \begin{bmatrix} 3 & 10 & 3 \\ 0 & 0 & 0 \\ -3 & -10 & -3 \end{bmatrix} Gy= 30−3100−1030−3
你可以使用cv2.Scharr
函数进行计算:
Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]) -> dst
img = cv2.imread('./lena.png')
dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0)
dy = cv2.Scharr(img, cv2.CV_64F, dx=0, dy=1)
dst = cv2.addWeighted(dx, 0.5, dy, 0.5, gamma=0)
plt.figure(figsize=[20,5])
plt.subplot(141);plt.imshow(img[:,:,::-1]);plt.title("img");
plt.subplot(142);plt.imshow(dx,cmap='gray');plt.title("dx");
plt.subplot(143);plt.imshow(dy,cmap='gray');plt.title("dy");
plt.subplot(144);plt.imshow(dst,cmap='gray');plt.title("dst");
2.3.3 Laplacian算子
Sobel
算子是通过模拟一阶导数来进行边缘检测,一阶导数变化越大的地方,边缘强度越强。那如果继续对一阶导数f'(t)
求导呢?如下图右侧所示,可以发现边缘处的二阶导数f''(t)=0
, 我们可以利用这一特性去寻找图像的边缘。
Laplacian
算子就是利用了这一思路,通过计算图像的二阶导数,来进行边缘检测。需要注意的是,二阶求导为0的位置也可能是无意义的位置,这些位置一般都是噪声,所以在使用Laplacian算子之前,通常需要对图像进行预平滑处理(如高斯模糊)。Laplacian
滤波器推导过程如下:
-
x
轴方向:- 一阶差分: f ′ ( x ) = f ( x ) − f ( x − 1 ) f'(x) = f(x) - f(x - 1) f′(x)=f(x)−f(x−1)
- 二阶差分: f ′ ′ ( x ) = f ′ ( x + 1 ) − f ′ ( x ) = ( f ( x + 1 ) − f ( x ) ) − ( f ( x ) − f ( x − 1 ) ) f''(x) = f'(x+1) - f'(x) = (f(x + 1) - f(x)) - (f(x) - f(x - 1)) f′′(x)=f′(x+1)−f′(x)=(f(x+1)−f(x))−(f(x)−f(x−1))
- 化简后: f ′ ′ ( x ) = f ( x − 1 ) − 2 f ( x ) ) + f ( x + 1 ) f''(x) = f(x - 1) - 2 f(x)) + f(x + 1) f′′(x)=f(x−1)−2f(x))+f(x+1)
-
y
轴方向:同理可得 f ′ ′ ( y ) = f ( y − 1 ) − 2 f ( y ) ) + f ( y + 1 ) f''(y) = f(y - 1) - 2 f(y)) + f(y + 1) f′′(y)=f(y−1)−2f(y))+f(y+1) -
x
轴,y
轴梯度叠加:f ′ ′ ( x , y ) = f x ′ ( x , y ) + f y ′ ( x , y ) f''(x,y) = f'_x(x,y) + f'_y(x,y) f′′(x,y)=fx′(x,y)+fy′(x,y)
f ′ ′ ( x , y ) = f ( x − 1 , y ) − 2 f ( x , y ) ) + f ( x + 1 , y ) + f ( x , y − 1 ) − 2 f ( x , y ) ) + f ( x , y + 1 ) f''(x,y) = f(x - 1, y) - 2 f(x,y)) + f(x + 1, y) + f(x, y - 1) - 2 f(x,y)) + f(x,y + 1) f′′(x,y)=f(x−1,y)−2f(x,y))+f(x+1,y)+f(x,y−1)−2f(x,y))+f(x,y+1)
f ′ ′ ( x , y ) = f ( x − 1 , y ) + f ( x + 1 , y ) + f ( x , y − 1 ) + f ( x , y + 1 ) − 4 f ( x , y ) ) f''(x,y) = f(x - 1, y) + f(x + 1, y) + f(x, y - 1) + f(x,y + 1) - 4 f(x,y)) f′′(x,y)=f(x−1,y)+f(x+1,y)+f(x,y−1)+f(x,y+1)−4f(x,y))
这个等式可以用矩阵写成:
f ′ ′ ( x , y ) = [ 0 1 0 1 − 4 1 0 1 0 ] ⨀ [ f ( x − 1 , y − 1 ) f ( x , y − 1 ) f ( x + 1 , y − 1 ) f ( x − 1 , y ) f ( x , y ) f ( x + 1 , y ) f ( x − 1 , y + 1 ) f ( x , y + 1 ) f ( x + 1 , y + 1 ) ] f''(x,y) = \left[\begin{matrix}0 & 1 & 0\\1 & -4 & 1\\0 & 1 & 0\end{matrix}\right] \bigodot \left[\begin{matrix}f(x-1, y-1) & f(x, y-1) & f(x+1,y-1)\\f(x-1,y) & f(x,y) & f(x+1,y)\\f(x-1,y+1) & f(x,y+1) & f(x+1,y+1)\end{matrix}\right] f′′(x,y)= 0101−41010 ⨀ f(x−1,y−1)f(x−1,y)f(x−1,y+1)f(x,y−1)f(x,y)f(x,y+1)f(x+1,y−1)f(x+1,y)f(x+1,y+1)
这样就得到了拉普拉斯算子3x3
的卷积核:
[
0
1
0
1
−
4
1
0
1
0
]
\begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}
0101−41010
cv2.Laplacian()
函数语法为:
# 直接对图像求二阶导数,不区分水平垂直方向
Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
简单示例:
import cv2
import numpy as np
img = cv2.imread('./chess.png')
Gaussian= cv2.GaussianBlur(img, (5, 5), 5)
dst = cv2.Laplacian(Gaussian, -1, ksize=5)
plt.figure(figsize=[20,6])
plt.subplot(131);plt.imshow(img[:,:,::-1]);plt.title("img");
plt.subplot(132);plt.imshow(Gaussian[:,:,::-1]);plt.title("Gaussian");
plt.subplot(133);plt.imshow(dst,cmap='gray');plt.title("Laplacian");
可见处理棋盘的效果比Sobel
算子更好,但是处理人物图效果一般。
img = cv2.imread('./lena.png')
Gaussian= cv2.GaussianBlur(img, (5, 5), 5)
Gaussian=cv2.cvtColor(Gaussian, cv2.COLOR_BGR2GRAY)
dst = cv2.Laplacian(Gaussian, -1, ksize=5)
plt.figure(figsize=[20,6])
plt.subplot(131);plt.imshow(img[:,:,::-1]);plt.title("img");
plt.subplot(132);plt.imshow(Gaussian,cmap='gray');plt.title("Gaussian");
plt.subplot(133);plt.imshow(dst,cmap='gray');plt.title("Laplacian");
2.3.4 Canny
OpenCV 中的 Canny
是一种经典的边缘检测方法,旨在找到图像中强烈变化的像素位置,从而检测到图像的边缘。Canny 算法具有多个步骤,能够实现可靠且精确的边缘检测。其主要原理为:
-
高斯滤波(Gaussian Filter):
由于图像中的噪声可能会影响边缘检测结果,Canny 算法的第一步是使用高斯滤波器对图像进行平滑处理。高斯滤波能够去除图像中的噪声,同时保留边缘信息。这一步骤有助于减少误检。 -
计算梯度(Gradient Calculation):
通过对图像进行 Sobel 算子 或 Roberts 算子 计算梯度,生成水平和垂直两个方向的梯度图 G x , G y G_x,G_y Gx,Gy,然后得到图像中每个像素的梯度幅值和梯度方向:- 梯度幅值: G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2
- 梯度方向:
θ
=
arctan
G
y
G
x
\theta = \arctan{\frac{G_y}{G_x}}
θ=arctanGxGy。梯度的方向被归为四类:——垂直,、水平、左下、右上。
同时包含梯度值和梯度方向
-
非极大值抑制(Non-Maximum Suppression):
- 目的:在获取了梯度和方向后,遍历图像,去除所有不是边界的点。目的是为了细化边缘。
- 实现方法:逐个遍历像素点, 判断当前像素点是否是周围像素点中具有相同方向梯度的最大值。如果是,则被保留为潜在的边缘点。
比如下图中,点A、B、C都靠近边缘,且具有相同的梯度方向。其中A的梯度值最大,则保留该点,其它点被抑制(归零)。这样只保留真正的边缘点A,防止边缘变得过宽,增强边缘清晰度。
经过NMS处理后的结果是(保留每一列同方向的最大值):
-
双阈值检测(Double Thresholding):
上一步处理的结果可能有错漏,所以增加一个判定。根据用户提供的两个阈值来判定强边缘和弱边缘。- 强边缘:大于高阈值的像素被认为是强边缘,一定会被保留。小于低阈值的像素将直接被忽略。
- 弱边缘:在高低阈值之间的像素被认为是弱边缘,如果此弱边缘与强边缘相连,则将其视为真正的边缘;否则将其丢弃。这一步骤能够去除噪声中的虚假边缘并保持真正的边缘。
-
最终输出:
经过以上步骤后,Canny 算法会输出一个二值化图像,其中边缘部分的为白色,其它区域为黑色。
我们可以通过cv2.Canny
函数进行计算:
Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) -> edges
- image:输入图像,必须是单通道灰度图像。
- threshold1:低阈值。
- threshold2:高阈值。
- apertureSize (可选):Sobel 算子的大小,默认为 3。
- L2gradient (可选): 布尔类型。如果为
True
,将使用更精确的 L2 范数来计算梯度幅值,即sqrt((dI/dx)^2 + (dI/dy)^2)
。如果为False
,则使用 L1 范数,即|dI/dx| + |dI/dy|
,默认为False
。
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('./lena.png')
lena1 = cv2.Canny(img,60,120)
lena2 = cv2.Canny(img,80,150)
lena3 = cv2.Canny(img,100,200)
plt.figure(figsize=[20,6])
plt.subplot(131);plt.imshow(lena1,cmap='gray');plt.title("lena1");
plt.subplot(132);plt.imshow(lena2,cmap='gray');plt.title("lena2");
plt.subplot(133);plt.imshow(lena3,cmap='gray');plt.title("lena3");
- lena1:threshold1太小,导致很多不是边缘的点没有被过滤,整张图看起来有很多杂质;
- lena3:threshold2太大,一些边缘点被过滤,导致一些轮廓线断断续续,而且细节也不够丰富。
2.3.5 总结
- Sobel算子:广泛用于边缘检测、图像锐化等任务
- Scharr算子:Sobel算子的增强版,用于更精确的边缘检测,尤其适合处理细节丰富的图像。
- Laplacian算子:计算图像的二阶导数,用于检测图像中所有方向的快速变化区域,对噪声敏感,非常适合用来检测图像中的锐利边缘。
- Canny边缘检测:多步骤边缘检测算法,具有良好的精度和鲁棒性,通过双阈值和非极大值抑制减少噪声,保留显著的边缘。