opencv学习总结

OpenCV学习

1、计算机眼中的图像

1.1、 图像简介

① 在计算机眼中,Lena 这个人的图像被分成很多很多个小方格。

② 每一个小格叫做一个像素点,计算机就是由这些像素点组成一张图像的。

③ 每一个像素点有 RGB 三个通道,每个通道的值在 0-255 之间,0 表示黑的,255 表示非常亮。

④ 图像长宽有多少个像素,RGB 矩阵就有多大。

img

① RGB 叫图像的颜色通道,通常情况下,我们看到的一个彩色图都是 RGB 三颜色通道的。

② 灰度图(或黑白图)只用一个通道来表示它的亮度就足够了。

③ RGB 矩阵的大小和图像的大小一样,若图像的像素点宽 500 长 500,则得到 [500,500,3] 的矩阵,这里的 3 就是 3 通道。

RGB(Red, Green, Blue)颜色空间最常用的用途就是显示器系统(计算机、电视机等都是采用RGB颜色空间来进行图像显示)。一般来说,电脑,电视机等是利用三个电子枪分别发射R分量,G分量,B分量的电子束,以此来激发屏幕上的RGB三种颜色的荧光粉,从而发出不同颜色、不同亮度的像素、进而组成了一幅图像;很明显,RGB颜色空间利用了物理学中的三原色叠加从而组成产生各种不同颜色的原理。在RGB颜色空间中,R、G、B三个分量的属性是独立的。也即是说,RGB颜色可以表示为(Red, Green, Blue)。其中,各个分量的数值越小,亮度越低。数值越大,亮度越高;如:(0,0,0)表示黑色,(255,255,255)表示白色;
RGB颜色空间表示颜色的格式有RGB565,RGB555,RGB24,RGB32等;
其中:

    • RGB565是使用16位表示一个像素:5位表示R,6位表示G,5位表示B;
    • RGB555是另一种16位表示一个像素的方法:分别用5位来表示RGB分量;剩余一位不用;
    • RGB24是使用24位表示一个像素:分别用8位表示RGB各个分量;这种方式最为常见;
    • RGB32是使用32位来表示一个像素:分别用8位表示RGB各个分量;剩余8位为alpha通道,也就是用来表示图像的“透明度”。注意:在某些系统中,剩余的8位并没有使用;
      RGB色彩空间称为与设备相关的色彩空间,因为不同的扫描仪扫描同一幅图像,会得到不同色彩的图像数据;不同型号的显示器显示同一幅图像,也会有不同 的色彩显示结果。显示器和扫描仪使用的RGB空间与CIE 1931 RGB真实三原色表色系统空间是不同的,后者 是与设备无关的颜色空间。

2、图像读取与显示

2.1、图像的读取

① 图像读取入口参数:

  • cv2.IMREAD_COLOR:彩色图像
  • cv2.IMREAD_GRAYSCALE:灰度图像

2.2、图像的显示

注意 在特殊情况下,你可以创建一个空窗口,然后再将图像加载到该窗口。在这种情况下,你可以指定窗口是否可调整大小。这是通过功能cv.namedWindow()完成的。默认情况下,该标志为cv.WINDOW_AUTOSIZE。但是,如果将标志指定为cv.WINDOW_NORMAL,则可以调整窗口大小。当图像尺寸过大以及向窗口添加跟踪栏时,这将很有帮助。

cv.namedWindow('image',cv.WINDOW_NORMAL)
cv.imshow('image',img)
cv.waitKey(0)
cv.destroyAllWindows()

2.2.1、 图像显示(普通方法)

# 魔法指令,直接展示图,Jupyter notebook 特有
# %matplotlib inline
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
# print(type(img)) # img 的类型为 numpy.ndarray 类型
# print(img)             # uint8 的取值范围在 0-255 之间
# opencv 默认读取格式是 BGR 格式,matplotlib 或其他库的读取格式可能是 RGB 的
# opencv 读取并用 opencv 自带的展示函数不需要进行通道转换,但 opencv 读取后用其他库展示图片需要通道转换
# 图像显示时,可以创建多个窗口
# 第一个入口参数为展示图像窗口的名字
# 第二个入口参数为展示图像窗口中所展示的图像
cv2.imshow('image_cat', img)
# 等待时间,毫秒级,0表示任意键终止,5000ms表示5s
cv2.waitKey(0)
# 销毁图像窗口
cv2.destroyAllWindows()

2.2.2、 图像显示(函数方法)

# 绘图显示(封装函数)
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
cv_show('image_cat', img)

2.2.3、 灰度图处理与图片保存

# %% md
# 灰度图处理与图片保存
# %% md
# 1. 读取BGR图(默认读取)
# %%
import cv2  # opencv的缩写为cv2
import matplotlib.pyplot as plt  # matplotlib库用于绘图展示
import numpy as np  # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
# % matplotlib inline
# %%
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
# (414, 500, 3)
# print(img.shape)  # (h,w,c) c表示 3 通道,这个 3 通道被 opencv 读进来是 BGR 的先后顺序的 3 通道
# %% md
# 2. 读取灰度图(设置读取)
# %%
img_gray = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg', cv2.IMREAD_GRAYSCALE)
# img_gray = cv2.imread('01_Picture/01_cat.jpg',cv2.IMREAD_COLOR)  # BGR图
img_gray  # 只有一个通道,同样是 uint8 类型
# %%
print('type(img_gray):', type(img_gray))
print('img_gray.size: ', img_gray.size)  # 414 × 500 = 20700
print('img_gray.dtype:', img_gray.dtype)
print('img_gray.shape:', img_gray.shape)  # 只有一个通道

# %% md
# 3. 显示图片
# %%
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
cv_show('image_cat_gray', img_gray)

# %% md
# 4. 保存图片
# %%
cv2.imwrite('01_Picture/02_cat_gray.jpg', img_gray)  # 保存图片

2.2.4、 HSV颜色空间

**HSV颜色空间 **

HSV(hue,saturation,value)颜色空间的模型对应于圆柱坐标系中的一个圆锥形子集,圆锥的顶面对应于V=1. 它包含RGB模型中的R=1,G=1,B=1 三个面,所代表的颜色较亮。色彩H由绕V轴的旋转角给定。红色对应于 角度0° ,绿色对应于角度120°,蓝色对应于角度240°。在HSV颜色模型中,每一种颜色和它的补色相差180° 。 饱和度S取值从0到1,所以圆锥顶面的半径为1。HSV颜色模型所代表的颜色域是CIE色度图的一个子集,这个 模型中饱和度为百分之百的颜色,其纯度一般小于百分之百。在圆锥的顶点(即原点)处,V=0,H和S无定义, 代表黑色。圆锥的顶面中心处S=0,V=1,H无定义,代表白色。从该点到原点代表亮度渐暗的灰色,即具有不同 灰度的灰色。对于这些点,S=0,H的值无定义。可以说,HSV模型中的V轴对应于RGB颜色空间中的主对角线。 在圆锥顶面的圆周上的颜色,V=1,S=1,这种颜色是纯色。HSV模型对应于画家配色的方法。画家用改变色浓和 色深的方法从某种纯色获得不同色调的颜色,在一种纯色中加入白色以改变色浓,加入黑色以改变色深,同时 加入不同比例的白色,黑色即可获得各种不同的色调。

① HSV 颜色空间为:

  • H - 色调(主波长)。
  • S - 饱和度(纯度/颜色的阴影)。
  • V - 强度。

HSV(Hue, Saturation, Value)表示色相、饱和度和亮度。该颜色空间可以用一个圆锥来表示

  • H表示颜色的相位角,取值范围是0~360;S表示颜色的饱和度;
  • S为一比例值,范围从0到1或者0~255。它表示成所选颜色的纯度和该颜色最大的纯度之间的比率,通俗点讲,S表示的是某种颜色的“纯度”, S取值越大,表示色彩越纯,取值越小,表示色彩越灰;
  • V表示色彩的明亮程度,范围从0到1或者0~255。V等于0表示圆锥的底部定点,也就是黑色,V等于1(255)表示圆锥的顶面,当V=1(255)并且S=0时表示纯白色.

需要注意的是:Apple的Mac操作系统以及photoshop都是采用HSV颜色空间

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
# %matplotlib inline
hsv = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg', cv2.COLOR_BGR2HSV)
cv2.imshow('hsv', hsv)
cv2.waitKey(0)
cv2.destroyAllWindows()

3、视频处理

3.1、视频转图片

① cv2.VideoCapture 函数可以捕获摄像头,或读取视频文件。

② cv2.VideoCapture 函数入口参数:

  • 用数字来控制不同的设备(摄像头),例如 0、1。
  • 如果是视频文件,直接指定好路径即可。
"""获取视频的第一帧图片"""
# 随着时间轴的变换,视频在动,虽然图像是静止的,但是图像在一帧一帧走,呈现视频的感觉

# 绘图显示(封装函数)
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


vc = cv2.VideoCapture('D:/pycharm/pycharm-cope/opencv/resource/videos/00_Scenery.mp4')

if vc.isOpened():  # 检查是否打开正确
    open, frame = vc.read()  # 这里的 vc.read() 相当于读取图像的第一帧
    # 若循环不断的执行 vc.read,则不断的读取第二帧、第三帧....
    print(open)  # 正常打开时,open会返回 True
    cv_show('image_scenery', frame)
else:
    open = False
----------------------------------------------------------------
"""抠图操作"""
# 导入所需要的库
import cv2
import numpy as np

# 定义保存图片函数
# image:要保存的图片名字
# addr;图片地址与相片名字的前部分
# num: 相片,名字的后缀。int 类型
def save_image(image, addr, num):
    address = addr + str(num) + '.jpg'
    cv2.imwrite(address, image)


# 读取视频文件
videoCapture = cv2.VideoCapture("D:/pycharm/pycharm-cope/opencv/resource/videos/00_Scenery.mp4")
# 通过摄像头的方式
"""这里是读取本地的视频,如果要真正实现实时预览,可以利用摄像头,通过推流方式实现实时预览。采用摄像头方式如下:
videoCapture=cv2.VideoCapture(1),这里的1表示的是摄像的前置还是后置镜头"""
# videoCapture=cv2.VideoCapture(1)

# 读帧
success, frame = videoCapture.read()
i = 0
"""当我们想按秒提取时,只要我们能知道视频每秒有多少帧,然后在上面的代码基础上每隔那么多帧提取一次就行了,
这就需要查看视频的帧率。一般情况下随便用一个播放器打开视频后右键查看其属性就可以看到帧率。现在我的视频帧率为12 ,
每隔12帧提取一次就可以得到每秒的图片。"""
timeF = 12
j = 0
while success:
    i = i + 1
    if (i % timeF == 0):
        j = j + 1
        save_image(frame, 'D:/pycharm/pycharm-cope/opencv/resource/videos/photo', j)
        print('save image:', i)
    success, frame = videoCapture.read()
    

3.2、加载视频

"""读取视频资源"""
vc = cv2.VideoCapture('D:/pycharm/pycharm-cope/opencv/resource/videos/00_Scenery.mp4')
while open: # 如果正常打开,则不停循环读取,这里可替换成 i 值,来确定读取 i 帧
    ret, frame = vc.read()
    if frame is None: # 视频读完以后的下一帧为空
        break
    if ret == True:
        # gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 读取的图片转换成黑白的
        # gray = cv2.cvtColor(frame, cv2.IMREAD_COLOR) # 读取的图片转换成彩色
        cv2.imshow('result', frame)
        if cv2.waitKey(15) & 0xFF == 27: # cv2.waitKey(10)为等多少时间执行下一帧,0xFF为退出键ESC
            break
vc.release() # release()完成与 open() 相反的工作.释放 open() 向内核申请的所有资源
cv2.destroyAllWindows() # 销毁所有窗口

4、ROI区域

4.1、 位置提取ROI

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包


def cv_show(name,img):
    cv2.imshow(name,img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
cat = img[0:200,0:200] # 选择图片感兴趣的区域
cv_show('cat',cat)

4.2、通道提取ROI

4.2.1、分离 BGR 通道

cv_show('cat_b',b)的运行结果

image-20220710181929732

cv_show('cat_g',g)运行结果

image-20220710182220824

cv_show('cat_r',r)运行结果

image-20220710182233993

img = cv2.merge((b,g,r));cv_show('cat', img),merge函数将他们重新组合在一起
image-20220710182247236

img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
b,g,r = cv2.split(img)
# b.shape: (414, 500)
# g.shape: (414, 500)
# r.shape: (414, 500)
# img.shape: (414, 500, 3)
cv_show('cat_b',b)
print('b.shape:',b.shape) # B通道,单通道,灰度图
cv_show('cat_g',g)
print('g.shape:',g.shape) # G通道,单通道,灰度图
cv_show('cat_r',r)
print('r.shape:',r.shape) # R通道,单通道,灰度图
img = cv2.merge((b,g,r))
cv_show('cat', img)
print('img.shape:',img.shape) # 3 通道,彩色图

4.2.2、展示 R 通道

image-20220710182435987

# # 只保留 R
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
b,g,r = cv2.split(img)
img = cv2.merge((b,g,r))
cur_img = img.copy()
cur_img[:,:,0] = 0
cur_img[:,:,1] = 0
cv_show('R',cur_img)

4.2.3、展示 G 通道

image-20220710182523940

# # 只保留 G
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
cur_img = img.copy()
cur_img[:,:,0] = 0
cur_img[:,:,2] = 0
cv_show('G',cur_img)

4.2.4、展示 B 通道

image-20220710182619251

# # 只保留 R
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
cur_img = img.copy()
cur_img[:,:,1] = 0
cur_img[:,:,2] = 0
cv_show('B',cur_img)

5、边界填充

5.1、边界填充简介

① 边界填充就是对图像进行一些变换,让原始图像进行扩大。

② 边界填充的入口参数:

  • BORDER_REPLICATE:复制法,也就是复制最边缘像素。
  • BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制例如:fedcba|abcdefgh|hgfedcb
  • BORDER_REFLECT_101:反射法,也就是以最边缘像素为轴,对称,gfedcb|abcdefgh|gfedcba
  • BORDER_WRAP:外包装法cdefgh|abcdefgh|abcdefg
  • BORDER_CONSTANT:常量法,常数值填充。

image-20220710191446228

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')

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,borderType=cv2.BORDER_WRAP)
# 方式五:常量法
constant = cv2.copyMakeBorder(img,top_size,bottom_size,left_size,right_size,cv2.BORDER_CONSTANT,value=0)

import matplotlib.pyplot as plt
plt.subplot(231), plt.imshow(img,'gray'), plt.title('ORIGINAL')
plt.subplot(232), plt.imshow(replicate,'gray'), plt.title('REPLICATE')
plt.subplot(233), plt.imshow(reflect,'gray'), plt.title('REPLECT')
plt.subplot(234), plt.imshow(wrap,'gray'),plt.title('REFLECT_101')
plt.subplot(235), plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236), plt.imshow(constant,'gray'),plt.title('CONSTAVI')

plt.show()

5.2、阈值越界处理

img_cat = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
img_dog = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')

img_cat2 = img_cat + 10 # 将 img_cat 矩阵中每一个值都加 10
print(img_cat[:5, :, 0])
# [[142 146 151 ... 156 155 154]
#  [108 112 118 ... 155 154 153]
#  [108 110 118 ... 156 155 154]
#  [139 141 148 ... 156 155 154]
#  [153 156 163 ... 160 159 158]]
print(img_cat2[:5, :, 0])
# [[152 156 161 ... 166 165 164]
#  [118 122 128 ... 165 164 163]
#  [118 120 128 ... 166 165 164]
#  [149 151 158 ... 166 165 164]
#  [163 166 173 ... 170 169 168]]
print((img_cat+img_cat2)[:5, :, 0])  # 0-255 若相加越界后 294 用 294%256 获得余数 38
# [[ 38  46  56 ...  66  64  62]
#  [226 234 246 ...  64  62  60]
#  [226 230 246 ...  66  64  62]
#  [ 32  36  50 ...  66  64  62]
#  [ 60  66  80 ...  74  72  70]]
cv2.add(img_cat, img_cat2)[:5, 0] # cv2.add 是越界后取最大值 255
print(cv2.add(img_cat, img_cat2)[:5, 0])
# [[255 255 255]
#  [226 244 255]
#  [226 248 255]
#  [255 255 255]
#  [255 255 255]]

6、图像处理

图像融合的前提是要保证两张图片的数据一致,下面的例子可以发现猫的数据为(414, 500, 3);狗的数据为(429, 499, 3),所以我们需要转换数据,保证一样,才能融合。

6.1、图像变换

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

## 1.1 图像变换
img_cat = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
img_dog = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/03_dog.jpg')

# print(img_cat.shape)
# # (414, 500, 3)
# print(img_dog.shape)
# # (429, 499, 3)
# img_cat+img_dog # 不同数据大小不能执行数值计算操作

print(img_cat.shape)
print(img_dog.shape)
# (414, 500, 3)
# (429, 499, 3)
# 把狗的像素大小调为和猫的一样
img_dog = cv2.resize(img_dog,(500,414))
# (414, 500, 3)
print(img_dog.shape)

6.2、图像融合

download

res = cv2.addWeighted(img_cat,0.4,img_dog,0.6,0) # img_cat 的权重为 0.4,img_dog 的权重为 0.6
print(img_dog.shape)
plt.imshow(res)

6.3、图像阈值

① ret, dst = cv2.threshold(src, thresh, maxval, type)

  • src: 输入图,只能输入单通道图像,通常来说为灰度图
  • thresh: 阈值
  • dst: 输出图
  • ret: 阈值
  • maxval: 当像素值超过了阈值 ( 或者小于阈值,根据 type 来决定 ),所赋予的值
  • type:二值化操作的类型,包含以下5种类型:
  • cv2.THRESH_BINARY 超过阈值部分取maxval ( 最大值 ),否则取0
  • cv2.THRESH_BINARY_INV THRESH_BINARY的反转
  • cv2.THRESH_TRUNC 大于阈值部分设为阈值,否则不变
  • cv2.THRESH_TOZERO 大于阈值部分不改变,否则设为0
  • cv2.THRESH_TOZERO_INV THRESH_TOZERO的反转
import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包


img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat',cv2.IMREAD_COLOR)
img_gray = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat',cv2.IMREAD_GRAYSCALE)
ret, thresh1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)
print(ret)
ret, thresh2 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV) # THRESH_BINARY_INV 相对 THRESH_BINARY 黑的变成白的,白的变成黑的
print(ret)
ret, thresh3 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TRUNC)
print(ret)
ret, thresh4 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO)
print(ret)
ret, thresh5 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_TOZERO_INV)
print(ret)

titles = ['original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, 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([])
plt.show()

image-20220711120052152

6.4、图像平滑处理

6.4.1、均值滤波

原图:

image-20220711160659971

image-20220711160730851

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
%matplotlib inline   

img = cv2.imread('01_Picture/04_LenaNoise.png')
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

# 均值滤波
# 简单的平均卷积操作,方框中的值相加,取平均,替换掉中心204的值

blur = cv2.blur(img,(3,3)) # (3,3) 为核的大小,通常情况核都是奇数 3、5、7
cv2.imshow('blur',blur)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.4.2、方框滤波

方框滤波如果做归一化,得到的结果和均值滤波一模一样(normalize=True)

image-20220711160903856

normalize=False(越界的值取 255)

image-20220711161220324

# 方框滤波
# 基本和均值一样,可以选择归一化

# 在 Python 中 -1 表示自适应填充对应的值,这里的 -1 表示与颜色通道数自适应一样
box = cv2.boxFilter(img,-1,(3,3),normalize=True)  # 方框滤波如果做归一化,得到的结果和均值滤波一模一样
cv2.imshow('box',box)
cv2.waitKey(0)
cv2.destroyAllWindows()

# 方框滤波
# 基本和均值一样,可以选择归一化,容易越界

box = cv2.boxFilter(img,-1,(3,3),normalize=False)  # 越界的值取 255
cv2.imshow('box',box)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.4.3、高斯滤波

image-20220711161238351

# 高斯函数,越接近均值时,它的概率越大。
# 离中心值越近的,它的权重越大,离中心值越远的,它的权重越小。

aussian = cv2.GaussianBlur(img,(5,5),1)
cv2.imshow('aussian',aussian)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.4.1、中值滤波

image-20220711161257410

# 中值滤波
# 排序后拿中值替代中间元素值的大小

median = cv2.medianBlur(img,5)
cv2.imshow('median',median)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.4.5、展示所有滤波

# 展示所有的

res = np.hstack((blur,aussian,median)) # 矩阵横着拼接
#res = np.vstack((blur,aussian,median)) # 矩阵竖着拼接
print(res)
cv2.imshow('median vs average', res)      
cv2.waitKey(0)
cv2.destroyAllWindows()

6.5、图像缩放

6.5.1、 倍数缩放

image-20220711160245058

img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/01_cat.jpg')
res = cv2.resize(img,(0,0),fx=3,fy=1) # (0,0)表示不确定具体值,fx=3 相当于行像素 x 乘 3,fy=1 相当于 y 乘 1
# plt.imshow(res)
cv2.imshow(' ', res)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.5.2、 等比例缩放

image-20220711160339330

res = cv2.resize(img,(0,0),fx=1.5,fy=1.5) # 同比例放缩
plt.imshow(res)
cv2.imshow(' ', res)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.6、腐蚀和膨胀

6.6.1、腐蚀

# 腐蚀与膨胀
# 1. 腐蚀操作

# 1.1 汉字腐蚀(例)
import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
# %matplotlib inline
# 腐蚀操作通常是拿二值图像做腐蚀操作

img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/05_Dige.png')
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 只要框里有黑色,中心点的值就变为黑色,即原来的白色被黑色腐蚀掉
kernel = np.ones((5,5),np.uint8)
erosion = cv2.erode(img,kernel,iterations=1)

cv2.imshow('erosion',erosion)
cv2.waitKey(0)
cv2.destroyAllWindows()
## 1.2 圆形腐蚀(例)
pie = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/06_pie.png')
cv2.imshow('pie',pie)
cv2.waitKey(0)
cv2.destroyAllWindows()
kernel = np.ones((30,30),np.uint8)
erosion_1 = cv2.erode(pie,kernel,iterations=1)
erosion_2 = cv2.erode(pie,kernel,iterations=2)
erosion_3 = cv2.erode(pie,kernel,iterations=3)
res = np.hstack((erosion_1,erosion_2,erosion_3))
cv2.imshow('res',res)
cv2.waitKey(0)
cv2.destroyAllWindows()

image-20220711162251655

6.6.2、膨胀

# 2. 膨胀操作
## 2.1 汉字膨胀(例)
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/05_Dige.png')
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 先腐蚀 后膨胀,抵消腐蚀造成的损害
kernel = np.ones((3,3),np.uint8)
# iterations = 1 迭代次数为1
dige_erosion = cv2.erode(img,kernel,iterations=1)
cv2.imshow('erosion',dige_erosion)
cv2.waitKey(0)
cv2.destroyAllWindows()

kernel = np.ones((3,3),np.uint8)
dige_dilate = cv2.dilate(dige_erosion,kernel,iterations=1)
cv2.imshow('dilate',dige_dilate)
cv2.waitKey(0)
cv2.destroyAllWindows()
## 2.2 圆形腐蚀(例)
pie = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/06_pie.png')

kernel = np.ones((30,30),np.uint8)
dilate_1 = cv2.dilate(pie,kernel,iterations=1)
dilate_2 = cv2.dilate(pie,kernel,iterations=2)
dilate_3 = cv2.dilate(pie,kernel,iterations=3)
res = np.hstack((dilate_1,dilate_2,dilate_3))
cv2.imshow('res',res)
cv2.waitKey(0)
cv2.destroyAllWindows()

image-20220711162324306

6.7、开运算与闭运算

6.7.1、开运算

先腐蚀,再膨胀

image-20220711163315646

# 开:先腐蚀,再膨胀
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/05_Dige.png')

kernel = np.ones((5,5),np.uint8)
opening = cv2.morphologyEx(img,cv2.MORPH_OPEN,kernel)

cv2.imshow('opening',opening)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.7.2、闭运算

先膨胀,再腐蚀

image-20220711163331209

# 闭:先膨胀,再腐蚀
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/05_Dige.png')

kernel = np.ones((5,5),np.uint8)
closing = cv2.morphologyEx(img,cv2.MORPH_CLOSE,kernel)

cv2.imshow('closing',closing)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.7.3、梯度运算

梯度 = 膨胀 - 腐蚀

image-20220711163354318

image-20220711163409563

# 梯度 = 膨胀 - 腐蚀
pie = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/06_pie.png')
# 选择了7×7的内核
kernel = np.ones((7,7),np.uint8)
dilate = cv2.dilate(pie,kernel,iterations=5)#执行五次腐蚀
erosion = cv2.erode(pie,kernel,iterations=5)#执行五次膨胀

res = np.hstack((dilate,erosion))#将两个结果组合在一起
cv2.imshow('res',res)
cv2.waitKey(0)
cv2.destroyAllWindows()
#得到边界信息,就是相当于用膨胀 - 腐蚀
gradient = cv2.morphologyEx(pie,cv2.MORPH_GRADIENT,kernel)
cv2.imshow('gradient',gradient)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.8、礼帽与黑帽

6.8.1、礼帽

礼帽 = 原始输入-开运算

image-20220711164334901

# 礼帽
# 原始带刺,开运算不带刺,原始输入-开运算 = 刺
img = cv2.imread('D:/pycharm/pycharm-cope/opencv/resource/photo/05_Dige.png')
kernel = np.ones((5,5),np.uint8)
tophat = cv2.morphologyEx(img,cv2.MORPH_TOPHAT,kernel)
cv2.imshow('tophat',tophat)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.8.2、黑帽

黑帽 = 闭运算-原始输入

image-20220711164522209

# 黑帽  
# 原始带刺,闭运算带刺并且比原始边界胖一点,闭运算-原始输入 = 原始整体
img = img = cv2.imread('01_Picture/05_Dige.png')
kernel = np.ones((5,5),np.uint8)
blackhat = cv2.morphologyEx(img,cv2.MORPH_BLACKHAT,kernel)
cv2.imshow('blackhat',blackhat)
cv2.waitKey(0)
cv2.destroyAllWindows()

6.9、图像梯度

6.9.1、Sobel算子

① Sobel算子函数:cv2.Sobel(src, ddepth, dx, dy, ksize),返回值为Sobel算子处理后的图像。

  • ddepth:图像的深度
  • dx 和 dy 分别表示水平和竖直方向
  • ksize 是 Sobel 算子的大小

② 靠近最近点的左右和上下的权重最高,所以为±2。

download

圆形处理为例

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

pie = cv2.imread('01_Picture/06_pie.png') # 读取图像
cv2.imshow('img',pie)
cv2.waitKey()
cv2.destroyAllWindows()

结果显示:

image-20220711174850829

# 梯度就是边界点,左边右边不一样
def cv_show(img,name):
    cv2.imshow(name,img)
    cv2.waitKey()
    cv2.destroyAllWindows()
    
# 白到黑是正数,黑到白是负数了,所有的负数会被截断成 0,所以要取绝对值
sobelx = cv2.Sobel(pie,cv2.CV_64F,1,0,ksize=3) # 1,0 表示只算水平方向梯度
cv_show(sobelx,'sobelx')

结果显示:计算的是X方向也就是水平方向梯度(没有取绝对值)

image-20220711175045150

sobelx = cv2.Sobel(pie,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx) # 取负数时,取绝对值
cv_show(sobelx,'sobelx')

sobely = cv2.Sobel(pie,cv2.CV_64F,0,1,ksize=3) # 1,0 只算 y 方向梯度
sobely = cv2.convertScaleAbs(sobely) # 取负数时,取绝对值
cv_show(sobely,'sobely')

结果显示:计算的是X方向也就是水平方向梯度(取绝对值)

image-20220711175549002

结果显示:计算的是Y方向也就是竖直方向梯度(取绝对值)

image-20220711175602778

两种计算x,y的方式

# 计算 x 和 y 后,再求和
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0) # 0是偏置项
cv_show(sobelxy,'sobelxy')

结果显示:

image-20220711175824508

# 不建议直接计算,还有重影
sobelxy = cv2.Sobel(pie,cv2.CV_64F,1,1,ksize=3)
sobelxy = cv2.convertScaleAbs(sobelxy)
cv_show(sobelxy,'sobelxy')

结果显示:

image-20220711175923473

人物为例

img = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE)
cv_show(img,'img')

img = cv2.imread('01_Picture/07_Lena.jpg',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)

# 整体计算有重影和模糊,不建议整体计算
img1 = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelxy1 = cv2.Sobel(img,cv2.CV_64F,1,1,ksize=3)
sobelxy1 = cv2.convertScaleAbs(sobelxy1)
res = np.hstack((sobelxy,sobelxy1))
cv_show(res,'sobelxy')

结果显示:

image-20220711180625831

6.9.2、Scharr算子

① 对结果的差异更敏感一些。

download

6.9.3、Laplacian算子

① Laplacian算子用的是二阶导,对噪音点更敏感一些。

② 如果中心点是边界,它与周围像素点差异的幅度会较大,Laplacian算子根据此特点可以把边界识别出来。

download

各个算子区别:

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

img = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE)
cv_show(img,'img')

# 不同算子的差异
img = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.convertScaleAbs(sobely)
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)   

scharrx = cv2.Scharr(img,cv2.CV_64F,1,0)
scharry = cv2.Scharr(img,cv2.CV_64F,0,1)
scharrx = cv2.convertScaleAbs(scharrx)
scharry = cv2.convertScaleAbs(scharry)
scharrxy = cv2.addWeighted(scharrx,0.5,scharry,0.5,0)

laplacian = cv2.Laplacian(img,cv2.CV_64F) # 没有 x、y,因为是求周围点的比较
laplacian = cv2.convertScaleAbs(laplacian)

res = np.hstack((sobelxy,scharrxy,laplacian))
cv_show(res,'res')

结果展示(sobel 、 scharr、laplacian )

image-20220711181209952

6.10、Canny边缘检测

6.10.1、流程介绍

① Canny边缘检测流程:

  • 使用高斯滤波器,以平滑图像,滤除噪声。
  • 计算图像中每个像素点的梯度强度和方向。
  • 应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应。
  • 应用双阈值(Double-Threshold)检测来确定真实的和潜在的边缘。
  • 通过抑制孤立的弱边缘最终完成边缘检测。

6.10.2、高斯滤波器

① 高斯滤波器靠近的中心点的权重比较大,较远中心点的权重比较小。

download

6.10.3、梯度和方向

download

6.10.4、非极大值抑制

① C 点的梯度和方向可以通过前一步算出来。

② C 点的梯度是不是一个极大值点,应该是去跟它的临近点去比较。

③ 利用 C 点梯度的方向,可以得到上面有一个交点 Q,下面有一个交点 Z,如果 C 点的梯度比 Q 和 Z 都大,那么 C 就是极大值点,其中 Q 和 Z 的梯度值通过线性差值法来计算。

④ 如果 C 的梯度是极大值点,那么 C 就是边缘点。否则 C 不是极大值点,就会被抑制。

download

① 简单计算将像素点周围固定为八个像素,当梯度角度相交的点与哪个方向近,就哪个方向的两个点。

② 例如,梯度方向是 43° 就取上下两个像素来做极大值判断,如果梯度方向是 46°,就取左下、右上两个像素来做极大值判断。

③ 如下图所示,如果 A 的梯度值比 B 和 C 都要大,那么 A 就是边界,由于边界与梯度方向垂直,所以如下图所示黑色为边界。

download

6.10.5、双阈值检测

① C 在 minVal 与 maxVal 之间,是候选边界,若 C 的左右或上下两边连有 A,而 A 是边界,那么定义 C 也是边界。

② B 在 minVal 与 maxVal 之间,是候选边界,若B的左右或上下像素都不是边界,那么 B 就被舍弃,不定义为边界。

download

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

def cv_show(img,name):
    cv2.imshow(name,img)
    cv2.waitKey()
    cv2.destroyAllWindows()
    
img = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE) 

v1 = cv2.Canny(img,80,150) # 第二个参数为minVal,第三个参数为maxVal
v2 = cv2.Canny(img,50,100)

res = np.hstack((v1,v2))
cv_show(res,'res')

img = cv2.imread('01_Picture/08_Car.png',cv2.IMREAD_GRAYSCALE) 

v1 = cv2.Canny(img,120,250) # 第二个参数为minVal,第三个参数为maxVal
v2 = cv2.Canny(img,50,100)

res = np.hstack((v1,v2))
cv_show(res,'res')

image-20220711182939970

image-20220711184001903

6.11、图像金字塔

6.11.1、图像金字塔简介

① 金字塔的底层是比较大,越往上越小,图像金字塔就是把图像组合成金字塔的形状。

② 图像金字塔可以做图像特征提取,做特征提取时有时可能不光对原始输入做特征提取,可能还会对好几层图像金字塔做特征提取。可能每一层特征提取的结果是不一样的,再把特征提取的结果总结在一起。

① 常用的两种图像金字塔形式:

  • 高斯金字塔
  • 拉普拉斯金字塔

download

6.11.2、高斯金字塔

①向下采样方法 ( 缩小 )

download

②向上采样方法 ( 放大 )

download

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包 

def cv_show(img,name):
    cv2.imshow(name,img)
    cv2.waitKey()
    cv2.destroyAllWindows()
    
# 输出原始图片大小  (442, 340, 3)  
img = cv2.imread('01_Picture/09_AM.png')
cv_show(img,'img')
print(img.shape)


# 输出放大图片大小  (884, 680, 3)  
img = cv2.imread('01_Picture/09_AM.png')
up = cv2.pyrUp(img)
cv_show(up,'up')
print(up.shape)


# 输出缩小图片大小  (221, 170, 3)  
img = cv2.imread('01_Picture/09_AM.png')
down = cv2.pyrDown(img)
cv_show(down,'down')
print(down.shape)

# 先放大再缩小与原图对比,结果发现有像素丢失
img = cv2.imread('01_Picture/09_AM.png')
up = cv2.pyrUp(img)
up_down = cv2.pyrDown(up) # 先上采样再下采样
cv_show(np.hstack((img,up_down)),'up_down')

image-20220711230847306

6.11.3、拉普拉斯金字塔

① 拉普拉斯金字塔的每一层图像尺寸不变。

② 拉普拉斯金字塔的每一层操作都是上一层处理后作为输入,该输入减去该输入缩小放大后的图像,获得该层的输出。

download

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包 


#(442, 340, 3)
img = cv2.imread('01_Picture/09_AM.png')
down = cv2.pyrDown(img)
down_up = cv2.pyrUp(down)
L_1 = img - down_up
cv_show(L_1,'L_1')
print(L_1.shape)

image-20220711231300382

6.12、图像轮廓

6.12.1、图像轮廓简介

① 边缘有一些零零散散的线段也可以当做边缘,反正梯度上下左右发生差异,就把它当做边缘了。

② 图像的轮廓必须是一个整体,不是零零散散的,而是连在一块的。

③ 图像轮廓函数:cv2.findContours(img,mode,method)

mode:轮廓检索模式

  • RETR_EXTERNAL :只检索最外面的轮廓。
  • RETR_LIST:检索所有的轮廓,并将其保存到一条链表当中。
  • RETR_CCOMP:检索所有的轮廓,并将他们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界。
  • RETR_TREE:检索所有的轮廓,并重构嵌套轮廓的整个层次。( 最常用 )

method:轮廓逼近方法

  • CHAIN_APPROX_NONE:以Freeman链码的方式输出轮廓,如下图左所示。所有其他方法输出多边形 ( 顶点的序列 ),如下图右所示。
  • CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分,如下图右所示。

④ 为了更高的准确率,轮廓检测使用二值图像。

download

6.12.2、绘制轮廓

(1)图像二值化

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
%matplotlib inline  

def cv_show(img,name):
    cv2.imshow(name,img)
    cv2.waitKey()
    cv2.destroyAllWindows()
    
img = cv2.imread('01_Picture/08_Car.png')
cv_show(img,'img')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于 17 的取 255,小于 127 的取 0       
cv_show(thresh,'thresh')    

image-20220711232413529

(2)轮廓检测

# 做完二值后,再用图像轮廓检测函数再去做
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
# cv_show(contours,'binary') # 返回的二值化后的图像
print(np.array(contours).shape) # 轮廓点的信息
print(hierarchy) # hierarchy 是把轮廓结果保存在层级结构当中,暂时用不上

(2579,)
[[[   1   -1   -1   -1]
  [   2    0   -1   -1]
  [   3    1   -1   -1]
  ...
  [  -1 2575 2577   -1]
  [2578   -1   -1 2576]
  [  -1 2577   -1 2576]]]

(3)绘制所有轮廓

# 传入参数:图像、轮廓、轮廓索引(自适应,画所有轮廓),颜色模式,线条厚度
# 注意需要copy,要不原图会变。。。
cv_show(img,'img')
draw_img = img.copy() # 若不用拷贝后的,而是用原图画轮廓,则画轮廓图绘把原始的输入图像重写,覆盖掉 
res = cv2.drawContours(draw_img,contours,-1,(0,0,255),2) 
cv_show(res,'res')

image-20220711232836001

(4)综合展示

img = cv2.imread('01_Picture/10_contours.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

draw_img = img.copy() # 若不用拷贝后的,而是用原图画轮廓,则画轮廓图绘把原始的输入图像重写,覆盖掉 
res = cv2.drawContours(draw_img,contours,-1,(0,0,255),2) #-1表示画出所有图形的里面轮廓,可以换不同的数字进行测试
cv_show(res,'res')

image-20220711233033643

6.12.3、轮廓特征提取

img = cv2.imread('01_Picture/10_contours.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       

contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

cnt = contours[0] # 通过轮廓索引,拿到该索引对应的轮廓特征
print(cv2.contourArea(cnt)) # 该轮廓的面积
print(cv2.arcLength(cnt,True)) # 该轮廓的周长,True表示闭合的

#输出结果:8500.5
#        437.9482651948929

6.12.4、轮廓近似

① 正常轮廓展示是最右边的图,但是当我们需要轮廓没有那么不规则,而是想要轮廓近似成规则的形状,这就叫轮廓近似,近似成下图中中间图像的轮廓。

② 一条呈抛物线的曲线的端点为 A、B 两点,取曲线上到直线 AB 距离最大的点,该点为 C 点,若 C 点到直线的距离小于设置的阈值,则可以把直线 AB 当做曲线的近似,若 C 点到直线的距离大于设置的阈值,那么曲线不能用直线 AB 来近似,而 AC 曲线可能用 AC 直线来代替、BC曲线可能用 BC 直线来代替。再通过阈值来判断是否可以代替。

download

正常轮廓展示

img = cv2.imread('01_Picture/11_contours2.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       

contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

draw_img = img.copy() # 若不用拷贝后的,而是用原图画轮廓,则画轮廓图绘把原始的输入图像重写,覆盖掉 
res = cv2.drawContours(draw_img,contours,-1,(0,0,255),2) # -1表示里外轮廓都显示
cv_show(res,'res')

image-20220711233725525

轮廓近似展示

img = cv2.imread('01_Picture/11_contours2.png')

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       
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) 
cv_show(res,'res')


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)
cv_show(res,'res')

image-20220711233752710

image-20220711233848264

外接矩形

img = cv2.imread('01_Picture/10_contours.png')

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cnt = contours[6]#里面的数字不同得到不同的信息,画出不一样的轮廓

x,y,w,h = cv2.boundingRect(cnt) # 可以得到矩形四个坐标点的相关信息
img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255),2)
cv_show(img,'img')


-------------------------------------------------------------
area = cv2.contourArea(cnt)
rect_area = w * h
extent = float(area) / rect_area
print('轮廓面具与边界矩形比:',extent)

#轮廓面具与边界矩形比: 0.7732441471571906

image-20220711233928314

外接圆

img = cv2.imread('01_Picture/10_contours.png')

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 大于17的取255,小于127的取0       
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cnt = contours[0]

draw_img = img.copy()
(x,y),redius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
redius = int(redius)
img = cv2.circle(draw_img,center,redius,(0,255,0),2)
cv_show(img,'img')

image-20220711233955703

7、图像处理2

7.1、模板匹配

7.1.1、模板匹配简介

① 模板匹配和卷积原理很像,模板在原图像上从原点开始滑动,计算模板与(图像被模板覆盖的地方)的差别程度(例如值127与值190的区别),这个差别程度的计算方法在opencv里有6种,然后将每次计算的结果放入一个矩阵里,作为结果输出。

② 假如原图形是AxB大小,而模板是axb大小,则输出结果的矩阵是(A-a+1)x(B-b+1)。

③ 模板匹配计算方式6种方式 ( 用归一化后的方式更好一些 ):

  • TM_SQDIFF:计算平方不同,计算出来的值越小,越相关。
  • TM_CCORR:计算相关性,计算出来的值越大,越相关。
  • TM_CCOEFF:计算相关系数,计算出来的值越大,越相关。
  • TM_SQDIFF_NORMED:计算归一化平方不同,计算出来的值越接近0,越相关。
  • TM_CCORR_NORMED:计算归一化相关性,计算出来的值越接近1,越相关。
  • TM_CCOEFF_NORMED:计算归一化相关系数,计算出来的值越接近1,越相关。

④ 公式:https://docs.opencv.org/3.3.1/df/dfb/group__imgproc__object.html#ga3a7850640f1fe1f58fe91a2d7583695d

7.1.2 、模板匹配单个对象

import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np   # numpy数值计算工具包

# 魔法指令,直接展示图,Jupyter notebook特有
%matplotlib inline  

template = cv2.imread('01_Picture/12_Face.jpg',0)  # 0 表示以灰度图方式读取
img = cv2.imread('01_Picture/13_Lena.jpg',0) 
h, w = template.shape[:2] # 获得模板的宽和高
print(img.shape)
print(template.shape)


# (263, 263)
# (110, 85)


methods = ['cv2.TM_CCOEFF','cv2.TM_CCOEFF_NORMED','cv2.TM_CCORR',
          'cv2.TM_CCORR_NORMED','cv2.TM_SQDIFF','cv2.TM_SQDIFF_NORMED']
res = cv2.matchTemplate(img, template, cv2.TM_SQDIFF)
print(res.shape) # 返回的矩阵大小 (A-a+1)x(B-b+1)
min_val,max_val,min_loc,max_loc = cv2.minMaxLoc(res) # 返回模板匹配后最小值、最大值的位置   
print(min_val) # cv2.TM_SQDIFF方法中,越小的值表示像素点的差异越小
print(max_val)
print(min_loc) # 当获得最小值对应的模板左上角的位置,加上模板自身的长、宽,可以在原图像中画出最匹配的区域
print(max_loc)

# (154, 179)
# 39168.0
# 74403584.0
# (107, 89)
# (159, 62)

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()

download

download

download

download

download

download

7.1.3、模板匹配多个对象

img_rgb = cv2.imread('01_Picture/14_Mario.jpg')
img_gray = cv2.cvtColor(img_rgb,cv2.COLOR_BGR2GRAY)
print('img_gray.shape:',img_gray.shape)
template = cv2.imread('01_Picture/15_Mario_coin.jpg',0)
print('template.shape:',template.shape)
h, w = template.shape[:2]

# res 是 result 的简称
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED) # res 是返回每一个小块窗口得到的结果值
print('res.shape:',res.shape)
threshold = 0.8

# 取匹配程度大于 80% 的坐标
loc = np.where(res >= threshold) # np.where 使得返回 res 矩阵中值大于 0.8 的索引,即坐标
print('type(loc):',type(loc)) # loc 为元组类型
print('len(loc):',len(loc))  # loc 元组有两个值
print('len(loc[0]):',len(loc[0]),'len(loc[1]):',len(loc[1]))   # loc 元组每个值 120 个元素
print('type(loc[0]):',type(loc[0]),'type(loc[1]):',type(loc[1])) # loc 元组每个值的类型为 numpy.array 
print("loc[::-1]:",loc[::-1]) # loc[::-1] 表示顺序取反,即第二个 numpy.array 放在第一个 numpy.array 前面     

i = 0
# zip函数为打包为元组的列表,例 a = [1,2,3] b = [4,5,6] zip(a,b) 为 [(1, 4), (2, 5), (3, 6)]    
for pt in zip(*loc[::-1]): # 当用 *b 作为传入参数时, b 可以为列表、元组、集合,zip使得元组中两个 numpy.array 进行配对   
    bottom_right = (pt[0] + w, pt[1] + h)
    cv2.rectangle(img_rgb, pt, bottom_right, (0,0,255),2)
    i = i + 1
print('i:',i)

cv2.imshow('img_rgb',img_rgb)
cv2.waitKey(0)

image-20220712153110741

7.2、图像直方图

7.2.1、图像直方图简介

① 图像直方图是把图像变为灰度图,分成一个一个像素点的值进行统计,如下图左所示。

② 直方图统计函数 cv2.calcHist(images,channels,mask,histSize,ranges)

  • images:原图像的图像格式为 uint8 或 float32。当传入函数时应该用中括号 [] 括来传入,例如[img]
  • channels:同样用中括号来传入,它会告诉函数统幅的哪幅灰度图的直方图。如果传入的图像是灰度图它的值就是 [0],如果是彩色图像,那么传入的参数可以是 [0]、[1]、[2],它们分别对应着 B、G、R 通道,每个通道的图像都是灰度图。
  • mask:掩模图像。统计整幅图像的直方图时就把它设为 None。但是如果你想统计图像的某一部分区域的直方图的,你就制作一个掩模图像并使用它。
  • histSize:BIN 的数目。也应用中括号括来。
  • ranges: 统计的像素值范围,常为 [0-256]。

download

7.2.2、图像直方图统计

import cv2
import numpy as np
import matplotlib.pyplot as plt # Matplotlib 是 RGB
%matplotlib inline

def cv_show(img,name):
    cv2.imshow(name,img)
    cv2.waitKey()
    cv2.destroyAllWindows()
    
img = cv2.imread('01_Picture/01_Cat.jpg',0) # 0 表示灰度图
print((img.ravel()).shape) # (207000,)
plt.hist(img.ravel(),256) # img.ravel()将 img 拉成一维数组
plt.show()
    

download

img = cv2.imread('01_Picture/01_Cat.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
    histr = cv2.calcHist([img],[i],None,[256],[0,256])
    print(histr.shape)
    plt.plot(histr,color=col)
    plt.xlim([0,256])
    
 # (256, 1)
 # (256, 1)
 # (256, 1)
    

download

7.2.3、图像掩码区域

img = cv2.imread('01_Picture/01_Cat.jpg',0)
# cv_show(img,'img')
print(img.shape[:2])
mask = np.zeros(img.shape[:2],np.uint8)# np.uint8无符号整形
print(mask.shape)
mask[100:300,100:400] = 255
# cv_show(mask,'mask')
masked_img = cv2.bitwise_and(img,img,mask=mask) # 与操作
cv_show(np.hstack((img,mask,masked_img)),'res')

(414, 500)
(414, 500)

image-20220712154511084

7.2.4、图像掩码直方图

hist_full = cv2.calcHist([img],[0],None,[256],[0,256]) # 不带掩码统计直方图
hist_mask = cv2.calcHist([img],[0],mask,[256],[0,256]) # 带上掩码统计直方图
plt.subplot(221), plt.imshow(img,'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img,'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask) # 掩码对应的部分区域的直方图的量要小一些      
plt.xlim([0,256])
plt.show()

download

7.2.5、直方图均衡化

① 直方图均衡化:一般可以用来提升图片的亮度。

② 直方图均衡前是一个瘦高的统计图,直方图均衡后是一个矮胖的统计图。

download

download

download

img = cv2.imread('01_Picture/16_Clahe.jpg',0)
plt.hist(img.ravel(),256)
plt.show()

download

equ = cv2.equalizeHist(img)
plt.hist(equ.ravel(),256)
plt.show()

download

res = np.hstack((img,equ))
cv_show(res,'res')

image-20220712155040320

7.2.6、自适应直方图均衡化

① 如上图所示,直方图均衡化,人脸石膏本来有一些特征,可能由于直方图均衡导致丢失一些细节。所以可能切分成几个小块,局部做直方图均衡化,会比较好。

② 切分成几个小块之后,可能会导致一个现象,每个格子都会产生一个边界,opencv是对每个格子的边界进行线性插值处理。

③ 直方图均衡化函数:cv2.createCLAHE(clipLimit,tileGridSize)

  • clipLimit 颜色对比度的阈值。
  • titleGridSize 进行像素均衡化的网格大小,即在多少网格下进行直方图的均衡化操作。
img = cv2.imread('01_Picture/16_Clahe.jpg',0)
clahe = cv2.createCLAHE(clipLimit=2.0,tileGridSize=(8,8)) # 自适应均衡化方法生成出来    
res_clahe = clahe.apply(img) # 方法应用到输入图片当中
res = np.hstack((img,equ,res_clahe))
cv_show(res,'res')

image-20220712155131488

7.3、傅里叶变换

7.3.1、傅里叶变换简介

① 我们生活在时间的世界中,早上7:00起来吃早饭,8:00去挤地铁,9:00开始上班。。。以时间为参照就是时域分析。

② 但是在频域中一切都是静止的!( 每天都吃早饭,每个工作日都挤地铁。。。频域更关心做这件事的频率是多少。)

参考文献:https://zhuanlan.zhihu.com/p/19763358

7.3.2、傅里叶变换作用

① 高频:变化剧烈的灰度分量,例如边界礁石。

② 低频:变化缓慢的灰度分量,例如一片大海。

③ 高通滤波器:只保留高频,会使得图像细节增强。高频边界锐化了,增强了,细节更明显了。

④ 低通滤波器:只保留低频,会使得图像模糊。低频信息保留下来了,高频信息没了,图像边界会变得模糊了。

① opencv 中主要就是 cv2.dft() 执行傅里叶变换到频域中 和 cv2.idft() 执行逆傅里叶变换,输入图像需要先转换成 np.float32 格式。

② 得到的结果中频率为 0 的部分会在左上角,通常要转换到中心位置,可以通过 shift 变换来实现。

③ cv2.dft() 返回的结果是双通道的 ( 实部,虚部 ),通常还需要转换成图像格式才能展示(0,255)像素值。

7.3.3、傅里叶变换

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('01_Picture/13_Lena.jpg',0)
img_float = np.float32(img) # 输入图片转换成 np.float32 格式
dft = cv2.dft(img_float, flags = cv2.DFT_COMPLEX_OUTPUT) # 傅里叶变换
dft_shift = np.fft.fftshift(dft) # 将低频值,频率为 0 的部分转换到中间的位置

# 得到灰度图能表示的形式
magnitude_spectrum = 20 * np.log(cv2.magnitude(dft_shift[:,:,0],dft_shift[:,:,1])) # 对两个通道进行转换才能得到图像形式表达,由于转换后的值为非常小的数值,因此还要转换到 0-255 之间        
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('Magnitude Spectrum'), plt.xticks([]), plt.yticks([]) # 越往中心频率越低(被 shift 拉到中间),越往两侧频率越高
plt.show()

download

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('01_Picture/13_Lena.jpg',0)
img_float32 = np.float32(img)

# DFT ( 傅里叶变换 )
dft = cv2.dft(img_float32, flags = cv2.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

rows, cols = img.shape
crow, ccol = int(rows/2), int(cols/2) # 中心位置

# 低通滤波
mask = np.zeros((rows,cols,2),np.uint8) # 全是0的全部保留
mask[crow-30:crow+30,ccol-30:ccol+30] = 1 # 只保留中心点周围的区域,中心点为最低频的

# IDPT (傅里叶逆变换)
fshift = dft_shift * mask # 用掩码提取 dft_shift 中相应区域,是 1 就保留,不是 1 就过滤了
f_ishift = np.fft.ifftshift(fshift) # 把拉到中心位置的频谱区域给还原回去,依旧回到左上角
img_back = cv2.idft(f_ishift)
img_back = cv2.magnitude(img_back[:,:,0],img_back[:,:,1]) # 将实部和虚部结合起来,才能将傅里叶变换的结果显示出来  

plt.subplot(121),plt.imshow(img,cmap='gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img_back,cmap='gray')
plt.title('Result'), plt.xticks([]), plt.yticks([])

download

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('01_Picture/13_Lena.jpg',0)
img_float32 = np.float32(img)

dft = cv2.dft(img_float32,flags=cv2.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

rows, cols = img.shape
crow, ccol = int(rows/2), int(cols/2) # 中心位置

# 高通滤波
mask = np.ones((rows,cols,2),np.uint8)# 全是1的全部保留
mask[crow-30:crow+30,ccol-30:ccol+30] = 0 # 中间区域置 0,外面的区域置 1

# IDFT
fshift = dft_shift * mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv2.idft(f_ishift)
img_back = cv2.magnitude(img_back[:,:,0],img_back[:,:,1])

plt.subplot(121),plt.imshow(img,cmap = 'gray')
plt.title('Input Image'), plt.xticks([]),plt.yticks([])
plt.subplot(122),plt.imshow(img_back,cmap='gray')
plt.title('Result'),plt.xticks([]),plt.yticks([])

download

8、图像特征

8.1、harris角点检测

8.1.1、角点检测原理

① 沿着水平方向、竖直方向,一个变化比较平稳,一个变化比较迅速,那它就是一个边界。

② 无论是沿着水平方向,还是沿着水平方向,变化都比较明显,则它是角点。

download

③ 平移前后的灰度值的变化情况,来判断该像素点是否是角点。

download

download

download

download

download

download

8.1.2、harris角点检测

① harris角点检测函数:cv2.cornerHarris()

  • img:数据类型为 float32 的入图像。
  • blockSize:角点检测中指定区域的大小。
  • ksize:Sobel求导中使用的窗口大小。常用 3。
  • k:取值参数为 [0,04,0.06]。常用 0.04。
import cv2
import numpy as np

img = cv2.imread('01_Picture/17_Chessboard.jpg')    
print('img.shape:',img.shape)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#gray = np.float32(gray)
dst = cv2.cornerHarris(gray,2,3,0.04) # 每个点与对应点的相似性地值,即变化值  
print('dst.shape:',dst.shape)    

img[dst>0.01*dst.max()] = [0,0,255] # 比相似性最大值的百分之一要大,则标注为角点    
cv2.imshow('dst',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

输出结果:
img.shape: (512, 512, 3)
dst.shape: (512, 512)
image-20220715085302257

8.2、sift关键点提取

8.2.1、不同尺度空间

① 在一定的范围内,无论物体是大还是小,人眼都可以分辨出来,然而计算机要有相同的能力却很难,所以要让机器能够对物体在不同尺度下有一个统一的认知,就需要考虑图像在不同的尺度下都存在的特点。

② 尺度空间的获取通常使用高斯模糊来实现。

③ 不同 σ 的高斯函数决定了对图像的平滑程度,越大的 σ 值对应的图像越模糊。

download

download

8.2.2、多分辨率金字塔

① 第一个是要做一个图像金字塔,第二个是金字塔的每层都要做高斯滤波。

download

8.2.3、高斯差分金字塔(DOG)

① 差分结果较大的被视为比较重要的特征。

download

download

8.2.4、DOG空间极值检测

① 为了寻找尺度空间的极值点,每个像素点要和其图像域(同一尺度空间)和尺度域(相邻的尺度空间)的所有相邻点进行比较,当其大于(或者小于)所有相邻点时,该点就是极值点。

② 如下图所示,中间的检测点要和其所在图像的3×3邻域8个像素点,以及其相邻的上下两层的3×3领域18个像素点,共26个像素点进行比较。

download

8.2.5、关键点的精确定位

① 这些候选关键点是 DOG 空间的局部极值点,而且这些极值点均为离散的点,精确定位极值点的一种方法是,对尺度空间 DOG 函数进行曲线拟合,计算其极值点,从而实现关键点的精确定位。

download

download

8.2.6、消除边界响应

download

8.2.7、特征点的主方向

① 每个特征点可以得到三个信息(x,y,σ,θ),即位置、尺度和方向。具有多个方向的关键点可以被复制成多份,然后将方向值分别赋给复制后的特征点,一个特征点就产生了多个坐标、尺度相等,但是方向不同的特征点。

download

8.2.8、生成特征描述

① 在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向。

download

② 为了保证特征矢量的旋转不变性,要以特征点为中心,在附近邻域内将坐标轴旋转θ角度,即将坐标轴旋转为特征点的主方向。

download

③ 旋转之后的主方向为中心取8x8的窗口,求每个像素的梯度幅值和方向,箭头方向代表梯度方向,长度代表梯度幅值,然后利用高斯窗口对其进行加权运算,最后在每个4x4的小块上绘制8个方向的梯度直方图,计算每个梯度方向的累加值,即可形成一个种子点,即每个特征的由4个种子点组成,每个种子点有8个方向的向量信息。

download

④ 论文中建议对每个关键点使用4x4共16个种子点来描述,这样一个关键点就会产生128维的SIFT特征向量。

download

8.2.9、sift特征点检测

import cv2
import numpy as np

img = cv2.imread('01_Picture/18_House.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
print(cv2.__version__)
# 4.6.0

sift = cv2.xfeatures2d.SIFT_create()  # 将 SIFT 算法实例化出来
kp = sift.detect(gray, None) # 把灰度图传进去,得到特征点、关键点

img = cv2.drawKeypoints(gray, kp, img)
cv2.imshow('drawKeypoints', img)
cv2.waitKey(0)
cv2.destroyAllWindows()


kp, des = sift.compute(gray, kp)
print(np.array(kp).shape) # 6827 个关键点
print(des.shape) # 每个关键点有 128 维向量
print(des[0])    # 获得第 0 号关键点的值

输出结果:

(6809,)
(6809, 128)
[  0.   0.   0.   0.   0.   0.   0.   0.  21.   8.   0.   0.   0.   0.
   0.   0. 157.  31.   3.   1.   0.   0.   2.  63.  75.   7.  20.  35.
  32.  74.  23.  66.   0.   0.   1.   3.   4.   1.   0.   0.  76.  15.
  13.  27.   8.   1.   0.   2. 157. 112.  50.  31.   2.   0.   0.   9.
  49.  42. 157. 157.  12.   4.   1.   5.   1.  13.   7.  12.  41.   5.
   0.   0. 104.   8.   5.  19.  53.   5.   1.  21. 157.  55.  35.  90.
  22.   0.   0.  17.   3.   6.  69. 157.  52.   0.   0.   0.   7.  33.
  10.  10.  11.   0.   1.   6.  44.   9.   3.   7.  19.   5.  14.  26.
  38.  28.  32.  92.  16.   2.   3.   4.   0.   0.   7.  92.  23.   0.
   0.   0.]

8.3、特征匹配与全景拼接

8.3.1、Brute-Force蛮力匹配

pip install matplotlib

import cv2
import numpy as np
import matplotlib.pyplot as plt

img1 = cv2.imread('01_Picture/19_Box.png',0)
img2 = cv2.imread('01_Picture/20_Box_in_scene.png',0)

def cv_show(name,img):
    cv2.imshow(name,img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    cv_show('img1',img1)
cv_show('img2',img2)

sift = cv2.xfeatures2d.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

# crossCheck 表示两个特征点要互相匹配,例如 A 中的第 i 个特征点与 B 中第 j 个特征点最近的,并且 B 中第 j 个特征点到 A 中的第 i 个特征点也是最近的。      
# 将两幅图像的特征点、特征向量算出来,用欧氏距离去比较特征向量相似性,一般情况下默认用的是归一化后的欧式距离去做,为了使得结果更均衡些。
# 如果不用 sift 特征计算方法去做,而是用其他特征计算方法需要考虑不同的匹配方式。
bf = cv2.BFMatcher(crossCheck = True)  # cv2.BFMatcher 蛮力匹配缩写

8.3.2、1对1的匹配

matches = bf.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)
img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=2)  # 画前十个点          
cv_show('img3',img3)

image-20220715101901428

8.3.3、k对最佳匹配

① 如果需要更快速完成操作,可以尝试使用 cv2.FlannBasedMatcher。

bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2) # k 参数可选,可以一个点跟它最近的k个点可选         

good = []
for m,n in matches:
    if m.distance < 0.75 * n.distance:  # m.distance 与 n.distance 比值小于 0.75,这是自己设定的过滤条件   
        good.append([m])
        
img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good, None, flags=2)
cv_show('img3',img3)

image-20220715101955929

8.3.4、随机抽样一致算法 (RANSAC)

download

① 选择初始样本点进行拟合,给定一个容忍范围,不断进行迭代。

download

② 每一次拟合后,容差范围内都有对应的数据点数,找出数据点个数最多的情况,就是最终的拟合结果。

download

8.3.5、单应性矩阵

① 一对点 (x',y') 与 (x,y) 只能列两个方程,因此八个未知数需要至少四对点。

② 通过 loss 找出四队最好配对的特征点。

download

8.3.6、全景拼接

import numpy as np
import cv2

class Stitcher:

    #拼接函数
    def stitch(self, images, ratio=0.75, reprojThresh=4.0,showMatches=False):
        #获取输入图片
        (imageB, imageA) = images
        #检测 A、B 图片的 SIFT 关键特征点,并计算特征描述子
        (kpsA, featuresA) = self.detectAndDescribe(imageA)
        (kpsB, featuresB) = self.detectAndDescribe(imageB)

        # 匹配两张图片的所有特征点,返回匹配结果
        M = self.matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh)           

        # 如果返回结果为空,没有匹配成功的特征点,退出算法
        if M is None:
            return None

        # 否则,提取匹配结果
        # H是3x3视角变换矩阵      
        (matches, H, status) = M
        # 将图片A进行视角变换,result是变换后图片
        result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
        self.cv_show('result', result)
        # 将图片B传入result图片最左端
        result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
        self.cv_show('result', result)
        # 检测是否需要显示图片匹配
        if showMatches:
            # 生成匹配图片
            vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches, status)
            # 返回结果
            return (result, vis)

        # 返回匹配结果
        return result
    
    def cv_show(self,name,img):
        cv2.imshow(name, img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    def detectAndDescribe(self, image):
        # 将彩色图片转换成灰度图
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 建立 SIFT 生成器
        descriptor = cv2.xfeatures2d.SIFT_create()
        # 检测 SIFT 特征点,并计算描述子
        (kps, features) = descriptor.detectAndCompute(image, None)

        # 将结果转换成 NumPy 数组
        kps = np.float32([kp.pt for kp in kps])

        # 返回特征点集,及对应的描述特征
        return (kps, features)

    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh):
        # 建立暴力匹配器
        matcher = cv2.BFMatcher()
  
        # 使用 KNN 检测来自 A、B 图的SIFT特征匹配对,K=2
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)

        matches = []
        for m in rawMatches:
            # 当最近距离跟次近距离的比值小于 ratio 值时,保留此匹配对
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
            # 存储两个点在 featuresA, featuresB 中的索引值
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # 当筛选后的匹配对大于 4 时,计算视角变换矩阵
        if len(matches) > 4:
            # 获取匹配对的点坐标
            ptsA = np.float32([kpsA[i] for (_, i) in matches])
            ptsB = np.float32([kpsB[i] for (i, _) in matches])

            # 计算视角变换矩阵
            (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)

            # 返回结果
            return (matches, H, status)

        # 如果匹配对小于4时,返回None
        return None

    def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
        # 初始化可视化图片,将A、B图左右连接到一起
        (hA, wA) = imageA.shape[:2]
        (hB, wB) = imageB.shape[:2]
        vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")
        vis[0:hA, 0:wA] = imageA
        vis[0:hB, wA:] = imageB

        # 联合遍历,画出匹配对
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # 当点对匹配成功时,画到可视化图上
            if s == 1:
                # 画出匹配对
                ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
                ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
                cv2.line(vis, ptA, ptB, (0, 255, 0), 1)

        # 返回可视化结果
        return vis
    
    import numpy as np
import cv2

class Stitcher:

    #拼接函数
    def stitch(self, images, ratio=0.75, reprojThresh=4.0,showMatches=False):
        #获取输入图片
        (imageB, imageA) = images
        #检测 A、B 图片的 SIFT 关键特征点,并计算特征描述子
        (kpsA, featuresA) = self.detectAndDescribe(imageA)
        (kpsB, featuresB) = self.detectAndDescribe(imageB)

        # 匹配两张图片的所有特征点,返回匹配结果
        M = self.matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh)           

        # 如果返回结果为空,没有匹配成功的特征点,退出算法
        if M is None:
            return None

        # 否则,提取匹配结果
        # H是3x3视角变换矩阵      
        (matches, H, status) = M
        # 将图片A进行视角变换,result是变换后图片
        result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
        self.cv_show('result', result)
        # 将图片B传入result图片最左端
        result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
        self.cv_show('result', result)
        # 检测是否需要显示图片匹配
        if showMatches:
            # 生成匹配图片
            vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches, status)
            # 返回结果
            return (result, vis)

        # 返回匹配结果
        return result
    
    def cv_show(self,name,img):
        cv2.imshow(name, img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    def detectAndDescribe(self, image):
        # 将彩色图片转换成灰度图
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 建立 SIFT 生成器
        descriptor = cv2.xfeatures2d.SIFT_create()
        # 检测 SIFT 特征点,并计算描述子
        (kps, features) = descriptor.detectAndCompute(image, None)

        # 将结果转换成 NumPy 数组
        kps = np.float32([kp.pt for kp in kps])

        # 返回特征点集,及对应的描述特征
        return (kps, features)

    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh):
        # 建立暴力匹配器
        matcher = cv2.BFMatcher()
  
        # 使用 KNN 检测来自 A、B 图的SIFT特征匹配对,K=2
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)

        matches = []
        for m in rawMatches:
            # 当最近距离跟次近距离的比值小于 ratio 值时,保留此匹配对
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
            # 存储两个点在 featuresA, featuresB 中的索引值
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # 当筛选后的匹配对大于 4 时,计算视角变换矩阵
        if len(matches) > 4:
            # 获取匹配对的点坐标
            ptsA = np.float32([kpsA[i] for (_, i) in matches])
            ptsB = np.float32([kpsB[i] for (i, _) in matches])

            # 计算视角变换矩阵
            (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)

            # 返回结果
            return (matches, H, status)

        # 如果匹配对小于4时,返回None
        return None

    
    def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
        # 初始化可视化图片,将A、B图左右连接到一起
        (hA, wA) = imageA.shape[:2]
        (hB, wB) = imageB.shape[:2]
        vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")
        vis[0:hA, 0:wA] = imageA
        vis[0:hB, wA:] = imageB

        # 联合遍历,画出匹配对
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # 当点对匹配成功时,画到可视化图上
            if s == 1:
                # 画出匹配对
                ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
                ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
                cv2.line(vis, ptA, ptB, (0, 255, 0), 1)

        # 返回可视化结果
        return vis

image-20220715102332306

image-20220715102410589

image-20220715102455803

8.4、背景建模

8.4.1、 帧差法

① 前景就是一张图像中感兴趣的地方,背景就是一张图像中不太感兴趣的地方。

② 在一个视频中,更感兴趣的东西应该是运动的东西。

③ 由于场景中的目标在运动,目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧图像进行差分运算,不同帧对应的像素点相减,判断灰度差的绝对值,当绝对值超过一定阈值时,即可判断为运动目标,从而实现目标的检测功能。

④ 帧差法非常简单,但是会引入噪音和空洞问题。例如,人的稍微移动,衣服的中间区域移动前后的灰度值没有变化。

download

8.4.2、混合高斯模型

① 在进行前景检测前,先对背景进行训练,对图像中每个背景采用一个混合高斯模型进行模拟,每个背景的混合高斯的个数可以自适应。

② 然后在测试阶段,对新来的像素进行 GMM 匹配,如果该像素值能够匹配其中一个高斯,则认为是背景,否则认为是前景。

③ 由于整个过程 GMM 模型在不断更新学习中,所以对动态背景有一定的鲁棒性。最后通过对一个有树枝摇摆的动态背景进行前景检测,取得了较好的效果。

④ 在视频中对于像素点的变化情况应当是符合高斯分布。

download

⑤ 背景的实际分布应当是多个高斯分布混合在一起,每个高斯模型也可以带有权重。

download

① 混合高斯模型学习方法

① 首先初始化每个高斯模型矩阵参数。

② 取视频中T帧数据图像用来训练高斯混合模型。来了第一个像素之后用它来当做第一个高斯分布。

③ 当后面来的像素值时,与前面已有的高斯的均值比较,如果该像素点的值与其模型均值差在3倍的方差内,则属于该分布,并对其进行参数更新。

④ 如果下一次来的像素不满足当前高斯分布,用它来创建一个新的高斯分布。

⑤ 默认设置 3-5 个混合的高斯分布模型。

②混合高斯模型测试方法

① 在测试阶段,对新来像素点的值与混合高斯模型中的每一个均值进行比较,如果其差值在2倍的方差之间的话,则认为是背景,否则认为是前景。

② 将前景赋值为255,背景赋值为0。这样就形成了一副前景二值图。

download

import numpy as np
import cv2

# 经典的测试视频
cap = cv2.VideoCapture('02_Video/02_Foreground.avi')

# 形态学操作需要使用
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)
    # 寻找视频中的轮廓
    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(150) & 0xff # 0xff 表示按退出键 ESC 就停止了
    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

image-20220717162039100

8.5、光流估计

8.5.1、光流估计

① 光流是空间运动物体在观测成像平面上的像素运动的 "瞬时速度",根据各个像素点的速度矢量特征,可以对图像进行动态分析,例如目标跟踪。

  • 亮度恒定:同一点随着时间的变化,其亮度不会发生改变。
  • 小运动:随着时间的变化不会引起位置的剧烈变化,只有小运动情况下才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数。
  • 空间一致:一个场景上邻近的点投影到图像上也是邻近点,且邻近点速度一致。因为光流法基本方程约束只有一个,而要求x,y方向的速度,有两个未知变量。所以需要连立n多个方程求解。

download

download

8.5.2、Lucas-Kanade 算法

download

① 如何求解方程组呢?看起来一个像素点根本不够,在物体移动过程中还有哪些特性呢?

② 光流估计中需要让 𝐴𝑇𝐴ATA 可逆,那么需要让 𝐴𝑇𝐴ATA 的两个特征值比较大,才可逆。

③ 若让 𝐴𝑇𝐴ATA 的两个特征值比较大,则需要进行角点检测,角点的特征值比较大。

download

④ 光流估计函数:cv2.calcOpticalFlowPyrLK():

参数:

  • prevImage 前一帧图像
  • nextImage 当前帧图像
  • prevPts 待跟踪的特征点向量
  • winSize 搜索窗口的大小
  • maxLevel 最大的金字塔层数

返回:

  • nextPts 输出跟踪特征点向量
  • status 特征点是否找到,找到的状态为1,未找到的状态为0
import numpy as np
import cv2

cap = cv2.VideoCapture('02_Video/02_Foreground.avi')

# 角点检测所需参数
# 如果不限制角点最大数量,速度就会有些慢,达不到实时的效果
# 品质因子会筛选角点,品质因子设置的越大,得到的角点越少                
feature_params = dict( maxCorners = 100,
                       qualityLevel = 0.3,
                       minDistance = 7 )

# lucas-kanada参数
# winSize:窗口大小 maxLevel:金字塔层数
lk_params = dict( winSize = (15,15), 
                  maxLevel = 2 )

# 随即颜色条
color = np.random.randint(0,255,(100,3))

# 拿到第一帧图像
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)                         

# cv2.goodFeaturesToTrack函数返回所有检测特征点,需要输入:图像,角点最大数量(效率),品质因子(特征值越大的越好来筛选)                                  
# 距离相当于这区间有比这个角点强的,就不要这个弱的了
# **变量 作为传入参数,是用来传入不定长的变量
p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params)  # 拿到第一帧的角点,后面视频中是对第一帧的角点进行追踪     

# 创建一个 mask
mask = np.zeros_like(old_frame)

while(True):
    ret, frame = cap.read()
    frame_gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)                       
    
    # 需要传入前一帧和当前图像以及前一帧检测到的角点
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)                         
    
    # st=1 表示
    good_new = p1[st==1]  # st==1 表示找到的特征点,没找到的特征点就不要了
    good_old = p0[st==1]
    
    for i, (new,old) in enumerate(zip(good_new,good_old)):
        a,b = new.ravel()
        c,d = old.ravel()
        mask = cv2.line(mask,(int(a),int(b)),(int(c),int(d)),color[i].tolist(),2)
        frame = cv2.circle(frame,(int(a),int(b)),5,color[i].tolist(),-1) 
    img = cv2.add(frame,mask)
    
    cv2.imshow('frame',img)
    k = cv2.waitKey(150) & 0xff
    if k == 27:
        break
        
    # 更新
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1,1,2)
    
cv2.destroyAllWindows()
cap.release()

image-20220717162503293

posted @ 2022-08-04 10:34  tuyin  阅读(364)  评论(0)    收藏  举报