赞
踩
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()
效果图:
(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((w/2, h/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)翻转
cv.flip(
src, # 旋转图像
flipCode, # 翻转方向,0:绕x轴做上下翻转,1:绕y轴做左右翻转
dst=None
)
// 例如:
image = cv.flip(image, 1)
(5)仿射变换
平行线在变换后仍然平行(就是矩形变成了平行四边形),需要做的就是给定变换前后图像中的三个点的位置,然后计算变换矩阵 M,然后同样送入 cv.warpAffine()。
cv.getAffineTransform(
src, # 原始图像中的三个点的位置,是一个3x2矩阵
dst # 变换之后,这三个点在图像中的位置
)
(6)透视变换
直线在变换之后仍然是直线(不保证平行),需要做的是给定变换前后的四个点的位置,这四个点中的任意三个不共线,计算变换矩阵 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()
)
(1)全局阈值
阈值操作可以用来二值化图像,阈值相当于一个分界线,将像素值划分为两个集合,分别进行不同的操作。全局阈值中的阈值人为设定,对于每一个像素点来说是相同、不变的。
cv.threshold(
src, # 输入图像
dst, # 输出图像
thresh, # 阈值
maxval, # 最大值,就是将满足条件的像素值置为该值
type # 阈值操作类型
)
type 类型有以下几种:
- THRESH_BINARY:大于 thresh 的像素置为 maxval,否则置为0
- THRESH_BINARY_INV:大于 thresh 的像素置为0,否则置为 maxval
- THRESH_TRUNC:大于 thresh 的像素置为 maxval,否则不变
- THRESH_TOZERO:大于 thresh 的像素不变,否则置为0
- THRESH_TOZERO_INV:大于 thresh 的像素置为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)官方网站给出了直方图阈值的计算方法,这里省略该过程
(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 # 边缘填充方式
)
以上的四种滤波方式均属于低通滤波,可以对比看一下它们各自的效果:
(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() # 边缘填充值
)
# 实例
kernel = np.ones((3, 3), np.uint8)
img = cv.erode(img, kernel)
腐蚀对于去掉二值图像中的一些白色噪点很有用,注意是白色噪点
(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可以快速的得到这些卷积核。
对应了前面的低通滤波,这里是高通滤波;低通滤波是去噪,高通滤波则用来检测边缘。
(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),单通道
算法步骤:
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,卷积核大小。
(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)
最终的效果:
更多图像融合相关参考这里
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。