OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
下面我们将以python为示例介绍OpenCV的基本操作
OpenCV中的图片以RGB的形式存储,只不过在OpenCV中的颜色通道不是RGB而是BGR。
import cv2
import numpy as np
# 使用imread()方法读入一个图片,注意图片路径的正确填写
image = cv2.imread("demo.jpg")
# 查看数据的存储维度
print(image.shape)
# 返回:(471, 508, 3),其中表示为图片的长,宽和通道数
print(image)
# 将读入的数据image打印出来
# 如果读入数据失败,返回值将是None,成功则返回array类型
cv2.imwrite("demo.bak.jpg", image)
# 将存储数据的image变量写到磁盘中,写出的文件名为demo.bak.jpg
import cv2
import numpy as np
# 读入图片
image = cv2.imread("demo.jpg")
# cv2.imshow(winname, mat)方法显示图片
# winname:窗口名称
# mat:要显示的图片数据
cv2.imshow("image", image)
# 键盘任意键退出
cv2.waitKey(0)
# 销毁创建的窗口
cv2.destroyAllWindows()
OpenCV能够实现对图片颜色和形状的变换
生活中大部分图片都是彩色图片,存储的颜色模型一般也都是RGB模型。彩色图片的存储形式是3个颜色通道分别用各自的颜色矩阵来记录数据。对于灰度图像来讲,它自然没有3个通道的说法,它的表现形式是一个矩阵。
彩色图片转换为灰度图像的过程中丢失了颜色信息,但是却保留了图片的纹理、线条和轮廓等特征。因此我们往往在对图片进行处理前将图片进行灰度化,使得存储的数据量减少,在对图片进行处理时的计算量也会减少很多。
import cv2
import numpy as np
image = cv2.imread("demo.jpg")
print(image.shape)
# 输出:(471, 508, 3)
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用cv2.cvtColor()方法将彩色图片转换维灰度图片
cv2.imwrite('gray_demo.jpg', gray_img)
print(gray_img.shape)
# 输出:(471, 508)
# 显示灰度化图片
cv2.imshow('image', gray_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
负片操作在很多图像处理软件中也叫反色,其明暗与原图像相反,其色彩则为原图像的补色
由于负片的操作过程比较简单,OpenCV并没有单独封装负片函数。这里我们需要将一张图片拆分为各个颜色通道矩阵,然后分别对每一个颜色通道矩阵进行处理,最后再将其重新组合为一张图片。
负片功能实现
import cv2
import numpy as np
# 读入图片
image = cv2.imread("demo.jpg")
# 获取图片高度和宽度,索引高在前,宽在后
height, weight = image.shape[0], image.shape[1]
# 生成一个空的三维张量,用于存放后续3个通道的数据
negative_file = np.zeros((height, weight, 3))
# 将BGR形式存储的彩色图片拆分成3个颜色通道,注意颜色通道的顺序是蓝、绿、红
b, g, r = cv2.split(image)
# 进行负片化处理,求每个通道颜色的补色
r = 255 - r
g = 255 - g
b = 255 - b
# 将处理后的记过赋值到前面生成的三维张量中
negative_file[:, :, 0] = b
negative_file[:, :, 1] = g
negative_file[:, :, 2] = r
# 将生成的图片保存起来,注意存储图片文件名中的扩展名
cv2.imwrite("negative_demo.jpg", negative_file)
# 注:一定要将数据保存为图片格式后再读取,才能正常显示负片化图片
negative_demo = cv2.imread("negative_demo.jpg")
# 显示负片化图片
cv2.imshow('image', negative_demo)
cv2.waitKey(0)
cv2.destroyAllWindows()
一般来说,图像处理算子是将一幅或多幅图像作为输入数据,产生一幅输出图像的函数。图像变换可分为一下两种。
- 点算子:基于像素变换,在这一类图像变换中,仅仅根据输入像素值(有时可加上某些额外信息)计算相应的输出像素值。
- 领域算子:基于图像区域进行变换。
两种常用的点算子使用常数对点的像素值进行乘法或加法运算,可以表示为
g ( i , j ) = α ⋅ f ( i , j ) + β \ g(i, j) = \alpha \centerdot f(i, j) + \beta\, g(i,j)=α⋅f(i,j)+β
其中,图像中点的位置为(i, j),α值代表增益,β值代表偏置。对图像进行亮度和对比度的变换就是一种点算子,这两个参数分辨可以用来控制对比度与亮度。
我们可以通过调节这两个参数的值,来对图片进行对比度或亮度的调节。即将原图像中的每一个像素点都加上一个偏置常数,则可以使图片的亮度变大。类似地,将原图片中的像素点乘上一个增益系数,可以调整图片的对比度。但是要注意图片像素点的取值范围是[0, 255],一定不能溢出。
对图片亮度与对比度转换演示
import cv2
import numpy as np
# 方法1:通过addWeighted()函数来实现
"""
addWeighted(img1, alpha, img2, beta, gamma):图像混合加权
img1:第一个输入图片。
alpha:第一个图片的权重。
img1:第二个输入图片,其大小和通道号与src1相同。
beta: 第二个图片的权重。
gamma:标量加到每个和上。
代表将两个图片进行如下计算:
new_img = alpha ⋅ img1 + beta ⋅ img2 + gamma
"""
def convert_img1(img, alpha, beta, gamma):
blank = np.zeros(img.shape, img.dtype) # dtype is uint8
return cv2.addWeighted(img, alpha, blank, beta, gamma)
# 方法2:通过for循环手动实现,与addWeighted()函数内部实现原理一样
def convert_img2(img, alpha, beta):
rows, cols, channel = img.shape
new_img = np.zeros(img.shape, img.dtype)
for i in range(0, rows):
for j in range(0, cols):
for k in range(0, channel):
# np.clip()将数值限制在[0, 255]区间,防止数字溢出
new_img[i, j, k] = np.clip(
alpha * img[i, j, k] + beta, 0, 255
)
return new_img
if __name__ == '__main__':
image = cv2.imread("demo.jpg")
cv2.imwrite('converted_demo_1.jpg', convert_img1(image, 2.2, 0, 50))
cv2.imwrite('converted_demo_2.jpg', convert_img2(image, 2.2, 50))
图像的几何变换是指对图片中的图像像素点的位置进行变换的一种操作,它将一幅图像中的坐标位置映射到新的坐标位置,也就是改变像素点的空间位置,同时也要估算新空间位置上的像素值。经过几何变换的图片, 直观来看就是其图像的形态发生了变化,例如常见的图像缩放、平移、旋转等都属于几何变换。
import cv2
image = cv2.imread('demo.jpg')
print(image.shape)
# (471, 508, 3)
new_image = image[121: 471, 108:508]
print(new_image.shape)
# (350, 400, 3)
cv2.imwrite('cut_demo.jpg', new_image)
修改图像的尺寸也就是修改图像的大小,OpenCV的resize()函数可以实现这样的功能。对图像进行尺寸变换时,必然会丢失或增加一些像素点,这就需要用到插值算法,resize()函数提供了指定插值算法的参数。在缩放时建议使用区域差值cv2.INTER_AREA,可以避免出现波纹;在放大时建议使用三次样条插值cv2.INTER_CUBIC,但是其计算速度相对较慢。也可以使用线性插值cv2.INTER_LINEAR,默认情况下所有改变图像尺寸大小的操作使用的插值法都是线形插值。
图像尺寸变换
import cv2
image = cv2.imread('demo.jpg')
print(image.shape)
# (471, 508, 3)
new_image = cv2.resize(image, (40, 40), interpolation=cv2.INTER_AREA)
# 区域插值
cv2.imwrite('resize_demo.jpg', new_image)
print(new_image.shape)
# (40, 40, 3)
new_image2 = cv2.resize(image, None, fx=0.5, fy=0.6, interpolation=cv2.INTER_AREA)
print(new_image2.shape)
# (283, 254, 3)
cv2.imwrite('resize_demo_2.jpg', new_image2)
噪声会使图片的质量大大下降,因此我们往往会使用一些方法对图像噪声进行消减处理。但是,对训练数据添加适量噪声,可以使训练后的模型更加好,对模型的性能提升有一定帮助。因此,为图像添加噪声可以起到数据增强的作用。
import cv2
import numpy as np
import random
# 添加椒盐噪声
def salt_and_pepper_noise(img, percentage):
rows, cols = img.shape
num = int(percentage * rows * cols)
for i in range(num):
x = random.randint(0, rows - 1)
y = random.randint(0, cols - 1)
if random.randint(0, 1):
img[x, y] = 0 # 黑色噪声
else:
img[x, y] = 255 # 白色噪声
return img
# 添加高斯随机噪声
def gaussian_noise(img, mu, sigma, k):
rows, cols = img.shape
for i in range(rows):
for j in range(cols):
# 生成高斯分布的随机数,与原始数据相加后要取整
value = int(img[i, j] + k * random.gauss(mu=mu, sigma=sigma))
# 限定数据值的上下边界
value = np.clip(a_max=255, a_min=0, a=value)
"""
numpy.clip(a, a_min, a_max, out=None)
a : 输入的数组
a_min: 限定的最小值 也可以是数组 如果为数组时 shape必须和a一样
a_max:限定的最大值 也可以是数组 shape和a一样
out:剪裁后的数组存入的数组
"""
img[i, j] = value
return img
if __name__ == '__main__':
image = cv2.imread('demo.jpg')
# 转换为灰度图像
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imwrite('gray_demo.jpg', image)
# 需要复制一份,不然是对图像的引用,后面的操作会重叠
gray_img2 = gray_img.copy()
# 保存椒盐噪声图像
cv2.imwrite('salt_and_pepper.jpg', salt_and_pepper_noise(gray_img, 0.3))
# 保存高斯噪声图像
cv2.imwrite('gaussian.jpg', gaussian_noise(gray_img2, 0, 1, 32))
OpenCV为我们提供了集中滤波方法,如中值滤波、双边滤波、高斯模糊、二维卷积等。
图像滤波
import cv2
import numpy as np
salt_and_pepper_img = cv2.imread('salt_and_pepper.jpg')
gaussian_img = cv2.imread('gaussian.jpg')
# 二维卷积
kernel = np.ones((5, 5), np.float32) / 25
# 得到一个5*5大小的矩阵作为卷积核,矩阵中的每个值都为0.04
conv_2d_img = cv2.filter2D(salt_and_pepper_img, -1, kernel)
"""
filter2D(src, ddepth, kernel, dst=None, anchor=None, delta=None, borderType=None)
src:输入的图片数据
dst:输出的图片数据
ddepth:目标图像深度,如果没写将生成与原图像深度相同的图像,当ddepth输入值为-1时,目标图像和原图像深度保持一致。
kernel: 卷积核(或者是相关核),一个单通道浮点型矩阵。如果想在图像不同的通道使用不同的kernel,可以先使用split()函数将图像通道事先分开。
anchor: 内核的基准点(anchor),其默认值为(-1,-1)说明位于kernel的中心位置。基准点即kernel中与进行处理的像素点重合的点。
delta: 在储存目标图像前可选的添加到像素的值,默认值为0
borderType: 像素向外逼近的方法,默认值是BORDER_DEFAULT,即对全部边界进行计算。
"""
cv2.imwrite('filter_2d_img.jpg', conv_2d_img)
# 中值滤波
median_blur_img = cv2.medianBlur(salt_and_pepper_img, 5)
# 参数5表示选择附近5*5区域的像素值进行计算
"""
medianBlur(src, ksize, dst=None)
src: 输入图像,图像为1、3、4通道的图像,当模板尺寸为3或5时,图像深度只能为CV_8U、CV_16U、CV_32F中的一个,如而对于较大孔径尺寸的图片,图像深度只能是CV_8U。
dst: 输出图像,尺寸和类型与输入图像一致,可以使用Mat::Clone以原图像为模板来初始化输出图像dst
ksize: 滤波模板的尺寸大小,必须是大于1的奇数,如3、5、7……
"""
cv2.imwrite('median_blur_img.jpg', median_blur_img)
# 高斯模糊
gaussian_blur_img = cv2.GaussianBlur(gaussian_img,(5, 5), 0)
# 标准差参数设置为0是根据窗口大小(5, 5)来自行计算高斯函数标准差
"""
GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)
src: 输入图像,可以是Mat类型,图像深度为CV_8U、CV_16U、CV_16S、CV_32F、CV_64F。
dst: 输出图像,与输入图像有相同的类型和尺寸。
ksize: 高斯内核大小,这个尺寸与前面两个滤波kernel尺寸不同,ksize.width和ksize.height可以不相同但是这两个值必须为正奇数,如果这两个值为0,他们的值将由sigma计算。
sigmaX: 高斯核函数在X方向上的标准偏差
sigmaY: 高斯核函数在Y方向上的标准偏差,如果sigmaY是0,则函数会自动将sigmaY的值设置为与sigmaX相同的值,如果sigmaX和sigmaY都是0,这两个值将由ksize.width和ksize.height计算而来。
borderType=BORDER_DEFAULT: 推断图像外部像素的某种便捷模式,有默认值BORDER_DEFAULT,如果没有特殊需要不用更改
"""
cv2.imwrite('gaussian_blur_img.jpg', gaussian_blur_img)
# 双边滤波
bilateral_filter_img = cv2.bilateralFilter(gaussian_img, 9, 75, 75)
# 9代表邻域直径,连个参数75分别代表值域与空域标准差
"""
bilateralFilter(src, d, sigmaColor, sigmaSpace, dst=None, borderType=None)
d:邻域直径
sigmaColor:颜色标准差
sigmaSpace:空间标准差
"""
cv2.imwrite('bilateral_filter_img.jpg', bilateral_filter_img)
参考文献:
[1]王文庆.Python人脸识别从入门到工程实践[M].机械工业出版社,2019.4.