小白的OpenCV学习
文章目录
一.学习环境
-
Python 3.6 + OpenCV-python 3.4.1.15 + OpenCV-contrib-python 3.4.1.15 + numpy + matplotlib + Jupyter Notebook
-
这里要多说一句,在OpenCV 3.4.2以后的版本有很多模型由于受到专利限制,无法使用,切记!
二.图像的基本操作
图像读取及显示
-
使用cv2.imread(path)读取图像
-
注意:path参数必须是全英文的,不能包含中文字符
-
image=cv2.imread(path),返回值image是一个ndarray类型的数组,并且是以BGR的通道顺序读进来的图像(如果不好记,麻烦想一下BGM…)
- 读进来的图像是通道放在末尾的形式(e.g: image.shape=(xxx,xxx,3),其中3表示BGR通道)
- image=cv2.imread(path,cv2.IMREAD_GRAYSCALE),后面的参数表示以灰度图的方式读取图像,此时image.shape=(xxx,xxx)
-
cv2.imshow(‘name’,image)用来显示图像,两个参数分别是图像的名称,和图像本身
需要用cv2.waitKey(0) + cv2.destroyAllWindows()来将显示的图像hold住,不然图像闪一下就过了,通常情况下,会定义一个小函数来帮助显示图像
import cv2 def cv_show(name,image): cv2.imshow(name,image) cv2.waitKey() cv2.destroyAllWindows() cv_show(name,image)
-
cv2.imwrite(path,image) 用来保存图像
读取视频
- cv2.VideoCapture可以捕捉摄像头,用不同的数字来控制不同的设备,如果是视频文件,直接指定文件路径即可
- vc=cv2.VideoCapture(path)
- 读取视频的原理就是一帧一帧的读取每张图片
vc=cv2.VideoCapture(path)
# 这里需要检查是否正常打开视频
if vc.isOpen():
open,frame=vc.read()
else:
open=False
while open:
ret,frame=vc.read()
# 表示视频读完了
if frame is None:
break
if ret==True:
# 将图像转换为灰度图
gray=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
cv2.imshow('result',gray)
# 这里waitKey的参数表示视频的播放时间,注意!不是视频本身的时间,而是你指定多久,就播放多久,以秒为单位,oxff==27就是键盘上的退出键(esc)
if cv2.waitKey(10) & 0xff==27:
break
vc.release()
cv2.destroyAllWindows()
截取部分图像数据(ROI region of interest)
image=cv2.imread(path)
-
图像大小提取
cat=image[0:300,0:200]
cv_show(‘cat’,cat)
由于图像的本质是三维矩阵(彩色图像),分别是[h,w,c](高,宽,通道),那么就可以像数组一样,采用切片的方式获取任意想得到的地方
-
颜色通道提取
-
b,g,r=cv2.split(image)
提取到的b,g,r都是一个二维矩阵,大小和图像的shape一样
-
image=cv2.merge((b,g,r))
重新将b,g,r合并,需要注意的是,merge参数是一个元组,合并后的图像就又是三维数组了
-
cur_image=image.copy()
cur_image[:,:,0]=0 根据b,g,r的排序,将b通道设置为0
cur_image[:,:,1]=0 同理,将g通道设置为0,这样就只留下了r通道
-
边界填充
# 指定上,下,左,右分别填充的大小
top_size,bottom_size,left_size,right_size=(50,50,50,50)
# 复制法:直接复制最边缘像素
replicate=cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,borderType=cv2.BORDER_REPLICATE)
# 反射法:对感兴趣的图像中的像素在两边进行复制
reflect=cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_REFLECT)
# 反射法:也就是以最边缘像素为轴,对称
reflect101=cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_REFLECT_101)
# 外包装法:
wrap=cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_WRAP)
# 常量法:用常数值进行填充
constant=cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_CONSTANT,value=0)
数值计算
image_cat=cv2.imread(path1)
image_dog=cv2.imread(path2)
image_cat[:5,:,0]
output:
array([[142, 146, 151, ..., 156, 155, 154],
[107, 112, 117, ..., 155, 154, 153],
[108, 112, 118, ..., 154, 153, 152],
[139, 143, 148, ..., 156, 155, 154],
[153, 158, 163, ..., 160, 159, 158]], dtype=uint8)
image_cat2=image_cat+10
image_cat2[:5,:,0]
output:
array([[152, 156, 161, ..., 166, 165, 164],
[117, 122, 127, ..., 165, 164, 163],
[118, 122, 128, ..., 164, 163, 162],
[149, 153, 158, ..., 166, 165, 164],
[163, 168, 173, ..., 170, 169, 168]], dtype=uint8)
(image_cat+image_cat2)[:5,:,0]
output:
array([[ 38, 46, 56, ..., 66, 64, 62],
[224, 234, 244, ..., 64, 62, 60],
[226, 234, 246, ..., 62, 60, 58],
[ 32, 40, 50, ..., 66, 64, 62],
[ 60, 70, 80, ..., 74, 72, 70]], dtype=uint8)
# 由于142+152=294(第一行第一列的数字),但是这个数字超过了256,所以用294%256=38
-------------------------------------------------------------------------
cv2.add(image_cat,image_cat2)[:5,:,0]
output:
array([[255, 255, 255, ..., 255, 255, 255],
[224, 234, 244, ..., 255, 255, 255],
[226, 234, 246, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255],
[255, 255, 255, ..., 255, 255, 255]], dtype=uint8)
# 在越界的情况下,就直接取最大值255,没越界就取自身
图像融合
- 图像融合的前提是两张图片大小相同
image_dog=cv2.resize(image_dog,填入猫图像的shape)
# 猫占40%,狗占60%
res=cv2.addWeighted(image_cat,0.4,image_dog,0.6,0)
cv_show('res',res)
-
R
=
α
x
1
+
β
x
2
+
b
R=\alpha{x_1}+\beta{x_2}+b
R=αx1+βx2+b,
α
,
β
,
b
\alpha,\beta,b
α,β,b 分别代表两张图像的占比和提亮系数
三.图像阈值 & 图像平滑
图像阈值
ret,dst=cv2.threshold(src,thresh,maxval,type)
-
src:输入图,只能输入单通道图像,通常是灰度图
-
dst:输出图
-
thresh:阈值
-
maxval:当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值
-
type:二值化操作的类型,包含以下5种类型:cv2.THRESH_BINARY;cv2.THRESH_BINARY_INV,cv2.THRESH_TRUNC,cv2.THRESH_TOZERO,
cv2.THRESH_TOZERO_INV
- cv2.THRESH_BINARY:超过阈值部分取maxval(最大值),否则取0
- cv2.THRESH_BINARY_INV:THRESH_BINARY的反转
- cv2.THRESH_TRUNC:大于阈值部分设为阈值,否则不变
- cv2.THRESH_TOZERO:大于阈值部分设为阈值,否则为0
- cv2.THRESH_TOZERO_INV:THRESH_TOZERO的反转
img_original=cv2.imread(path)
img=cv2.imread(path,cv2.IMREAD_GRAYSCALE)
ret,thresh1=cv2.threshold(img,127,255,cv2.THRESH_BINARY)
ret,thresh2=cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3=cv2.threshold(img,127,255,cv2.THRESH_TRUNC)
ret,thresh4=cv2.threshold(img,127,255,cv2.THRESH_TOZERO)
ret,thresh5=cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV)
titles=['original','binary','binary_inv','trunc','to_zero','to_zero_inv']
images=[img_original,thresh1,thresh2,thresh3,thresh4,thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
图像平滑
- 分别展示均值滤波,方框滤波,高斯滤波,中间值滤波
- 均值滤波:平均卷积操作
- 方框滤波:和均值滤波基本相同,但是可以选择归一化
- 高斯滤波:高斯模糊的卷积核里的数值满足高斯分布,相当于更重视中间值
- 中间值滤波:用中间值替代(注意:使用中间值(位置在中间)而不是均值)
- 图像顺序(原图,均值,方框1,方框2,高斯,中间值)
四.形态学操作(腐蚀操作,膨胀操作,开/闭运算,梯度运算,礼帽/黑帽)
腐蚀操作
- 腐蚀操作一般都在二值图像上进行
image=cv2.imread(path)
kernel=np.ones((3,3),np.uint8)
# erode参数:
# image:需要操作的图像
# kernel:卷积核大小
# iterations:迭代次数,次数越多,腐蚀越狠
erosion=cv2.erode(image,kernel,iterations=1)
膨胀操作
- 膨胀操作和腐蚀操作互为逆操作
image=cv2.imread(path)
kernel=np.ones((3,3),uint8)
# 先腐蚀
erosion=cv2.erode(image,kernel,iterations=1)
# 再膨胀
dilation=cv2.dilate(erosion,kernel,iterations=1)
- 可以看到,图片变“胖了”
开运算与闭运算
- 将腐蚀与膨胀操作结合起来
- 开(下图左):先腐蚀,后膨胀
- 闭(下图右):先膨胀,后腐蚀
image=cv2.imread(path)
kernel=np.ones((3,3),uint8)
opening=cv2.morphologyEx(image,cv2.MORPH_OPEN,kernel)
--------------------------------------------------------------
image=cv2.imread(path)
closing=cv2.morphologyEx(image,cv2.MORPH_CLOSE,kernel)
- 可以看到,经过闭运算后,“毛刺”仍然存在!
梯度运算
- 梯度=膨胀-腐蚀
- 可以猜到,图片经过膨胀再减去腐蚀后留下的应该是图片的轮廓,下图左是图片原图,右是经过梯度运算的结果
image=cv2.imread(path)
kernel=np.ones((3,3),uint8)
gradient=cv2.morphologyEx(image,cv2.MORPH_GRADIENT,kernel)
礼帽与黑帽
-
礼帽:原始输入-开运算
-
黑帽:闭运算-原始输入
-
可以猜到,如果原始图像中有一些“毛刺”,那么礼帽运算会将其留下来,
而经过黑帽以后,会留下图像的大致轮廓
-
下图中,左是礼帽运算,右是黑帽运算
image=cv2.imread(path)
kernel=np.ones((3,3),uint8)
tophat=cv2.morphologyEx(image,cv2.MORPH_TOPHAT,kernel)
--------------------------------------------------------------------
balckhat=cv2.morphologyEx(image,cv2.MORPH_BLACKHAT,kernel)
五.图像梯度处理
Sobel算子
-
G x = [ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] ∗ A G_x=\left[ \begin{matrix} -1 & 0 & +1\\ -2 & 0 & +2\\ -1 & 0 & +1 \end{matrix} \right] *A Gx=⎣⎡−1−2−1000+1+2+1⎦⎤∗A G y = [ − 1 − 2 − 1 0 0 0 + 1 + 2 + 1 ] ∗ A G_y=\left[ \begin{matrix} -1 & -2 & -1\\ 0 & 0 & 0\\ +1 & +2 & +1 \end{matrix} \right] *A Gy=⎣⎡−10+1−20+2−10+1⎦⎤∗A
-
dst=cv2.Sobel(src,ddepth,dx,dy,ksize)
- ddepth:图像的深度,通常情况下为-1
- dx,dy:分别表示水平和竖直方向
- ksize是Sobel算子的大小
-
原图像
- 对x方向而言,整个Sobel计算是从左到右的,这是一个二值图像,从左向右,从白到黑就是正数,从黑到白就是负数,所有的负数都会被截断成0
image=cv2.imread(path)
# cv2.CV_64F (一般是默认参数)
# 1,0:表示x方向
sobelx=cv2.Sobel(image,cv2.CV_64F,1,0,ksize=3)
- 可以看到,图像只有左半边的轮廓,因为右半边为负数,被自动截断成了0,所以需要给结果取绝对值!!!
sobelx=cv2.Sobel(image,cv2.CV_64F,1,0,ksize=3)
sobelx=cv2.convertScaleAbs(sobelx)
- 一般求图片的轮廓是分别计算x,y方向的轮廓,再将他们组合起来,而不是一起求,因为一起求会有重影!!!
sobely=cv2.Sobel(image,cv2.CV_64F,0,1,ksize=3)
sobely=cv2.convertScaleAbs(sobely)
sobelxy=cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
- 下图左是sobely的图像,右是sobelxy的图像
- 再来看一下美丽的lena小姐姐经过梯度处理后的样子(左边是原图,右边是梯度处理后的图),可以看到,图像的轮廓被清晰的提取出来了
Scharr算子和laplacian算子
- Scharr算子
G x = [ − 3 0 3 − 10 0 10 − 3 0 + 3 ] ∗ A G_x=\left[ \begin{matrix} -3 & 0 & 3\\ -10 & 0 & 10\\ -3 & 0 & +3 \end{matrix} \right] *A Gx=⎣⎡−3−10−3000310+3⎦⎤∗A G y = [ − 3 − 10 − 3 0 0 0 − 3 − 10 − 3 ] ∗ A G_y=\left[ \begin{matrix} -3 & -10 & -3\\ 0 & 0 & 0\\ -3 & -10 & -3 \end{matrix} \right] *A Gy=⎣⎡−30−3−100−10−30−3⎦⎤∗A
- laplacian算子
G = [ 0 1 0 1 − 4 1 0 1 0 ] G=\left[ \begin{matrix} 0 & 1 & 0\\ 1 & -4 & 1\\ 0 & 1 & 0 \end{matrix} \right] G=⎣⎡0101−41010⎦⎤
- 来比较一下不同算子的差异(从左到右依次是sobel,scharr,laplacian)
img=cv2.imread(path,cv2.IMREAD_GRAYSCALE)
sobelx=cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx=cv2.convertScaleAbs(sobelx)
sobely=cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobely=cv2.convertScaleAbs(sobely)
sobelxy=cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
scharrx=cv2.Scharr(img,cv2.CV_64F,1,0)
scharrx=cv2.convertScaleAbs(scharrx)
scharry=cv2.Scharr(img,cv2.CV_64F,0,1)
scharry=cv2.convertScaleAbs(scharry)
scharrxy=cv2.addWeighted(scharrx,0.5,scharry,0.5,0)
laplacian=cv2.Laplacian(img,cv2.CV_64F)
laplacian=cv2.convertScaleAbs(laplacian)
六.边缘检测算法
canny边缘检测
- 1)使用高斯滤波器,以平滑图像,消除噪声
- 2)计算图像中每个像素点的梯度和方向
- 3)应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应
- 4)应用双阈值(Double-Threshold)检测来确定真实和潜在的边缘
- 5)通过抑制孤立的弱边缘最终完成边缘检测
高斯滤波器
H = [ 0.0924 0.1192 0.0924 0.1192 0.1538 0.1192 0.0924 0.1192 0.0924 ] H=\left[ \begin{matrix} 0.0924 & 0.1192 & 0.0924\\ 0.1192 & 0.1538 & 0.1192\\ 0.0924 & 0.1192 & 0.0924 \end{matrix} \right] H=⎣⎡0.09240.11920.09240.11920.15380.11920.09240.11920.0924⎦⎤ <–这里进行了归一化处理
e = H ∗ A = [ h 1 1 h 1 2 h 1 2 h 2 1 h 2 2 h 2 3 h 3 1 h 3 2 h 3 3 ] ∗ [ a b c d e f g h i ] = s u m { [ a ∗ h 1 1 b ∗ h 1 2 c ∗ h 1 3 d ∗ h 2 1 e ∗ h 2 2 f ∗ h 2 3 g ∗ h 3 1 h ∗ h 3 2 i ∗ h 3 3 ] } e=H*A=\left[ \begin{matrix} h_11 & h_12 & h_12\\ h_21 & h_22 & h_23\\ h_31 & h_32 & h_33 \end{matrix} \right]*\left[ \begin{matrix} a & b & c\\ d & e & f\\ g & h & i \end{matrix} \right]= sum\left\{\left[ \begin{matrix} a*h_11 & b*h_12 & c*h_13\\ d*h_21 & e*h_22 & f*h_23\\ g*h_31 & h*h_32 & i*h_33 \end{matrix} \right]\right\} e=H∗A=⎣⎡h11h21h31h12h22h32h12h23h33⎦⎤∗⎣⎡adgbehcfi⎦⎤=sum⎩⎨⎧⎣⎡a∗h11d∗h21g∗h31b∗h12e∗h22h∗h32c∗h13f∗h23i∗h33⎦⎤⎭⎬⎫
梯度和方向
G = G x 2 + G y 2 G=\sqrt{G_x^2+G_y^2} G=Gx2+Gy2 θ = a r c t a n ( G y / G x ) \theta=arctan({G_y}/{G_x}) θ=arctan(Gy/Gx)
S x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] S_x=\left[\begin{matrix} -1 & 0 & 1\\ -2 & 0 & 2\\ -1 & 0 & 1 \end{matrix}\right] Sx=⎣⎡−1−2−1000121⎦⎤ S y = [ 1 2 1 0 0 0 − 1 − 2 − 1 ] S_y=\left[\begin{matrix} 1 & 2 & 1\\ 0 & 0 & 0\\ -1 & -2 & -1 \end{matrix}\right] Sy=⎣⎡10−120−210−1⎦⎤
G x = S x ∗ A = [ − 1 0 1 − 2 0 2 − 1 0 1 ] ∗ [ a b c d e f g h i ] = s u m { [ − a 0 c − 2 d 0 2 f − g 0 i ] } G_x=S_x*A=\left[\begin{matrix} -1 & 0 & 1\\ -2 & 0 & 2\\ -1 & 0 & 1 \end{matrix}\right] *\left[\begin{matrix} a & b & c\\ d & e & f\\ g & h & i \end{matrix}\right]=sum\left\{\left[\begin{matrix} -a & 0 & c\\ -2d & 0 & 2f\\ -g & 0 & i \end{matrix}\right]\right\} Gx=Sx∗A=⎣⎡−1−2−1000121⎦⎤∗⎣⎡adgbehcfi⎦⎤=sum⎩⎨⎧⎣⎡−a−2d−g000c2fi⎦⎤⎭⎬⎫
双阈值检测
- 梯度值>maxVal:则处理为外便捷
- minVal<梯度值<maxVal:连有边界则保留,否则舍弃
- 梯度值<minVal:舍弃
image=cv2.imread(path)
v1=cv2.Canny(image,120,250)
v2=cv2.Canny(image,50,100)
res=np.hstack((v1,v2))
七.图像金字塔与轮廓检测
高斯金字塔
1 16 [ 1 4 6 4 1 4 16 24 16 4 6 24 36 24 6 4 16 24 16 4 1 4 6 4 1 ] \frac{1}{16}\left[\begin{matrix} 1 & 4 & 6 & 4 & 1\\ 4 & 16 & 24 & 16 & 4\\ 6 & 24 & 36 & 24 & 6\\ 4 & 16 & 24 & 16 & 4\\ 1 & 4 & 6 & 4 & 1 \end{matrix}\right] 161⎣⎢⎢⎢⎢⎡1464141624164624362464162416414641⎦⎥⎥⎥⎥⎤
- 将 G i G_i Gi与高斯内核卷积
- 将所有偶数行和列去除
向上采样(放大)
[ 10 30 56 96 ] − − − > [ 10 0 30 0 0 0 0 0 56 0 96 0 0 0 0 0 ] \left[\begin{matrix} 10 & 30\\ 56 & 96 \end{matrix}\right] --->\left[\begin{matrix} 10 & 0 & 30 & 0\\ 0 & 0 & 0 & 0\\ 56 & 0 & 96 & 0\\ 0 & 0 & 0 & 0 \end{matrix}\right] [10563096]−−−>⎣⎢⎢⎡10056000003009600000⎦⎥⎥⎤
- 1.将图像在每个方向扩大为原来的两倍,新增的行和列用0填充
- 2.使用先前同样的内核(乘以4)与放大后的图像卷积,获得近似值
- 先将图像进行上采样,再将图像进行下采样,虽然图像的大小不改变,但是图像糊变得模糊,因为在上采样过程中是用0对图像进行填充,相当于损失了一些信息,而下采样过程中用卷积核卷积,也损失了一些信息(左边是原图,右边是先上采样后下采样的图像)
image=cv2.imread(path)
up=cv2.pyrUp(image)
up_down=cv2.pyrDown(up)
拉普拉斯金字塔
-
L i = G i − P y r U p ( P y r D o w n ( G i ) ) L_i=G_i-PyrUp(PyrDown(G_i)) Li=Gi−PyrUp(PyrDown(Gi))
-
其中: G i G_i Gi是输入图片
-
1.低通滤波
-
2.缩小尺寸
-
3.放大尺寸
-
4.图像相减
down=cv2.pyrDown(image)
down_up=cv2.pyrUp(down)
l_l=image-down_up
图像轮廓
- cv2.findContours(img,mode,method)
- mode:轮廓检索模式
- RETR_EXTERNAL:只检索最外面的轮廓
- RETR_LIST:检索所有的轮廓,并将其保存到一条链表中
- RETR_CCOMP:检索所有的轮廓,并将他们组织为两层,等曾是各部分的外部边界,第二层是空洞的边界
- RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次**(用的最多的)**
- method:轮廓逼近方法
- CHAIN_APPROX_NONE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)
- CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分
- return:binary,contours,hierarchy,分别是(二值化图像,轮廓信息,层级)
- mode:轮廓检索模式
- 为了更高的准确率,使用二值图像
image=cv2.imread(path)
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
binary,contours,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
- 绘制轮廓
- 传入绘制图像,轮廓,轮廓索引,颜色模式,线条厚度
- 注意:需要copy原图,不然原图会发生改变
draw_image=image.copy()
# drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
# @brief Draws contours outlines or filled contours.
# draw_image:需要绘制的图像
# contours:轮廓,是findContours函数的第二个返回值
# -1:表示默认画出所有轮廓线
#(0,0,255):表示用红色的线画出轮廓
# 2:线宽
res=cv2.drawContours(draw_image,contours,-1,(0,0,255),2)
- 下图左是图像原图,右是轮廓图,注意:轮廓图中,每个图像的内外都被画上了轮廓,因为给定的参数是-1,至于为什么边框是蓝色的…因为为了方便直接保存图像,我用的plt.imshow()函数显示的图像,他的色彩通道是r,g,b…
- 再来看一下如果参数-1换成0会如何,可以看到,只有一个图像的外部被添加上了轮廓
轮廓特征
image=cv2.imread(path)
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
binary,contours,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
# 拿到第0个轮廓
cnt=contours[0]
# 计算面积
ares=cv2.contoursArea(cnt)
# 计算周长 True表示闭合的
girth=cv2.arcLength(cnt,True)
轮廓近似
img=cv2.imread(path)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
binary,contours,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
cnt=contours[0]
draw_img=img.copy()
res=cv2.drawContours(draw_img,[cnt],-1,(0,0,255),2)
epsilon=0.1*cv2.arcLength(cnt,True) # 0.1那个值,越小,就越贴合轮廓
approx=cv2.approxPolyDP(cnt,epsilon,True)
draw_img=img.copy()
res=cv2.drawContours(draw_img,[approx],-1,(0,0,255),2)
- 左边是原轮廓图,右边是轮廓近似图
边界矩形(重要!!)
image=cv2.imread('./data/Image manipulation/contours.png')
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
binary,contours,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
cnt=contours[0]
# 传入轮廓,然后计算边界矩形的x,y,w,h分别对应(x,y)坐标点,以及矩形的宽,高
x,y,w,h=cv2.boundingRect(cnt)
# The function cv::rectangle draws a rectangle outline or a filled rectangle whose two opposite corners,通过两个对角点来绘制一个边界矩形
# 绘制出这个矩形,需要参数:
# image:原图像
# (x,y):对应的起始坐标点
# (x+w,y+h):对角坐标点
# (0,255,0),2:绿线,线宽为2
image=cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),2)
- 计算轮廓面积与边界矩形之比
area=cv2.contourArea(cnt)
x,y,w,h=cv2.boundingRect(cnt)
rect_area=w*h
extent=float(area)/rect_area
print('轮廓面积与边界矩形比:',extent)
- 外接圆(和外接矩形同理)
# 拿到圆心,拿到半径就行了
(x,y),radius=cv2.minEnclosingCircle(cnt)
center=(int(x),int(y))
radius=int(radius)
img=cv2.circle(img,center,radius,(0,255,0),2)
八.直方图与傅里叶变换
直方图
cv2.calcHis(images,channels,mask,histSize,ranges)
- images:原图像格式为uint8或float32,当传入函数时应使用中括号[image]
- channels:同样用中括号,它会告诉函数我们整幅图像的直方图,如果图像是灰度图,他的值就是[0],如果是彩色图,他传入的参数可以是[0][1][2],他们分别是BGR
- mask:掩码图像,统计整幅图像的直方图就将它置为None,但是如果想统计图像某一部分的直方图,就制作一个掩码图像,并使用
- histSize:BIN的数目,也需要用中括号
- ranges:像素值范围常为[0-256]
image=cv2.imread(path)
hist=cv2.calHist([image],[0],None,[256],[0,256])
hist.shape
output:
(256, 1)
plt.hist(img.ravel(),256)
plt.show()
image=cv2.imread(path)
color=('b','g','r')
for i,col in enumerate(color):
histr=cv2.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color=col)
plt.xlim([0,256])
掩码操作
# 创建mask
mask=np.zeros(image.shape[:2],np.uint8)
mask[100:300,100:400]=255
# 上面两步操作,第一步是创建一个全零二维数组,大小和图像大小一样(其实就是一块和图像大小相同的黑色图像),然后将中间一块地方变成255(也就是变成白色)
masked_image=cv2.bitwise_and(image,image,mask=mask) # 与操作(&)
hist_full=cv2.calcHist([image],[0],None,[256],[0,256])
hist_mask=cv2.calcHist([image],[0],mask,[256],[0,256])
plt.subplot(221),plt.imshow(image,'gray')
plt.subplot(222),plt.imshow(mask,'gray')
plt.subplot(223),plt.imshow(masked_image,'gray')
plt.subplot(224),plt.plot(hist_full),plt.plot(hist_mask)
plt.xlim([0,255])
plt.show()
直方图均值化
- 可以观察到,在上面的直方图中,存在“陡峭”的现象,直方图均值化可以使直方图变得平均
img=cv2.imread(path,0) # 这里是以灰度图的方式读进图像
plt.hist(img.ravel(),256)
plt.show()
equ=cv2.equalizeHist(img)
plt.hist(equ.ravel(),256)
plt.show()
res=np.hstack((img,equ))
cv_show('res',res)
- 可以看到,经过直方图均值化后,图像的整体亮度变高,但是,会丢失掉一些图像的细节部分!
自适应直方图均值化
- 自适应直方图均值化是将图片分成几个部分,每个部分别均值化,然后在整合到一起,这样就避免了图像细节部分的丢失
clahe=cv2.createCLAHE(clipLimit=2.0,tileGridSize=(8,8))
res_clahe=clahe.apply(img)
res=np.hstack((img,equ,res_clahe))
傅里叶变换(我也没搞明白)
- 高频:变化剧烈的灰度分量,例如边界
- 低频:变化缓慢的灰度分量,例如大海
img=cv2.imread(path,0)
img_float32=np.float32(img)
dft=cv2.dft(img_float32,flags=cv2.DFT_COMPLEX_OUTPUT)
dft_shift=np.fft.fftshift(dft)
# 得到灰度图像能表示的形式
magnitude_spectrum=20*np.log(cv2.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
plt.subplot(121),plt.imshow(img,cmap='gray')
plt.title('input image'),plt.xticks([]),plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum,cmap='gray')
plt.title('Magitude Spectrum'),plt.xticks([]),plt.yticks([])
plt.show()
滤波
- 低通滤波器:只保留低频,会使得图像模糊
- 高通滤波器:只保留高频,会使得图像细节增强
- OpenCV中主要就是cv2.dft()和cv2.idft(),输入图像需要需要先转换成np.float32格式
- 得到的结果中频率为0的部分会在左上角,通常需要转换到中心位置,可以通过shift变换来实现
- cv2.dft()返回的结果是双通道的(实部,虚部),通常还需要转换成图像格式才能展示(0,255)
九.模板匹配
匹配单个对象
- 模板匹配和卷积原理相像,模板在原图像上从原点开始滑动,计算模板与(图像被模板覆盖的地方)的差别程度,这个差别程度的计算方法在openCV里有6中,然后将每次计算的结果放入一个矩阵里,作为结果输出,假如原图形是AxB大小,模板大小是axb,则输出结果的矩阵是(A-a+1)x(B-b+1)
- TM_SQDIFF:计算平方不同,计算出来的值越小,越相关
- TM_CCORR:计算相关性,计算出来的值越大,越相关
- TM_CCOEFF:计算相关系数,计算出的值越大,越相关
- TM_SQDIFF_NORMED:计算归一化平方不同,计算出来的值越接近0,越相关
- TM_CCORR_NORMED:就散归一化相关性,计算出来的值越接近1,越相关
- TM_CCOEFF_NORMED:计算归一化相关系数,计算出的值越接近1,越相关
img=cv2.imread('./data/Image manipulation/lena.jpg',0)
template=cv2.imread('./data/Image manipulation/face.jpg',0)
img.shape,template.shape
output:
((263, 263), (110, 85))
methods=['cv2.TM_SQDIFF','cv2.TM_CCOEFF_NORMED','cv2.TM_CCORR','cv2.TM_CCORR_NORMED','cv2.TM_SQDIFF','cv2.TM_SQDIFF_NORMED']
res=cv2.matchTemplate(img,template,1)
min_val,max_val,min_loc,max_loc=cv2.minMaxLoc(res)
h,w=template.shape[:2]
for meth in methods:
img2=img.copy()
# 匹配方法的真值
method=eval(meth)
print(method)
res=cv2.matchTemplate(img,template,method)
min_val,max_val,min_loc,max_loc=cv2.minMaxLoc(res)
# 如果是平方差匹配TM_SQDIFF或归一化平方差匹配TM_SQDIFF_NORMED,取最小值
if method in [cv2.TM_SQDIFF,cv2.TM_SQDIFF_NORMED]:
top_left=min_loc
else:
top_left=max_loc
bottom_right=(top_left[0]+w,top_left[1]+h)
# 画矩形
cv2.rectangle(img2,top_left,bottom_right,255,2)
plt.subplot(121),plt.imshow(res,cmap='gray')
plt.xticks([]),plt.yticks([])
plt.subplot(122),plt.imshow(img2,cmap='gray')
plt.xticks([]),plt.yticks([]) # 隐藏坐标
plt.suptitle(meth)
plt.show()
匹配多个对象
# 读入图像
img_rgb=cv2.imread(path')
# 灰度处理
img_gray=cv2.cvtColor(img_rgb,cv2.COLOR_BGR2GRAY)
# 读模板
template=cv2.imread(path,0)
# 拿到模板的高,宽
h,w=template.shape[:2]
# 匹配
res=cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED)
threshold=0.8
# 取匹配程度大于%80的坐标
loc=np.where(res>=threshold)
for pt in zip(*loc[::-1]):
bottom_right=(pt[0]+w,pt[1]+h)
cv2.rectangle(img_rgb,pt,bottom_right,(0,0,255),2)
cv_show('img_rgb',img_rgb)
十.图像特征
Harris角点检测
近似可得:
c
(
x
,
y
;
Δ
x
,
Δ
y
)
≈
∑
ω
(
I
z
(
u
,
v
)
Δ
x
+
I
y
(
u
,
v
)
Δ
y
)
2
=
[
Δ
x
,
Δ
y
]
M
(
x
,
y
)
[
Δ
x
Δ
y
]
c(x,y;\Delta{x},\Delta{y})\approx{\sum_\omega(I_z(u,v)\Delta{x}+I_y(u,v)\Delta{y})^2}=[\Delta{x},\Delta{y}]M(x,y) \left[\begin{matrix} \Delta{x}\\ \Delta{y} \end{matrix}\right]
c(x,y;Δx,Δy)≈ω∑(Iz(u,v)Δx+Iy(u,v)Δy)2=[Δx,Δy]M(x,y)[ΔxΔy]
其中M:
M
(
x
,
y
)
=
∑
ω
[
I
z
(
x
,
y
)
2
I
z
(
x
,
y
)
I
y
(
x
,
y
)
I
z
(
x
,
y
)
I
y
(
x
,
y
)
I
y
(
x
,
y
)
2
]
M(x,y)=\sum_\omega{\left[\begin{matrix} I_z(x,y)^2 & I_z(x,y)I_y(x,y)\\ I_z(x,y)I_y(x,y) & I_y(x,y)^2 \end{matrix}\right] }
M(x,y)=∑ω[Iz(x,y)2Iz(x,y)Iy(x,y)Iz(x,y)Iy(x,y)Iy(x,y)2] =
[ ∑ ω I z ( x , y ) 2 ∑ ω I z ( x , y ) I y ( x , y ) ∑ ω I z ( x , y ) I y ( x , y ) ∑ ω I y ( x , y ) 2 ] = [ A c c b ] \left[\begin{matrix} \sum_\omega{I_z(x,y)^2} & \sum_\omega{I_z(x,y)I_y(x,y)}\\ \sum_\omega{I_z(x,y)I_y(x,y)} & \sum_\omega{I_y(x,y)^2} \end{matrix}\right]=\left[\begin{matrix} A & c\\ c & b \end{matrix}\right] [∑ωIz(x,y)2∑ωIz(x,y)Iy(x,y)∑ωIz(x,y)Iy(x,y)∑ωIy(x,y)2]=[Accb]
简化可得: c ( x , y ; Δ x , Δ y ) ≈ A Δ x 2 + 2 C Δ x Δ y + B Δ y 2 c(x,y;\Delta{x},\Delta{y})\approx{A\Delta{x^2}+2C\Delta{x}\Delta{y}+B\Delta{y^2}} c(x,y;Δx,Δy)≈AΔx2+2CΔxΔy+BΔy2
A = ∑ ω I z 2 , B = ∑ ω I y 2 , C = ∑ ω I x I y A=\sum_\omega{I_z^2},B=\sum_\omega{I_y^2},C=\sum_\omega{I_xI_y} A=∑ωIz2,B=∑ωIy2,C=∑ωIxIy
二次项函数本质上是一个椭圆函数,椭圆方程为:
[
Δ
x
,
Δ
y
]
M
(
x
,
y
)
[
Δ
x
Δ
y
]
=
1
[\Delta{x},\Delta{y}]M(x,y)\left[\begin{matrix}\Delta{x}\\\Delta{y}\end{matrix}\right]=1
[Δx,Δy]M(x,y)[ΔxΔy]=1
- cv2.cornerHarris(img,blockSize,ksize,k)
- img:数据类型为float32的输入图像
- blockSize:角点检测中指定区域的大小
- ksize:Sobel求导中使用的窗口大小
- k:取值参数为[0.04,0.06]
SIFT(Scale Invariant Feature Transform)算法
-
图像尺度空间
在一定范围内,无论物体是大还是小,人眼都可以分辨,然而计算机要有相同的能力却很难,所以要让机器能够对物体在不同尺度下有一个同一的认知,就需要考虑图像在不同的尺度下都存在的特点
尺度空间的获取通常需要使用高斯模糊来实现
image=cv2.imread(path)
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
sift=cv2.xfeatures2d.SFIT_creat()
# kp就是关键点
kp=sift.detect(gray,None)
image=cv2.drawKeypoints(gray,kp,image)
cv_show('drawKeypoints',image)
# 计算特征
kp,des=sift.compute(gray,kp)
十一.背景建模
帧差法
-
由于场景中的目标在移动,目标的影像在不同图像帧中的位置不同,该类算法对时间上连续的两帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判定为运动目标,从而实现目标的检测功能
帧差法会引入噪音和空洞问题
混合高斯模型
- 在进行前景检测前,先对背景进行训练,对图像中的每个背景采用一个混合高斯模型进行检测,每个背景的混合高斯的个数可以自适应,然后在测试阶段,对新来的像素进行GMM匹配,如果该像素值能够匹配其中一个高斯值,则认为是背景,由于整个过程GMM模型在不断更新学习中,所以对动态背景有一定的鲁棒性,最后通过对一个有树枝摇摆的动态背景进行前景检测,取得了较好效果
- 在视频中对于像素点的辩护情况应当是符合高斯分布
- 混合高斯模型学习方法
- 1.首先初始化每个高斯模型矩阵参数
- 2.取视频中T帧数据图像用来训练高斯混合模型,来了第一个像素后用它来做第一个高斯分布
- 3.当后面来的像素时,与前面已有的高斯的均值比较,如果该像素点的值与其模型均值差在3倍的方差内,则属于该分布,并对其进行参数更新
- 4.如果下一次来的像素不满足当前高斯分布,用它来创建一个新的高斯分布
- 混合高斯模型测试方法
- 在测试阶段,对新来的像素点的值与混合高斯模型中的每个均值进行比较,如果其差值在2倍的方差之间的话,则认为是背景,否则认为是前景,将前景赋值为255,背景赋值为0,这样就形成了一副前景二值图
cap=cv2.VideoCapture('path')
kernel=cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
fgbg=cv2.createBackgroundSubtractorMOG2()
while True:
ret,frame=cap.read()
fgmask=fgbg.apply(frame)
# 开运算除噪点
fgmask=cv2.morphologyEx(fgmask,cv2.MORPH_OPEN,kernel)
# 找到轮廓
im,contours,hierarchy=cv2.findContours(fgmask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# 计算周长
perimeter=cv2.arclength(c,True)
if perimeter>188:
# 找到一个直矩形
x,y,w,h=cv2.boundingRect(c)
cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
cv2.imshow('frame',frame)
cv2.imshow('fgmask',fgmask)
k=cv2.waitKey(100) & 0xff
if k==27:
break
cap.release()
cv2.destroyAllWindows()
该像素值能够匹配其中一个高斯值,则认为是背景,由于整个过程GMM模型在不断更新学习中,所以对动态背景有一定的鲁棒性,最后通过对一个有树枝摇摆的动态背景进行前景检测,取得了较好效果
- 在视频中对于像素点的辩护情况应当是符合高斯分布
- 混合高斯模型学习方法
- 1.首先初始化每个高斯模型矩阵参数
- 2.取视频中T帧数据图像用来训练高斯混合模型,来了第一个像素后用它来做第一个高斯分布
- 3.当后面来的像素时,与前面已有的高斯的均值比较,如果该像素点的值与其模型均值差在3倍的方差内,则属于该分布,并对其进行参数更新
- 4.如果下一次来的像素不满足当前高斯分布,用它来创建一个新的高斯分布
- 混合高斯模型测试方法
- 在测试阶段,对新来的像素点的值与混合高斯模型中的每个均值进行比较,如果其差值在2倍的方差之间的话,则认为是背景,否则认为是前景,将前景赋值为255,背景赋值为0,这样就形成了一副前景二值图
cap=cv2.VideoCapture('path')
kernel=cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
fgbg=cv2.createBackgroundSubtractorMOG2()
while True:
ret,frame=cap.read()
fgmask=fgbg.apply(frame)
# 开运算除噪点
fgmask=cv2.morphologyEx(fgmask,cv2.MORPH_OPEN,kernel)
# 找到轮廓
im,contours,hierarchy=cv2.findContours(fgmask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# 计算周长
perimeter=cv2.arclength(c,True)
if perimeter>188:
# 找到一个直矩形
x,y,w,h=cv2.boundingRect(c)
cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
cv2.imshow('frame',frame)
cv2.imshow('fgmask',fgmask)
k=cv2.waitKey(100) & 0xff
if k==27:
break
cap.release()
cv2.destroyAllWindows()