Python计算机视觉编程第十章——OpenCV基础知识

(一)OpenCV 的 Python 接口

OpenCV 是一个C++ 库,它包含了计算机视觉领域的很多模块。除了 C++ 和 C, Python 作为一种简洁的脚本语言,在 C++ 代码基础上的 Python 接口得到了越来越广泛的支持

(二)OpenCV 基础知识

2.1 读取和写入图像

载入一张jpg图像,打印出图像大小,对图像进行转换并保存为 png 格式:

# -*- coding: utf-8 -*-
import cv2
# 读取图像
im = cv2.imread('D:\\Python\\chapter10\\empire.jpg')
h,w = im.shape[:2]
print h,w
# 保存图像
cv2.imwrite('D:\\Python\\chapter10\\result.png',im)

控制台输出:


函数 imread() 返回图像为一个标准的 NumPy 数组,并且该函数能够处理很多不同格式的图像

载入一张ppm图像,打印出图像大小,对图像进行转换并保存为 png 格式:

# -*- coding: utf-8 -*-
import cv2
# 读取图像
im = cv2.imread('D:\\Python\\chapter10\\images\\viff.000.ppm')
h,w = im.shape[:2]
print h,w
# 保存图像
cv2.imwrite('D:\\Python\\chapter10\\result3.png',im)

控制台输出:

2.2 颜色空间

在 OpenCV 中,图像不是按传统的 RGB 颜色通道,而是按 BGR 顺序(即 RGB 的倒序)存储的。读取图像时默认的是 BGR。

在读取原图像之后,紧接其后的是 OpenCV 颜色转换代码,其中最有用的一些转换代码如下:

  1. cv2.COLOR_BGR2GRAY (原图像转化为灰度图像)
  2. cv2.COLOR_BGR2RGB (原图像转化为RGB图像)
  3. cv2.COLOR_GRAY2BGR (灰度图像图像转化为BGR图像)

编写代码:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image


# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

# 读取图像
im = imread('D:\\Python\\chapter10\\empire.jpg')
im1 = cv2.imread('D:\\Python\\chapter10\\empire.jpg')
gray = cv2.cvtColor(im1,cv2.COLOR_BGR2GRAY)
BGR = cv2.cvtColor(gray,cv2.COLOR_GRAY2BGR)
figure()
subplot(131)
title(u'(a)原始图像', fontproperties=font)
axis('off')
imshow(im)
subplot(132)
title(u'(b)COLOR_BGR2GRAY转换后图像', fontproperties=font)
axis('off')
imshow(gray)
subplot(133)
title(u'(c)(b)经过COLOR_GRAY2RGB转换后图像', fontproperties=font)
axis('off')
imshow(BGR)
show()

代码运行效果如下:

扩展:
图像处理中有多种色彩空间,例如 RGB、HLS、HSV、HSB、YCrCb、CIE XYZ、CIE Lab等,经常要遇到色彩空间的转化,以便生成mask图等操作.

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image

import matplotlib.pyplot as plt
import cv2

img_BGR = cv2.imread('D:\\Python\\chapter10\\empire.jpg') # BGR
plt.subplot(3,3,1); plt.imshow(img_BGR);plt.axis('off');plt.title('BGR')

img_RGB = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2RGB)
plt.subplot(3,3,2); plt.imshow(img_RGB);plt.axis('off');plt.title('RGB')

img_GRAY = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2GRAY)
plt.subplot(3,3,3); plt.imshow(img_GRAY);plt.axis('off');plt.title('GRAY')

img_HSV = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2HSV)
plt.subplot(3,3,4); plt.imshow(img_HSV);plt.axis('off');plt.title('HSV')

img_YcrCb = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2YCrCb)
plt.subplot(3,3,5); plt.imshow(img_YcrCb);plt.axis('off');plt.title('YcrCb')

img_HLS = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2HLS)
plt.subplot(3,3,6); plt.imshow(img_HLS);plt.axis('off');plt.title('HLS')

img_XYZ = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2XYZ)
plt.subplot(3,3,7); plt.imshow(img_XYZ);plt.axis('off');plt.title('XYZ')

img_LAB = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2LAB)
plt.subplot(3,3,8); plt.imshow(img_LAB);plt.axis('off');plt.title('LAB')

img_YUV = cv2.cvtColor(img_BGR, cv2.COLOR_BGR2YUV)
plt.subplot(3,3,9); plt.imshow(img_YUV);plt.axis('off');plt.title('YUV')
plt.show()

疑问:为什么GRAY格式不是灰色?

代码验证:

print('img_GRAY:',img_GRAY.shape)
print('img_RGB:',img_RGB.shape)

可以看到 GRAY 是单通道的灰度图了,也可以采用 PIL 库可视化一下,看到是灰度图。

img_pil = Image.fromarray(img_GRAY);
img_pil.show()

原因是matplotlib默认以彩色图显示图像,不是RGB三通道的会自行渲染成RGB格式,可以在参数里设置:

plt.imshow(img_GRAY,cmap=plt.cm.gray);

2.3 显示图像及结果

从文件中读取一幅图像,并创建一个整数图像表示:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image


# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

# 读取图像
im = cv2.imread('D:\\Python\\chapter10\\fisherman.jpg')
gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
# 计算积分图像
intim = cv2.integral(gray)
# 归一化并保存
intim = (255.0*intim) / intim.max()

figure()
subplot(121)
title(u'(a)原始图像', fontproperties=font)
axis('off')
imshow(im)
subplot(122)
title(u'(b)归一化后图像', fontproperties=font)
axis('off')
imshow(intim)
show()

读取图像后,将其转化为灰度图像,函数 integral() 创建一幅图像,该图像的每个像素值是原图上方和左边强度值相加后的结果;这对于快速评估特征是一个非常有用的技巧

从一个种子像素进行泛洪填充:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image


# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

# 读取图像
im = cv2.imread('D:\\Python\\chapter10\\fisherman.jpg')
h,w = im.shape[:2]

# 泛洪填充
diff = (6,6,6)
mask = zeros((h+2,w+2),uint8)
im1 = cv2.floodFill(im,mask,(10,10), (255,255,0),diff,diff)

# 在OpenCV 窗口中显示结果
cv2.imshow('flood fill',im)
cv2.waitKey()

# 保存结果
cv2.imwrite('D:\\Python\\chapter10\\result.jpg',im)

对图像应用泛洪填充并在 OpenCV 窗口中显示结果。waitKey() 函数一直处于暂停状态,直到有按键按下,此时窗口才会自动关闭。这里的 floodfill() 函数获取(灰度或彩色)图像、一个掩膜(非零像素区域表明该区域不会被填充)、一 个种子像素以及新的颜色值来代替下限和上限阈值差的泛洪像素。泛洪填充以种子像素为起始,只要能在阈值的差异范围内添加新的像素,泛洪填充就会持续扩展。 不同的阈值差异由元组 (R,G,B) 给出。

用 OpenCV 提取 SURF 特征并画出提取出来的 SURF 特征:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image


# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

# 读取图像
im = cv2.imread('D:\\Python\\chapter10\\fisherman.jpg')

# 下采样
im_lowres = cv2.pyrDown(im)

# 变换成灰度图像
gray = cv2.cvtColor(im_lowres,cv2.COLOR_RGB2GRAY)

# 检测特征点
s = cv2.xfeatures2d.SIFT_create()
mask = uint8(ones(gray.shape))
keypoints = s.detect(gray,mask)

# 显示结果及特征点
vis = cv2.cvtColor(gray,cv2.COLOR_GRAY2BGR)

for k in keypoints[::10]:
    cv2.circle(vis,(int(k.pt[0]),int(k.pt[1])),2,(0,255,0),-1)
    cv2.circle(vis,(int(k.pt[0]),int(k.pt[1])),int(k.size),(0,255,0),2)
cv2.imshow('local descriptors', vis)
cv2.waitKey()

读取图像后,如果没有给定新尺寸,则用 pyrDown() 进行下采样,创建一个尺寸为原 图像大小一半的新图像,然后将该图像转换为灰度图像,并传递到一个 SURF 关键点检测对象;掩膜决定了在哪些区域应用关键点检测器。在画出检测结果时,将灰度图像转换成彩色图像,并用绿色通道画出检测到的关键点。在每到第十个关键点时循环一次,并在中心画一个圆环,每一个圆环显示出关键点的尺度(大小)。绘图函数 circle() 获取一幅图像、图像坐标(仅整数坐标)元组、半径、绘图的颜色元组以及线条粗细(-1 是实线圆环)。上图显示了提取出来的 SURF 特征。

遇到问题:

AttributeError: 'module' object has no attribute 'SURF'

问题解决:
修改

s = cv2.SURF()

s = cv2.xfeatures2d.SURF_create()

如果继续报错

AttributeError: module ‘cv2.cv2’ has no attribute ‘xfeatures2d’

需降低opencv版本

pip install opencv-contrib-python==3.3.0.10

OpenCV基础知识扩展
(1)图像平移:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 读取图像
img = cv2.imread('D:\\Python\\chapter10\\road.jpg')
pr基础int(img.shape)
# 定义平移矩阵
M = np.float32([[1, 0, 500],
                [0, 1, 100]])
# 变换后的大小
rows, cols = img.shape[:2]
# 平移
translation = cv2.warpAffine(img, M, (cols, rows)) 
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(translation)
plt.show()

(2)图像缩放

# -*- coding: utf-8 -*-
import cv2
import matplotlib.pyplot as plt
from pylab import  *
from PIL import Image
import sys

img = imread('D:\\Python\\chapter10\\jimei.jpg')
# 插值:interpolation
# None本应该是放图像大小的位置的,后面设置了缩放比例,
# 所有就不要了
res1 = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
# 直接规定缩放大小,这个时候就不需要缩放因子
height, width = img.shape[:2]
res2 = cv2.resize(img, (2 * width, 2 * height), interpolation=cv2.INTER_CUBIC)
plt.subplot(131)
plt.imshow(img)
plt.subplot(132)
plt.imshow(res1)
plt.show()

(3)图像旋转

# -*- coding: utf-8 -*-
import cv2
import matplotlib.pyplot as plt
from pylab import  *
from PIL import Image
import sys

img = imread('D:\\Python\\chapter10\\dog2.jpg')
rows, cols = img.shape[:2]
# 第一个参数旋转中心,第二个参数旋转角度,第三个参数:缩放比例
M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 1)
# 第三个参数:变换后的图像大小
res = cv2.warpAffine(img, M, (cols, rows))

plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(res)
plt.show()

(4)图像翻转

# -*- coding: utf-8 -*-
import cv2
import matplotlib.pyplot as plt
from pylab import  *
from PIL import Image
import sys
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=10)
img = imread('D:\\Python\\chapter10\\face.jpg')

#横向翻转
res1 = cv2.flip(img,1)
#纵向翻转
res2 = cv2.flip(img,0)
#同时翻转
res3 = cv2.flip(img,-1)
plt.subplot(141)
plt.imshow(img)
title(u'(a)原始图像', fontproperties=font)
plt.subplot(142)
plt.imshow(res1)
title(u'(b)横向翻转', fontproperties=font)
plt.subplot(143)
plt.imshow(res2)
title(u'(c)#纵向翻转', fontproperties=font)
plt.subplot(144)
plt.imshow(res3)
title(u'(d)#同时翻转', fontproperties=font)
plt.show()

(5)图像仿射

# -*- coding: utf-8 -*-
import cv2
import matplotlib.pyplot as plt
from pylab import  *
from PIL import Image
import sys
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=10)
img = imread('D:\\Python\\chapter10\\sun.jpg')

rows, cols = img.shape[:2]
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(pts1, pts2)
# 第三个参数:变换后的图像大小
res = cv2.warpAffine(img, M, (cols, rows))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(res)
plt.show()

(6)图像透射

# -*- coding: utf-8 -*-
import cv2
import matplotlib.pyplot as plt
from pylab import  *
from PIL import Image
import sys
# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=10)
img = imread('D:\\Python\\chapter10\\flower4.jpg')
rows,cols = img.shape[:2]
pts1 = np.float32([[56,65],[238,52],[28,237],[239,240]])
pts2 = np.float32([[0,0],[200,0],[0,200],[200,200]])
M = cv2.getPerspectiveTransform(pts1,pts2)
res = cv2.warpPerspective(img,M,(200,150))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(res)
plt.show()

(7)画图

# -*- coding: utf-8 -*-
import cv2
import numpy as np
#生成一个空的黑底图像
img=np.zeros((512,512,3), np.uint8)#np.zeros()有两个参数,一个是创建的图片矩阵大小,另一个是数据类型,512,512是像素(第一个512像素高,第二个是512像素宽),3指BGR三种颜色uint8是用0-255表示所有颜色。
#画线
cv2.line(img,(0,0),(511,511),(255,228,225),5)  #img图像名称,起点坐标,终点坐标,(255,228,225)是颜色,5是线的宽度
#画矩形
cv2.rectangle(img,(384,0),(510,128),(0,255,127),3) #img图像名称,左上顶点坐标,右下顶点坐标,(0,255,127)是颜色,线宽为3
cv2.rectangle(img,(0,0),(126,128),(0,255,127),3)
#画圆
cv2.circle(img,(447,63), 63, (255,181,197), -1)  #图像名称,圆心坐标,半径63,(0,0,255)颜色,线宽为-1,当线宽-1时表示封闭图形的颜色填充。
#画圆
cv2.circle(img,(63,63), 63, (255,181,197), -1)
#画椭圆
cv2.ellipse(img,(256,256),(100,50),0,0,180,(144,238,144),-1)  #图像名称,中心点坐标,长轴长度,短轴长度,旋转角度,图像出现的部分(长轴顺时针方向起始的角度和结束角度)0,180是下半个椭圆,颜色数组,线宽
#画多边形
pts = np.array([[240,240],[200,245],[235,255],[300,250]], np.int32)  #图像名称,顶点列表(这个多边形在array中有四个顶点),True表示闭合,(0,255,255)是黄色,3是线宽
pts = pts.reshape((-1,1,2))
img = cv2.polylines(img,[pts],True,(0,255,255),3)
#添加文字
font=cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img,'OpenCV',(10,500), font, 4,(0,255,255),2)#图像名称,字符串,坐标,字体,字号,(0,255,255)是黄色、线宽2

winname = 'img'
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.waitKey(0)
cv2.destroyWindow(winname)

(8)单通道与多通道转换

# -*- coding: utf-8 -*-
import cv2
import argparse
import numpy as np
from pylab import  *
from PIL import Image

# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
img1 = cv2.imread("D:\\Python\\chapter10\\flower3.jpg")
(B, G, R) = cv2.split(img1)#将一个多通道数组分离成几个单通道数组

figure()
subplot(131)
title(u'(a)R通道图像', fontproperties=font)
axis('off')
imshow(R)
subplot(132)
title(u'(b)G通道图像', fontproperties=font)
axis('off')
imshow(G)
subplot(133)
title(u'(c)B通道图像', fontproperties=font)
axis('off')
imshow(B)
show()

分离出来的各个通道并不是RGB本身的颜色的,彩色图像都是三个通道的。要想得到有颜色需要扩展通道。

(9)扩展通道

# -*- coding: utf-8 -*-
import cv2
import argparse
import numpy as np
from pylab import  *
from PIL import Image

# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)
import cv2
import argparse
import numpy as np

img1 = cv2.imread("D:\\Python\\chapter10\\flower3.jpg")
(B, G, R) = cv2.split(img1)

# 通道扩展
zeros = np.zeros(img1.shape[:2], np.uint8)
img_R = cv2.merge([R, zeros, zeros])
img_G = cv2.merge([zeros, G, zeros])
img_B = cv2.merge([zeros, zeros, B])


figure()
subplot(131)
title(u'(a)R通道图像', fontproperties=font)
axis('off')
imshow(img_R)
subplot(132)
title(u'(b)G通道图像', fontproperties=font)
axis('off')
imshow(img_G)
subplot(133)
title(u'(c)B通道图像', fontproperties=font)
axis('off')
imshow(img_B)
show()

(10)像素的逻辑运算

# -*- coding: utf-8 -*-
import cv2
import argparse
import numpy as np
from pylab import  *
from PIL import Image
import matplotlib.pyplot as plt

# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

src1 = cv2.imread('D:\\Python\\chapter10\\sun.jpg')
src2 = cv2.imread('D:\\Python\\chapter10\\result3.jpg')
#像素的逻辑运算(与、或、非)   两张图片必须shape一致
#与运算  每个像素点每个通道的值按位与
dst1 = cv2.bitwise_and(src1, src2)
#或运算   每个像素点每个通道的值按位或
dst2 = cv2.bitwise_or(src1, src2)
#非运算   每个像素点每个通道的值按位取反
dst3 = cv2.bitwise_not(src1)
figure()
subplot(231)
title(u'(a)原图像src1', fontproperties=font)
axis('off')
plt.imshow(src1)
subplot(232)
title(u'(b)原图像src2', fontproperties=font)
axis('off')
plt.imshow(src2)
subplot(233)
title(u'(c)与运算', fontproperties=font)
axis('off')
plt.imshow(dst1)
subplot(234)
title(u'(d)或运算', fontproperties=font)
axis('off')
plt.imshow(dst2)
subplot(235)
title(u'(e)非运算', fontproperties=font)
axis('off')
plt.imshow(dst3)
show()

(11)像素的算术运算

# -*- coding: utf-8 -*-
import cv2
import argparse
import numpy as np
from pylab import  *
from PIL import Image
import matplotlib.pyplot as plt

# 添加中文字体支持
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\SimSun.ttc", size=14)

src1 = cv2.imread('D:\\Python\\chapter10\\sun.jpg')
src2 = cv2.imread('D:\\Python\\chapter10\\opencv.jpg')
#像素的加运算
dst1 = cv2.add(src1, src2)
#像素的减运算
dst2 = cv2.subtract(src1, src2)
#像素的除运算
dst3 = cv2.divide(src1, src2)
#像素的乘运算
dst4 = cv2.multiply(src1, src2)
figure()
subplot(231)
title(u'(a)原图像src1', fontproperties=font)
axis('off')
plt.imshow(src1)
subplot(232)
title(u'(b)原图像src2', fontproperties=font)
axis('off')
plt.imshow(src2)
subplot(233)
title(u'(c)像素的加运算', fontproperties=font)
axis('off')
plt.imshow(dst1)
subplot(234)
title(u'(d)像素的减运算', fontproperties=font)
axis('off')
plt.imshow(dst2)
subplot(235)
title(u'(e)像素的除法运算', fontproperties=font)
axis('off')
plt.imshow(dst3)
subplot(236)
title(u'(f)像素的乘法运算', fontproperties=font)
axis('off')
plt.imshow(dst4)
show()

(12)调节图片对比度和亮度

# -*- coding: utf-8 -*-
import cv2
import argparse
import numpy as np
from pylab import  *
from PIL import Image

src = cv2.imread('D:\\Python\\chapter10\\face.jpg')
def contrast_brightness_image(img1, ratio, b):    #第2个参数rario为对比度  第3个参数b为亮度
    h, w, ch = img1.shape
    img2 = np.zeros([h, w, ch], img1.dtype)    # 新建的一张全黑图片和img1图片shape类型一样,元素类型也一样
    dst = cv2.addWeighted(img1, ratio, img2, 1 - ratio, b)
    cv2.imshow("after", dst)
cv2.imshow("begin", src)
contrast_brightness_image(src, 1, 100)
cv2.waitKey(0)
cv2.destroyAllWindows()

(三)处理视频

单纯使用 Python 来处理视频有些困难,因为需要考虑速度、编解码器、摄像机、操作系统和文件格式。目前还没有针对 Python 的视频库,使用 OpenCV 的 Python 接口是唯一不错的选择。

3.1 视频输入

OpenCV 能够很好地支持从摄像头读取视频。下面给出了一个捕获视频帧并在 OpenCV 窗口中显示这些视频帧的完整例子:

# -*- coding: utf-8 -*-
import cv2
# 设置视频捕获
cap = cv2.VideoCapture(0)
while True:
    ret,im = cap.read()
    cv2.imshow('video test',im)
    key = cv2.waitKey(10)
    if key == 27:
        break
    if key == ord(' '):
        cv2.imwrite('D:\\Python\\chapter10\\vid_result.jpg',im)

捕获对象 VideoCapture 从摄像头或文件捕获视频。通过一个整数进行初始化,该整数为视频设备的 id;如果仅有一个摄像头与计算机相连接,那么该摄像头的 id 为 0。read() 方法解码并返回下一视频帧,第一个变量 ret 是一个判断视频帧是否 成功读入的标志,第二个变量im则是实际读入的图像数组。函数 waitKey() 等待用户 按键:如果按下的是 Esc 键(ASCII 码是 27)键,则退出应用;如果按下的是空格键,就保存该视频帧

拓展上面的例子,将摄像头捕获的数据作为输入,并在 OpenCV 窗口中实时显示经模糊的(彩色)图像:

# -*- coding: utf-8 -*-
import cv2
# 设置视频捕获
cap = cv2.VideoCapture(0)
while True:
    ret,im = cap.read()
    blur = cv2.GaussianBlur(im, (0, 0), 5)
    cv2.imshow('camera blur', blur)
    if cv2.waitKey(10) == 27:
        break

每一视频帧都会被传递给 GaussianBlur() 函数,该函数会用高斯滤波器对传入的该帧图像进行滤波。这里,我们传递的是彩色图像,所以 Gaussian Blur() 函数会录入对彩色图像的每一个通道单独进行模糊。该函数需要为高斯函数设定滤波器尺寸 (保存在元组中)及标准差;在本例中标准差设为 5。如果该滤波器尺寸设为 0,则它由标准差自动决定,显示出的结果与上图相似。

3.2 将视频读取到 NumPy 数组中

使用 OpenCV 可以从一个文件读取视频帧,并将其转换成 NumPy 数组。下面是一个从摄像头捕获视频并将视频帧存储在一个 NumPy 数组中的例子:

# -*- coding: utf-8 -*-
import cv2
import numpy as np
# 设置视频捕获
cap = cv2.VideoCapture(0)
frames = []
# 获取帧,存储到数组中
while True:
    ret,im = cap.read()
    cv2.imshow('video',im)
    frames.append(im)
    if cv2.waitKey(10) == 27:
        break
frames = array(frames)
# 检查尺寸
print im.shape
print frames.shape

在这里插入图片描述
上述代码将每一视频帧数组添加到列表末,直到捕获结束。最终得到的数组会有帧数、帧高、帧宽及颜色通道数(3 个),打印出的结果如上。

(四)跟踪

4.1 光流

光流是目标、场景或摄像机在连续两帧图像间运动时造成的目标的运动。它是图像在平移过程中的二维矢量场。

光流法主要依赖于三个假设。
(1) 亮度恒定 (图像中目标的像素强度在连续帧之间不会发生变化)。
(2) 时间规律 (相邻帧之间的时间足够短,以至于在考虑运行变化时可以忽略它们之 间的差异。该假设用于导出下面的核心方程)。
(3) 空间一致性 (相邻像素具有相似的运动)

在很多情况下这些假设并不成立,但是对于相邻帧间的小运动以及短时间跳跃,它还是一个非常好的模型。

假设一个目标像素在 t t 时刻亮度为 I ( x , y , t ) I(x,y,t) ,在 t + δ t t+δt 时刻运动 [ δ x , δ y ] [δx,δy] 后与 t t 时刻具有相同的亮度,即 I ( x , y , t ) = I ( x + δ x , y + δ y , t + δ t ) I(x,y,t)=I(x+δx, y+δy, t+δt) . 对该约束用泰勒公式进行一阶展开并关于 t t 求偏导可以得到光流方程:

I T v = I t \triangledown I^{T}v=-I_{t}

v = [ u , v ] v=[u,v] 是运动矢量, I t It 是时间偏导。对于图像中那些单个的点,该方程是线性方程组。由于 v 包含两个未知变量,所以该方程是不可解的。通过强制加入空间一致性约束,则有可能获得该方程的解。

# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pylab import  *
from PIL import Image
def draw_flow(im,flow,step=16):
    """ 在间隔分开的像素采样点处绘制光流 """
    h,w = im.shape[:2]
    y,x = mgrid[step/2:h:step,step/2:w:step].reshape(2,-1)
    fx, fy = flow[y, x].T
    # 创建线的终点
    lines = vstack([x,y,x+fx,y+fy]).T.reshape(-1,2,2)
    lines = int32(lines)
    # 创建图像并绘制
    vis = cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)
    for (x1, y1), (x2, y2) in lines:
      cv2.line(vis, (x1, y1), (x2, y2), (0, 255, 0), 1)
      cv2.circle(vis, (x1, y1), 1, (0, 255, 0), -1)
    return vis
  #设置视频捕获
cap = cv2.VideoCapture(0)
ret,im = cap.read()
prev_gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
while True:
      # 获取灰度图像
      ret,im = cap.read()
      gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
      # 计算流
      flow = cv2.calcOpticalFlowFarneback(prev_gray,gray,None,0.5,3,15,3,5,1.2,0)
      prev_gray = gray
      # 画出流矢量
      cv2.imshow('Optical flow',draw_flow(gray,flow))
      if cv2.waitKey(10) == 27:
        break

分析:
利用摄像头捕获图像,并对每个连续图像对进行光流估计。由 calcOpticalFlowFarneback() 返回的运动光流矢量保存在双通道图像变量 flow 中。辅助函数 draw_flow() 会在图像均匀间隔的点处绘制光流矢量, 它利用 OpenCV 的绘图函数 line() 和 circle(),并用变量 step 控制流样本的间距。 最终的结果上图所示:圆环网格表示流样本的位置,带有线条的流矢量显示了每个样本点是怎样运动的。

4.2 Lucas-Kanade算法

跟踪最基本的形式是跟随感兴趣点,比如角点。Lucas-Kanade 跟踪算法可以应用于任何一种特征,不过通常使用一些角点,角点是结构张量(Harris 矩阵)中有两个较大特征值的那些点,且更小的特征值要大于某个阈值。

如果基于每一个像素考虑,该光流方程组是欠定方程,即每个方程中含很多未知变量。利用相邻像素有相同运动这一假设,对于 n 个相邻像素,可以将这些方程写成如下系统方程:

[ I T ( x 1 ) I T ( x 2 ) . . . I T ( x n ) ] v = [ I x ( x 1 ) I y ( x 1 ) I x ( x 2 ) I y ( x 2 ) . . . . . . I x ( x n ) I y ( x n ) ] [ u v ] = [ I t ( x 1 ) I t ( x 2 ) . . . I t ( x n ) ] \begin{bmatrix} \triangledown I^{T}(x_{1})\\ \triangledown I^{T}(x_{2})\\ . \\ . \\ . \\\triangledown I^{T}(x_{n}) \end{bmatrix}v=\begin{bmatrix} I_{x}(x_{1}) &I_{y}(x_{1}) \\ I_{x}(x_{2}) & I_{y}(x_{2})\\ .&. \\ . & .\\ .&. \\ I_{x}(x_{n})& I_{y}(x_{n}) \end{bmatrix}\begin{bmatrix} u\\ v \end{bmatrix}=-\begin{bmatrix} I_{t}(x_{1})\\ I_{t}(x_{2})\\ .\\ .\\ .\\ I_{t}(x_{n})\end{bmatrix}

该系统方程的优势是,现在方程的数目多于未知变量,并且可以用最小二乘法解出该系统方程。对于周围像素的贡献可以进行加权处理,使越远的像素影响越小;高斯权重是一种最常见的选择。可以得出以下关系:
M I ˉ v = [ I t ( x 1 ) I t ( x 2 ) . . . I t ( x n ) ] \bar{M_{I}}v=-\begin{bmatrix} I_{t}(x_{1})\\ I_{t}(x_{2})\\ .\\ .\\ .\\ I_{t}(x_{n})\end{bmatrix}

简记为 A v = b Av=b
该超定方程组可以用最小二乘法求解,得出运动矢量:
v = ( A T A ) 1 A T b v=(A^{T}A)^{-1}A^{T}b

v 只有在 A T A A^TA 可逆时才是可解的,这就是 Lucas-Kanade 跟踪算法运行矢量怎样计算出来的全过程。

标准的 Lucas-Kanade 跟踪适用于小位移;为了能够处理较大位移,需要采用分层的方法。在该情形下,光流可以通过对图像由粗到精采样计算得到。这就是 OpenCV 函数 calcOpticalFlowPyrLK() 要做的事。

这些 Lucas-Kanade 函数包含在 OpenCV 中,建立一个 Python 跟踪类。创建名为 lktrack.py 的文件,向其添加下面的类和构造函数:

# -*- coding: utf-8 -*-
from numpy import *
import cv2


# 一些常数及默认参数
lk_params = dict(winSize=(15,15),maxLevel=2,
                criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,0.03)) 

subpix_params = dict(zeroZone=(-1,-1),winSize=(10,10),
                     criteria = (cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS,20,0.03))

feature_params = dict(maxCorners=500,qualityLevel=0.01,minDistance=10)


class LKTracker(object):
    """  用金字塔光流 Lucas-Kanade 跟踪类"""

    def __init__(self,imnames):
        """  使用图像名称列表初始化 """

        self.imnames = imnames
        self.features = []
        self.tracks = []
        self.current_frame = 0

    def step(self,framenbr=None):
        """   移到下一帧。如果没有给定参数,直接移到下一帧  """

        if framenbr is None:
            self.current_frame = (self.current_frame + 1) % len(self.imnames)
        else:
            self.current_frame = framenbr % len(self.imnames)

    def detect_points(self):
        """  利用子像素精确度在当前帧中检测“利于跟踪的好的特征”( 角点 ) """

        # 载入图像并创建灰度图像
        self.image = cv2.imread(self.imnames[self.current_frame])
        self.gray = cv2.cvtColor(self.image,cv2.COLOR_BGR2GRAY)

        #  搜索好的特征点
        features = cv2.goodFeaturesToTrack(self.gray, **feature_params)

        #  提炼角点位置
        cv2.cornerSubPix(self.gray,features, **subpix_params)
        
        self.features = features
        self.tracks = [[p] for p in features.reshape((-1,2))]
        
        self.prev_gray = self.gray

    def track_points(self):
        """ 跟踪检测到的特征 """

        if self.features != []:
            self.step() # 移到下一帧

            
            #  载入图像并创建灰度图像
            self.image = cv2.imread(self.imnames[self.current_frame])
            self.gray = cv2.cvtColor(self.image,cv2.COLOR_BGR2GRAY)
            
            #reshape() 操作,以适应输入格式
            tmp = float32(self.features).reshape(-1, 1, 2)
            
            # 计算光流
            features,status,track_error = cv2.calcOpticalFlowPyrLK(self.prev_gray,self.gray,tmp,None,**lk_params)

            # 去除丢失的点
            self.features = [p for (st,p) in zip(status,features) if st]
            
            #  从丢失的点清楚跟踪轨迹
            features = array(features).reshape((-1,2))
            for i,f in enumerate(features):
                self.tracks[i].append(f)
            ndx = [i for (i,st) in enumerate(status) if not st]
            ndx.reverse() # 从后面移除
            for i in ndx:
                self.tracks.pop(i)
            
            self.prev_gray = self.gray
            
    def track(self):
        """  发生器,用于遍历整个序列 """

        for i in range(len(self.imnames)):
            if self.features == []:
                self.detect_points()
            else:
                self.track_points()
            
            #创建一份 RGB 副本
            f = array(self.features).reshape(-1,2)
            im = cv2.cvtColor(self.image,cv2.COLOR_BGR2RGB)
            yield im,f

    def draw(self):
        """  用 OpenCV 自带的画图函数画出当前图像及跟踪点,按任意键关闭窗口 """

        # 用绿色圆圈画出跟踪点
        for point in self.features:
            cv2.circle(self.image,(int(point[0][0]),int(point[0][1])),3,(0,255,0),-1)
        
        cv2.imshow('LKtrack',self.image)
        cv2.waitKey()

现在,我们用 OpenCV 函数实现了一个完整独立的跟踪系统。

1. 使用跟踪器
将该跟踪类用于真实的场景中。下面的脚本初始化一个跟踪对象,对视频序列进行角点检测、跟踪,并画出跟踪结果:

# -*- coding: utf-8 -*-
import lktrack
imnames = ['D:\\Python\\chapter10\\bt.003.pgm', 'D:\\Python\\chapter10\\bt.002.pgm', 'D:\\Python\\chapter10\\bt.001.pgm', 'D:\\Python\\chapter10\\bt.000.pgm']
# 创建跟踪对象
lkt = lktrack.LKTracker(imnames)
# 在第一帧进行检测,跟踪剩下的帧
lkt.detect_points()
lkt.draw()
for i in range(len(imnames)-1):
    lkt.track_points()
    lkt.draw()



每次画出一帧,并显示当前跟踪到的点,按任意键会转移到序列的下一帧。上图显示了牛津corridor 序列的前 4 幅图像的跟踪结果。

2. 使用发生器
将下面的方法添加到 LKTracker 类:

 def track(self):
        """  发生器,用于遍历整个序列 """

        for i in range(len(self.imnames)):
            if self.features == []:
                self.detect_points()
            else:
                self.track_points()
            
            #创建一份 RGB 副本
            f = array(self.features).reshape(-1,2)
            im = cv2.cvtColor(self.image,cv2.COLOR_BGR2RGB)
            yield im,f

上面的方法创建一个发生器,可以使遍历整个序列并将获得的跟踪点和这些图像以 RGB 数组保存,以方便画出跟踪结果。

还是将它应用于牛津corridor 序列,并画出这些点及这些点的跟踪结果,代码如下:

# -*- coding: utf-8 -*-
import lktrack
import cv2
import numpy as np
from pylab import  *
from PIL import Image
imnames = ['D:\\Python\\chapter10\\bt.003.pgm', 'D:\\Python\\chapter10\\bt.002.pgm', 'D:\\Python\\chapter10\\bt.001.pgm', 'D:\\Python\\chapter10\\bt.000.pgm']
# 用 LKTracker 发生器进行跟踪
lkt = lktrack.LKTracker(imnames)
for im,ft in lkt.track():
    print 'tracking %d features' % len(ft)
# 画出轨迹
figure()
imshow(im)
for p in ft:
    plot(p[0],p[1],'bo')
for t in lkt.tracks:
    plot([p[0] for p in t],[p[1] for p in t])
axis('off')
show()

上图显示了跟踪点的轨迹。

(五)更多示例

5.1 图像修复

对图像丢失或损坏的部分进行重建的过程叫做修复,既包括以复原为目的的对图像丢失数据或损坏部分进行恢复的算法,也包括在照片编辑应用程序中去除红眼或物体的算法。

采用cv2模块中的图像修补inpaint,主要提供了两种进行图像修补的算法:

  1. 基于快速行进算法cv2.INPAINT_TELEA
    (考虑图像中要修复的区域。算法从该区域的边界开始,然后进入区域内,逐渐填充边界中的所有内容。它需要在邻近的像素周围的一个小邻域进行修复。该像素由邻居中所有已知像素的归一化加权和代替。选择权重是一个重要的问题。对于靠近该点的那些像素,靠近边界的法线和位于边界轮廓上的像素,给予更多的权重。一旦像素被修复,它将使用快速行进方法移动到下一个最近的像素。 FMM确保首先修复已知像素附近的像素,这样它就像手动启发式操作一样工作。使用标志cv2.INPAINT_TELEA启用此算法。)
  2. 基于流体动力学并使用了偏微分方程cv2.INPAINT_NS
    (基本原则是heurisitic。它首先沿着已知区域的边缘行进到未知区域(因为边缘是连续的)。它继续等照片(连接具有相同强度的点的线,就像轮廓连接具有相同高度的点一样),同时在修复区域的边界处匹配渐变矢量。为此,使用来自流体动力学的一些方法。获得颜色后,填充颜色以减少该区域的最小差异。使用标志cv2.INPAINT_NS启用此算法。)

库函数:

dst = cv2.inpaint(src,mask,dst, inpaintRadius,flags)

src:输入8位1通道或3通道图像。
inpaintMask:修复掩码,8位1通道图像。非零像素表示需要修复的区域。
dst:输出与src具有相同大小和类型的图像。
inpaintRadius:算法考虑的每个点的圆形邻域的半径。
flags
INPAINT_NS基于Navier-Stokes的方法
Alexandru Telea的INPAINT_TELEA方法

# -*- coding: utf-8 -*-
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('D:\\Python\\chapter10\\54.png')
mask = cv2.imread('D:\\Python\\chapter10\\55.png',0)

dst_TELEA = cv2.inpaint(img,mask,3,cv2.INPAINT_TELEA)
dst_NS = cv2.inpaint(img,mask,3,cv2.INPAINT_NS)

plt.subplot(221), plt.imshow(img)
plt.title('degraded image')
plt.subplot(222), plt.imshow(mask, 'gray')
plt.title('mask image')
plt.subplot(223), plt.imshow(dst_TELEA)
plt.title('TELEA')
plt.subplot(224), plt.imshow(dst_NS)
plt.title('NS')

plt.tight_layout()
plt.show()

观察发现,两种算法的修复效果几乎没有任何区别。

5.2 利用分水岭变换进行分割

在地理学中,分水岭是一个山脊,该山脊通过不同的水系来区分排水区域。集水盆地是把水排入河流或水库的地理区域。分水岭变换把这些概念应用到灰度图像处理中,从而解决许多图像分割问题。

理解分水岭变换要求我们把灰度图像视为一个拓扑表面,表面中f(x, y)的值被解释为高度。例如, 我们可以把下图(a)中的简单图像形象化为下图(b)中的三维表面。如果雨水降落到该表面上,那么雨水明显会流人两个集水盆地中。正好降落到分水岭脊线上的雨水会等概率地流到两个集水盆地中。 分水岭变换将找到灰度图像中的集水盆地和脊线。在解决图像分割问题方面,关键概念是把起始图像变为另一幅图像,在变换后的图像中,集水盆地就是我们要识别的物体或区域。

OpenCV 采用了基于掩模的分水岭算法,在这种算法中我们要设置那些山谷点会汇合,那些不会。这是一种交互式的图像分割。我们要做的就是给我们已知的对象打上不同的标签。如果某个区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。如果某个区域肯定不是对象而是背景就使用另外一个颜色标签标记。而剩下的不能确定是前景还是背景的区域就用 0 标记。这就是我们的标签。然后实施分水岭算法。 每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为 -1。

代码验证:

# -*- coding=GBK -*-
import numpy as np
import cv2
from matplotlib import pyplot as plt

src = cv2.imread('D:\\Python\\chapter10\\coins.png')
img = src.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(
    gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 消除噪声
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# 膨胀
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# 距离变换
dist_transform = cv2.distanceTransform(opening, 1, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)

# 获得未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

# 标记
ret, markers1 = cv2.connectedComponents(sure_fg)

# 确保背景是1不是0
markers = markers1 + 1

# 未知区域标记为0
markers[unknown == 255] = 0

#标签图像将会被修改,边界区域的标记将变为 -1
markers3 = cv2.watershed(img, markers)
img[markers3 == -1] = [0, 0, 255]

plt.subplot(241), plt.imshow(cv2.cvtColor(src, cv2.COLOR_BGR2RGB)),
plt.title('Original'), plt.axis('off')
plt.subplot(242), plt.imshow(thresh, cmap='gray'),
plt.title('Threshold'), plt.axis('off')
plt.subplot(243), plt.imshow(sure_bg, cmap='gray'),
plt.title('Dilate'), plt.axis('off')
plt.subplot(244), plt.imshow(dist_transform, cmap='gray'),
plt.title('Dist Transform'), plt.axis('off')
plt.subplot(245), plt.imshow(sure_fg, cmap='gray'),
plt.title('Threshold'), plt.axis('off')
plt.subplot(246), plt.imshow(unknown, cmap='gray'),
plt.title('Unknow'), plt.axis('off')
plt.subplot(247), plt.imshow(np.abs(markers), cmap='jet'),
plt.title('Markers'), plt.axis('off')
plt.subplot(248), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)),
plt.title('Result'), plt.axis('off')

plt.show()


在这里插入图片描述

5.3 利用霍夫变换检测直线

霍夫变换利用点和线之间的对偶性,将图像空间中直线上离散的像素点通过参数方程映射为霍夫空间的曲线,并将霍夫空间中多条曲线的交点作为直线方程的参数映射为图像空间的直线。数学原理可以查看《OpenCV3编程入门》,7.2.2霍夫线变换。

OpenCV提供了两种用于直线检测的Hough变换形式。其中基本的版本是cv2.HoughLines。其输入一幅含有点集的二值图(由非0像素表示),其中一些点互相联系组成直线。

霍夫变换检测直线:

# -*- coding: utf-8 -*-
# coding=utf-8
import cv2
import numpy as np

img = cv2.imread("D:\\Python\\chapter10\\empire.jpg", 0)

img = cv2.GaussianBlur(img, (3, 3), 0)
edges = cv2.Canny(img, 50, 150, apertureSize=3)
lines = cv2.HoughLines(edges, 1, np.pi / 180, 118)  # 这里对最后一个参数使用了经验型的值
result = img.copy()
for line in lines[0]:
    rho = line[0]  # 第一个元素是距离rho
    theta = line[1]  # 第二个元素是角度theta
    print rho
    print theta
    if (theta < (np.pi / 4.)) or (theta > (3. * np.pi / 4.0)):  # 垂直直线
        # 该直线与第一行的交点
        pt1 = (int(rho / np.cos(theta)), 0)
        # 该直线与最后一行的焦点
        pt2 = (int((rho - result.shape[0] * np.sin(theta)) / np.cos(theta)), result.shape[0])
        # 绘制一条白线
        cv2.line(result, pt1, pt2, (255))
    else:  # 水平直线
        # 该直线与第一列的交点
        pt1 = (0, int(rho / np.sin(theta)))
        # 该直线与最后一列的交点
        pt2 = (result.shape[1], int((rho - result.shape[1] * np.cos(theta)) / np.sin(theta)))
        # 绘制一条直线
        cv2.line(result, pt1, pt2, (255), 1)

cv2.imshow('Canny', edges)
cv2.imshow('Result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

首先使用Canny算子获得图像边缘,然后使用Hough变换检测直线。其中HoughLines函数的参数3和4对应直线搜索的步长。函数将通过步长为1的半径和步长为π/180的角来搜索所有可能的直线。最后一个参数是经过某一点曲线的数量的阈值,超过这个阈值,就表示这个交点所代表的参数对(rho, theta)在原图像中为一条直线。可以观察到第三幅图分别都有一条直线。

发布了39 篇原创文章 · 获赞 51 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Dujing2019/article/details/95306203