三、OpenCV-python 之 图像处理(Ⅰ)

1、空间转换

OpenCV的图像颜色空间很多,常见的有BGR、HSV等。

cv.cvtColor(input_image, flag)		# 空间转换函数,flag参数多达150多种,常用的cv.COLOR_BGR2GRAY、cv.COLOR_BGR2HSV

(1)HSV空间中,色调范围Hue range【0,179】,饱和度Saturation range【0,255】,颜色明亮程度Value range【0,255】;
(2)HSV空间中更加容易表示颜色,所以通常将BGR图像转换为HSV,然后进行提取。

cv.inRange(		# 该函数返回一个mask,对每一个通道的颜色值检测是否位于lowerb~upperb之间
    src,		# 输入图像
    lowerb,		# 下界,它是一个数组,形式跟输入图像的单个像素点的形式一致
    upperb,		# 上界
    dst
)

cv.inRange()函数通常用于提取特定颜色、特定目标物体,但是需要先将图像转换为HSV格式,HSV对颜色表示更容易,提取更方便。

应用举例:

import cv2 as cv
import numpy as np

cap = cv.VideoCapture(0)
while(1):
    # Take each frame
    _, frame = cap.read()							# 第一个参数给出是否获取帧成功
    
    # Convert BGR to HSV
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    
    # Define range of blue color in HSV
    lower_blue = np.array([110,50,50])				# 注意,这是在HSV空间下的
    upper_blue = np.array([130,255,255])
    
    # Threshold the HSV image to get only blue colors
    mask = cv.inRange(hsv, lower_blue, upper_blue)
    
    # Bitwise-AND mask and original image
    res = cv.bitwise_and(frame,frame, mask= mask)	# 像素与操作,提取mask所在位置的像素
    
    cv.imshow('frame',frame)
    cv.imshow('mask',mask)
    cv.imshow('res',res)
    
    k = cv.waitKey(5) & 0xFF
    if k == 27:
        break
cv.destroyAllWindows()

效果图:

在这里插入图片描述

2、图像几何变换

(1)缩放

cv.resize(src, dst, dsize, fx, fy, interpolation)	# dsize是输出图像的尺寸(w,h),fx-fy是缩放的比例参数,dsize和后面的 fx,fy 二选一即可,最后一个是插值类型,有下面几种

cv.INTER_NEAREST            # 最近邻插值,放大效果很模糊
cv.INTER_LINEAR             # 双线性插值,放大效果比最近邻稍好
cv.INTER_CUBIC              # 双三次插值
cv.INTER_AREA               # 像素区域重采样,放大效果跟最近邻差不多
cv.INTER_LANCZOS4           # Lanczos插值

OpenCV中但凡涉及到图像尺寸的数据,一般都是按照 (w, h, c) 的形式来操作,比如上面缩放操作传入的尺寸;但是通过 image.shape 获取图像尺寸时却是按照 (h, w, c) 返回的,因为 image 是 numpy 数据,shape 是 numpy 的方法。另外,OpenCV的缩放操作采用插值的数学计算方式,这和后面将要看到的采样不太一样,在高斯金字塔中上采样通过插入0行、0列然后卷积的方式,下采样是直接舍弃一半的行、列。
(2)平移

cv.warpAffine(
    src, 
    dst, 
    M,				# 平移矩阵,其size是2x3
    dsize,			# 输出图像的尺寸,它的形式是(width,height)
    flags,			# 插值方式
    borderMode,		# 边界的模式,有可能是直接置为0,有可能是置为边缘像素值
    borderValue		# 边界的填充值
)

这里用到的 cv.warpAffine() 函数是一个仿射变换函数,核心在于变换矩阵 M。

仿射变换更多的是改变像素的位置,而不改变像素值的大小,所以矩阵 M 是作用在像素位置上的,这跟卷积核的操作不一样,卷积核的目的是改变图像的色彩呈现,所以它是和图像中的每一个像素做计算。

(3)旋转
获取旋转参数,得到的是一个参数矩阵M,然后直接送入上面的 cv.warpAffine()。要注意的是,如果旋转的前后图像尺寸不一致,可能会发生意料之外的剪裁。

cv.getRotationMatrix2D(
    center,		# 旋转中心,一般是图像中心
    angle,		# 旋转角度
    scale		# 缩放比例
)

// 例如:
matRotate = cv.getRotationMatrix2D((h/2, w/2), -180, 1.0)	# 顺时针旋转180度
mat_rotate[0, 2] += (w_new - w)/2							# 适应旋转后的尺寸,不剪裁图片
mat_rotate[1, 2] += (h_new - h)/2

frame = cv.warpAffine(frame, mat_rotate, (w_new, h_new))	# 根据旋转矩阵参数来实现旋转变换

(4)仿射变换
平行线在变换后仍然平行(就是矩形变成了平行四边形),需要做的就是给定变换前后图像中的三个点的位置,然后计算变换矩阵 M,然后同样送入 cv.warpAffine()。

cv.getAffineTransform(
    src,		# 原始图像中的三个点的位置,是一个3x2矩阵
    dst			# 变换之后,这三个点在图像中的位置
)

(5)透视变换
直线在变换之后仍然是直线(不保证平行),需要做的是给定变换前后的四个点的位置,这四个点中的任意三个不共线,计算变换矩阵 M,然后送入 cv.warpPerspective()。

M = cv.getPerspectiveTransform(src, dst, solveMethod = DECOMP_LU)
cv.warpPerspective(
    src, 
    dst, 
    M,			# 注意:这个变换矩阵和仿射变换的略微不同,它是一个3x3的
    dsize,
    flags = INTER_LINEAR,
    borderMode = BORDER_CONSTANT,
    borderValue = Scalar()
)

3、阈值

(1)全局阈值
阈值操作可以用来二值化图像,阈值相当于一个分界线,将像素值划分为两个集合,分别进行不同的操作。全局阈值中的阈值人为设定,对于每一个像素点来说是相同、不变的。

cv.threshold(
    src,		# 输入图像
    dst,		# 输出图像
    thresh,		# 阈值
    maxval,		# 最大值,就是将满足条件的像素值置为该值
    type		# 阈值操作类型,小于置0大于置maxval,大于置0小于置maxval,小于不变大于置maxval,小于不变大于置0,小于置0大于不变
)

(2)自适应阈值
这种阈值操作一般用在图像光照不均匀的情况下,像素的阈值由其附近的领域像素值决定。

cv::adaptiveThreshold(
    src,
    dst,
    maxValue,        // 最大值
    adaptiveMethod,  // 自适应方式有两种,cv.ADAPTIVE_THRESH_MEAN_C领域面积减去常数C后的均值,cv.ADAPTIVE_THRESH_GAUSSIAN_C邻域值减去常数C的高斯加权和
    thresholdType,   // 阈值操作类型,这跟上面的全局阈值是一致的
    blockSize,       // 邻域大小
    C                // 常数值
)

(3)Otsu’s:直方图阈值
它的阈值是通过直方图来确定的,适用于双模态图像;因为双模态图像的直方图一般只有两个波峰,阈值就取波峰的均值。

cv.threshold(
    src,
    0,
    255,
    cv.THRESH_BINARY+cv.THRESH_OTSU		# 使用方式是在阈值操作类型的后面加上一个cv.THRESH_OTSU参数
)

(1)直方图阈值对于图像去噪很好用,可以先用高斯滤波去掉一部分噪点,然后用直方图阈值,得到的图像基本没有噪点,但是仅限于双模态图像
(2)官方网站给出了直方图阈值的计算方法,这里省略该过程

4、图像滤波

(1)基本概念

  • 高频:图像中颜色变化剧烈的部分,它展示了图像中的细节,例如边缘、噪点等位置
  • 低频:图像中颜色变化不大的部分,多是一些平坦光滑的大部分区域
  • 高通滤波:过滤掉低频部分,只保留那些变化剧烈的边缘、噪点(波峰波谷不见了,但是抖动仍然在),常用于图像的边缘检测
  • 低通滤波:过滤掉高频部分,保留的是平坦的大部分区域(波峰波谷还在,过滤掉细节,图像的波线更加平滑),效果就是去掉噪点、使图像变得模糊(因为边缘特征的高频变成了低频)

(图像与滤波可以参考这里

(2)均值滤波
均值滤波的卷积核是一个 3x3 矩阵,每一个元素的值都相同,取加权求和之后的值作为滤波后的像素值;根据卷积核的元素值不同,低通滤波有两种。

cv.blur(
    src,
    dst,
    ksize,							# 卷积核的尺寸,卷积核的每一个元素值都是相同的,且和为1,该参数必须是一个tuple
    anchor = Point(-1,-1),			# 锚点位置,默认为(-1,-1)就是指卷积核的中心位置
    borderType = BORDER_DEFAULT		# 边缘的填补方式
)

cv.boxFilter(
    src,
    dst,
    ddepth,							# 输出图像的深度,默认为-1,表示与输入图像一致
    ksize,							# 卷积核的尺寸,卷积核的元素值受到该尺寸和下面的normalize参数的影响,该参数必须是一个tuple
    anchor = Point(-1,-1),			# 锚点
    normalize = true,				# 是否进行归一化,如果是那就跟上面的cv.blur没有区别(取均值),如果不是那就代表是求和(也就是卷积核每一个元素的值是1)
    borderType = BORDER_DEFAULT		# 边缘的填补方式
)

(3)高斯滤波
在去除高斯噪点(例如毛边)的时候很有用,它的计算方式跟均值滤波没有区别,不同之处在于卷积核;低通滤波会使得图像整体变模糊,高斯滤波也不例外。

cv.GaussianBlur(
    src,
    dst,
    ksize,						# 卷积核大小,width、height可以不同,但是必须是正奇数;卷积核的元素值服从高斯分布,中心元素替换为邻域内的高斯加权和,该参数必须是一个tuple
    sigmaX,						# 卷积核在 X 方向的标准差
    sigmaY = 0,					# 卷积核在 Y 方向的标准差,如果没设置那就跟 sigmaX 相同;如果sigmaX、sigmaY都是0,那就从卷积核来计算
    borderType = BORDER_DEFAULT	# 边缘填充方式
)

(4)中值滤波
上面的均值滤波、高斯滤波都是通过计算(可能是区域中存在的,也可能不存在)产生一个值替代中间元素,而中值则是直接使用区域内的中间值来完成替代。因为椒盐噪声的像素值要么是0要么是255,不可能是区域内的中值,所以中值滤波对于椒盐去噪很有效。

椒盐噪声:椒–黑,盐–白,所以它指的是黑白色噪点,通常出现在灰度图中。
高斯噪声:服从高斯分布的一类噪声,通常是因为不良照明和高温引起的传感器噪声,通常在RGB图像中显现比较明显。

cv.medianBlur(
    src,
    dst,
    ksize			# 整形参数,表示卷积核的大小,它只是起到一个指定区域的作用,必须是正奇数(1,3,5,...)
)

(5)双边滤波
它能够在有效去除噪声的同时保留边缘信息,但是它的速度比较慢;它使用的是双高斯滤波,空间高斯函数确保只考虑附近像素的模糊,而强度差高斯函数(这是一个像素差的函数)确保只考虑与中心像素强度相似的像素的模糊,所以边缘就会被保留。

cv::bilateralFilter(
    src,
    dst,
    d,								# 整形参数,卷积核大小,d>5就会很慢,一般令 d=5 来保证实时
    sigmaColor,						# 颜色空间值,如果<10,该滤波器基本不会有效果;如果>150,该滤波器的滤波将会很强,将图像变成卡通风格;值越大表示与中心像素差距越大的像素将被考虑进来
    sigmaSpace,						# 座标空间值,它通常与颜色空间值保持一致,特性也是一样的;值越大表示越远的像素值将被考虑进来
    borderType = BORDER_DEFAULT		# 边缘填充方式
)

以上的四种滤波方式均属于低通滤波,可以对比看一下它们各自的效果:
在这里插入图片描述

5、形态学操作

(1)腐蚀Erosion
形态学操作针对的是二值图像,而且是对白色区域(255)而言的,腐蚀就是把白色区域变小。具体来说,如果卷积核区域存在0,那么中心就被置为0,除非卷积核区域全为1

cv::erode(
    src,
    dst,
    kernel,							# 卷积核的大小,一般是一个3x3矩阵,比如使用 np.ones((3,3),np.uint8)
    anchor = Point(-1,-1),			# 中心位置
    iterations = 1,					# 腐蚀的操作次数,默认为1次
    borderType = BORDER_CONSTANT,	# 边缘填充类型
    borderValue = morphologyDefaultBorderValue()	# 边缘填充值
)

腐蚀对于去掉二值图像中的一些白色噪点很有用,注意是白色噪点

(2)膨胀Dilation
与腐蚀相反,如果卷积核区域包含1那就把中心区域置为1,除非卷积核区域全为0,这会把白色区域变大,它可以方便的去掉二值图像中的黑色噪点

cv.dilate(src, dst, kernel, anchor, iterations, borderType, borderValue)		# 参数跟腐蚀一模一样

(3)开操作
先腐蚀后膨胀,可以去掉白色噪点且保持剩余图像尺寸不变,白色的拐角区域、细瘦的白色区域也会消失

cv.morphologyEx(
    src,
    dst,
    op,						# 形态学操作类型,cv.MORPH_CLOSE
    kernel,
    anchor = Point(-1,-1),
    iterations = 1,			# 开操作次数
    borderType = BORDER_CONSTANT,
    borderValue = morphologyDefaultBorderValue()
)

(4)闭操作
先膨胀后腐蚀,可以去掉黑色噪点且保持剩余图像尺寸不变,黑色拐角区域、细瘦黑色区域也会消失

cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)

(5)形态学梯度
先进行膨胀、腐蚀操作,然后膨胀结果减去腐蚀结果。

cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)

(6)顶帽操作
先进行开运算,然后原图像减去开运算结果。开运算会让一些细小的白色区域(比如毛边轮廓、噪点)被过滤掉,减法操作就会让这些区域显现出来。

cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)

(7)黑帽操作
先进行闭运算操作,然后将闭运算结果减去原图像。

cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)

(1)这些形态学操作虽然有各自的特性与适用范围,最终选择哪一个还是要是具体情况、最终效果而定;
(2)卷积核的形状并不一定非要是矩形的,它也可以是椭圆、十字形的,cv.getStructuringElement可以快速的得到这些卷积核。

6、图像梯度

对应了前面的低通滤波,这里是高通滤波;低通滤波是去噪,高通滤波则用来检测边缘。
(1)sobel
它是高斯平滑与微分处理的结合体,因此对噪声具有更加强的抵抗力;它有垂直、水平两个方向的边缘检测。

在这里插入图片描述

cv.Sobel(
    src,
    dst,
    ddepth,			# 输出图像的深度
    dx,				# 对 X 方向的导数(系数),1 表示对 X 方向求导,0 表示不求导;垂直方向
    dy,				# 对 Y 方向的导数(系数),1 表示对 Y 方向求导,0 表示不求导;水平方向
    ksize = 3,		# sobel 核的大小,一般这个是固定的,具体的值见下图
    scale = 1,		# 计算导数的比例因子???默认情况下不进行缩放
    delta = 0,		# 在将计算结果存储到dst之前添加的delta值,一般不用
    borderType		# 边界填充类型
)

(1)cv.Scharr() 是同 sobel 相类似的边缘检测算子,区别是卷积核的值不同,scharr 是[-3,-10,-3],它的精度会比 sobel 更好;它解决了 sobel 算子内核由于求导近似产生的误差;
(2)sobel 算子也被称为一阶微分算子,它只进行一阶求导,这与下面的拉普拉斯算子是有区别的;
(3)算法步骤:高斯平滑(去噪),转灰度,求梯度(微分),合并近似梯度。

(2)Laplacian
拉普拉斯求的是二阶导数,这使得拉普拉斯算子具有各向同性、旋转不变性。

cv.Laplacian(
    src,
    dst,
    ddepth,
    ksize = 1,	# 拉普拉斯的核的大小和之前的卷积核大小的意义有些不同,因为它是二阶的,所以当ksize=1时实际上卷积核的尺寸是3
    scale = 1,	# 缩放比例
    delta = 0,	# 在存储结果之前添加的delta值
    borderType	# 边缘填充方式
)

(1)拉普拉斯算子对图像中的孤立线、孤立点、线端点特别敏感,它能使得原来的亮点更亮
(2)同sobel算子一样,拉普拉斯算子也会增强图像中的噪点,所以进行该高通滤波之前往往需要先用低通滤波去掉噪点
(3)拉普拉斯算子求边缘效果比sobel更强

(3)关于目标图像的深度
一般我们设置参数ddepth=-1,让目标图像深度与输入图像一致,但是这里有个问题,如果目标图像的类型是CV_8U或者np.uint8,在求梯度的时候 OpenCV 按照从左到右、从上到下的顺序进行,这样如果是 white-black 边界的话梯度是正值,可以保留下来,但是如果是 black-white 边界的话梯度将是负值,当我们将数据转换为CV_8U或者uint8的时候,这些负值就变成了0,这时边界就消失了。
怎么解决呢?可以将ddepth设置为CV_16S、CV_64F等,反正不要用CV_8U就行。

CV_8U/CV_16S/CV_64F的区别?
在OpenCV中,ddepth类型的结构定义为:CV_<bit_depth>(S|U|F)C<number_of_channels>
----bit_depth:表示一个像素点所占用的空间大小(单位是bite)
----U表示无符号整型,S表示有符号整型,F表示单精度浮点型
----number_of_channels表示通道数
例如:CV_8UC1就表示一个像素点占用8bite,像素值类型是无符号整型(也就是0~255),单通道

7、Canny边缘检测

算法步骤:

  1. 去噪:边缘和噪点都属于高频特征,噪点会影响边缘检测的准确性,所以需要先用高斯滤波进行去噪
  2. 梯度计算:利用sobel算子检测水平/垂直方向上的梯度G_x/G_y(一阶微分),然后计算边缘上的梯度
  3. NMS:非极大值抑制,检查每一个像素在邻域内的梯度是否为最大值,如果不是就将梯度置为0
  4. 阈值确定:这里需要两个阈值maxval、minval,梯度值在maxval之上的被确定为边界,在minval之下的被丢弃,位于二者之间的如果和边界有连接就被认为是边界,否则也将被丢弃
cv::Canny(
    image,                # 输入图像
    edges,                # 输出边缘图像
    threshold1,           # 阈值1,注意并不一定是maxval,该函数会将两个阈值进行比较,之后决定maxval、minval
    threshold2,           # 阈值2,阈值范围越大边缘细节越多
    apertureSize = 3,     # sobel算子的卷积核大小
    L2gradient = false    # 像素梯度的计算方法:true表示采用平方和开方的方式,false表示采用绝对值相加的方式
)

我们将三种边缘检测算子做一个对比:

img_sobel = cv.Sobel(img, cv.CV_8U, 1, 1, ksize=3)
img_lapla = cv.Laplacian(img, cv.CV_8U, ksize=1)
img_canny = cv.Canny(img, 100, 200, 3)

要注意的是有几个参数会影响输出的结果:输出图像的深度ddepth,卷积核大小。

在这里插入图片描述

8、图像金字塔

(1)高斯金字塔
下采样:该操作会丢失一些信息,即便再通过上采样还原尺寸也无法恢复;实现方式是先用卷积核进行加权求和,然后舍弃一半的行和列,这样尺寸就减半,达到模糊效果。

cv::pyrDown(                        
    src,                            # 原始图像
    dst,                            # 输出图像
    dstsize = Size(),               # 输出图像的尺寸,一般是原始图像宽高的1/2
    borderType = BORDER_DEFAULT     # 边界填充方式
)

上采样:它的卷积核跟下采样是相同的,区别在于采样的方式;实现方式是先往原始图像中添加0元素行、列(上采样),图像尺寸增加一倍,然后用4倍于下采样卷积核的卷积核进行加权求和。

cv::pyrUp(                          
    src,
    dst,
    dstsize = Size(),
    borderType = BORDER_DEFAULT
)

采样卷积核:

在这里插入图片描述

(2)Laplacian金字塔
前面看到laplacian算子是一种高通滤波,用来获取边缘信息;这里的laplacian金字塔也是针对边缘图像的,它是从高斯金字塔中获得的,常用来做图像压缩(没有专门的函数)
实现方式:先对原图像使用下采样得到1/2原图像,然后对下采样图像进行上采样,再将原图像和上采样图像做减法(得到图像的细节信息,例如边缘、纹理等)。

在这里插入图片描述

laplacian金字塔呈现的是黑白图,它包含的是边缘轮廓、纹理等细节特征,很有可能已经看不出来原始图像是哪一个

(3)图像融合
直接拼接的方式会在边界留下痕迹,我们考虑用图像金字塔的方式实现。
先下采样得到高斯金字塔,这会丢失一些细节信息,比如纹理;然后利用高斯金字塔上采样、做减法得到拉普拉斯金字塔,这就可以得到图像的细节信息;然后在拉普拉斯金字塔层面上做拼凑(它的最高层是保留了大部分图像信息的),最后做图像上采样、求和,得到拼凑的结果。

import cv2 as cv
import numpy as np,sys
A = cv.imread('apple.jpg')
B = cv.imread('orange.jpg')
# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpA.append(G)
# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpB.append(G)
# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in xrange(5,0,-1):
    GE = cv.pyrUp(gpA[i])
    L = cv.subtract(gpA[i-1],GE)
    lpA.append(L)
# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in xrange(5,0,-1):
    GE = cv.pyrUp(gpB[i])
    L = cv.subtract(gpB[i-1],GE)
    lpB.append(L)
# Now add left and right halves of images in each level
LS = []
for la,lb in zip(lpA,lpB):
    rows,cols,dpt = la.shape
    ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
    LS.append(ls)
# now reconstruct
ls_ = LS[0]
for i in xrange(1,6):
    ls_ = cv.pyrUp(ls_)
    ls_ = cv.add(ls_, LS[i])
# image with direct connecting each half
real = np.hstack((A[:,:cols/2],B[:,cols/2:]))
cv.imwrite('Pyramid_blending2.jpg',ls_)
cv.imwrite('Direct_blending.jpg',real)

最终的效果:

在这里插入图片描述

更多图像融合相关参考这里

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章