饭饭的OpenCV-Python笔记

本文最后更新于:2024年8月14日 下午

饭饭的OpenCV-Python笔记

施工中

最近开始做CV方向的一些研究,OpenCV作为CV中一个很常用的基础库,趁这个机会也学习一下

参考文档: https://docs.opencv.org/4.5.3/

OpenCV简介

OpenCV于1999年由Gary Bradsky在英特尔创立,并于2000年发布第一个版本。 随后Vadim Pisarevsky加入了Gary Bradsky负责管理英特尔的俄罗斯软件OpenCV团队。 2005年,OpenCV被用于Stanley车型,并赢得2005年DARPA挑战。 后来,它在Willow Garage的支持下由Gary Bradsky和Vadim Pisarevsky继续领导该项目积极发展。 OpenCV现在支持与计算机视觉和机器学习相关的众多算法,并且正在日益扩展。 OpenCV支持各种编程语言,如C ++,Python,Java等,可在不同的平台上使用,包括Windows,Linux,OS X(该系统已经更名为macOS),Android和iOS。 基于CUDA和OpenCL的高速GPU操作接口也在积极开发中。
OpenCV-Python是OpenCV的Python API,结合了OpenCV C++ API和Python语言的最佳特性。

输入输出

图像

读取图像

cv.imread(file, mode)

第二个参数(mode)

  • cv.IMREAD_COLOR:默认参数,以彩色模式加载图像,图像的透明度将被忽略。
  • cv.IMREAD_GRAYSCALE:以灰度模式加载图像。
  • cv.IMREAD_UNCHANGED:以alpha通道模式加载图像。

注意:你也可以通过传递1,0,-1来代替上面三个函数功能。

显示图像

cv.imshow(name, img)

函数的第一个参数是窗口的名称,是字符串类型。第二个参数是要加载的图像。你可以显示多个图像窗口,但是每个窗口名称必须不同。

关闭所有窗口:cv.destroyAllWindow()

如果想要关闭特定的窗口,请使用cv.destroyWindow()函数,把要关闭的窗口名称作为参数。

保存图像

cv.imwrite(filename, img)

实例代码

1
2
3
4
5
6
7
8
9
10
import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg',0)
cv.imshow('image',img)
k = cv.waitKey(0)
if k == 27: # wait for ESC key to exit
cv.destroyAllWindows()
elif k == ord('s'): # wait for 's' key to save and exit
cv.imwrite('messigray.png',img)
cv.destroyAllWindows()

OpenCV中的图像也可以直接传入matplotlib进行处理

视频

读取视频

cap = cv.VideoCapture(file/cameraId)

获取帧

ret, frame = cap.read()

read()本质上是一个迭代器,每调用一次会返回下一帧信息

ret返回一个bool值(True / False)。如果读取帧正确,则它将为True。因此,你可以通过值来确定视频的结尾。

frame返回当前帧图像

通过cap.get(cv.CAP_PROP_FRAME_WIDTH)cap.get(cv.CAP_PROP_FRAME_HEIGHT)分别检查帧宽和高度。

播放视频文件

cap.readcv.imshow一起使用即可实现视频播放

cv.waitKey(time/key)是一个键盘事件函数,它的参数以毫秒为单位,该函数在毫秒的时间内去等待键盘事件,如果时间之内有键盘事件触发则程序继续,如果函数参数设置为0,则无限时间的等待键盘事件触发。它也可以设置为检测指定按键的触发,比如等待按键a的触发.

保存视频

首先创建一个VideoWriter对象,然后应该指定FourCC代码并传递每秒帧数(fps)和帧大小。最后一个是isColor标志,如果是True,则每一帧是彩色图像,否则每一帧是灰度图像。 FourCC是用于指定视频编解码器的4字节代码。可以在fourcc.org中找到可用代码列表,它取决于平台。以下编解码器是官方参考:

  • 在Fedora中:DIVX,XVID,MJPG,X264,WMV1,WMV2。(XVID更为可取.MJPG会产生高大小的视频.X264提供非常小的视频)
  • 在Windows中:DIVX(更多要测试和添加)
  • 在OSX中:MJPG(.mp4),DIVX(.avi),X264(.mkv)

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
import cv2 as cv

cap = cv.VideoCapture(0)

# Define the codec and create VideoWriter object
fourcc = cv.VideoWriter_fourcc(*'XVID')
out = cv.VideoWriter('output.avi',fourcc, 20.0, (640,480))

while(cap.isOpened()):
ret, frame = cap.read()
if ret==True:
frame = cv.flip(frame,0)

# write the flipped frame
out.write(frame)

cv.imshow('frame',frame)
if cv.waitKey(1) & 0xFF == ord('q'):
break
else:
break

# Release everything if job is finished
cap.release()
out.release()
cv.destroyAllWindows()

需要注意的是,保存视频时图像的大小必须和VideoWriter的大小相同,不然无法保存,在上述代码中如果读到的视频不为(640,480)将无法获得保存的视频,因此改进代码为

1
2
3
width, height = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH)), int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('output.mp4', fourcc, fps, (width, height))

绘图

  • startendcenterlocation: (x, y)
  • color: (R, G, B)
  • thickness: int(-1时填充内部)

直线

cv.line(img, start, end, color, thickness)

矩形

cv.rectangle(img, start, end, color, thickness)

椭圆

cv.ellipse(img, center, (long, short), angle, startAngle, endAngle, color, thickness)

  • long: 长轴长
  • short: 短轴长
  • angle: 旋转角
  • startAngle, endAngle: 画线范围(0, 180为半圆, 0, 360为整圆)

多边形

cv.polylines(img,[points], alllink(True) , color)

  • alllink: Bool值,表示是否只连接外围点(如果设置为False,则绘制所有点的相连图形而不是闭合图形。)

文字

cv.putText(img, text, location, font, size, color, thickness, cv.LINE_AA)

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import cv2 as cv
# Create a black image
img = np.zeros((512,512,3), np.uint8)
# Draw a diagonal blue line with thickness of 5 px
cv.line(img,(0,0),(511,511),(255,0,0),5)

cv.rectangle(img,(384,0),(510,128),(0,255,0),3)

cv.circle(img,(447,63), 63, (0,0,255), -1)

cv.ellipse(img,(256,256),(100,50),0,0,180,255,-1)

pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
pts = pts.reshape((-1,1,2))
cv.polylines(img,[pts],True,(0,255,255))

font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)

图像处理

像素值

可以通过行和列的坐标值获取该像素点的像素值,对于BGR图像,它返回一个蓝色,绿色,红色值的数组。对于灰度图像,仅返回相应的强度值。

对于单个像素访问,Numpy数组方法array.item()array.itemset()被认为是更好的选择

1
2
3
4
5
print(img[100,100]) # [157 166 200]
img[100,100] = [255,255,255]

img.item(10,10,2) # 59
img.itemset((10,10,2),100)

同时,图像img也和numpy数组一样,支持进行切片操作

1
img[280:340, 330:390]

属性

img.shape可以获取图像的形状。它返回一组行,列和通道的元组(如果图像是彩色的)。

如果图像是灰度图像,则返回的元组仅包含行数和列数,因此这是检查加载的图像是灰度还是彩色的一种很好的方法。

img.size获取的像素总数

img.dtype在调试时非常重要,因为OpenCV-Python代码中的大量错误是由无效的数据类型引起的

1
2
3
print(img.shape) #(342, 548, 3)
print(img.size) #562248
print(img.dtype) #uint8

图像通道的拆分和合并

使用b,g,r = cv.split(img)img = cv.merge((b,g,r))来进行通道的拆分和合并。
但考虑到性能消耗,更推荐使用Numpy索引来进行操作

图像边框

cv.copyMakeBorder(img, top, bottom, left, right, borderType, (color))

它在卷积运算,零填充等方面有更多的应用

  • borderType: 边界的类型
    • cv2.BORDER_CONSTANT - 添加一个固定的彩色边框,还需要下一个参数(value)。
    • cv2.BORDER_REFLECT - 边界元素的镜像。
    • cv2.BORDER_REFLECT_101 or cv2.BORDER_DEFAULT - 跟上面一样,但稍作改动。
    • cv2.BORDER_REPLICATE - 重复最后一个元素。
    • cv2.BORDER_WRAP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

BLUE = [255,0,0]

img1 = cv.imread('opencv-logo.png')

replicate = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REPLICATE)
reflect = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_WRAP)
constant=cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_CONSTANT,value=BLUE)

plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL')
plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE')
plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT')
plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101')
plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT')
plt.show()

border

色彩空间

颜色转换

cv.cvtColor(input_image,flag)

  • flag为转换类型。对于BGR→Gray转换,我们使用cv.COLOR_BGR2GRAY

颜色提取

我们可以将BGR图像转换为HSV,使用HSV色彩空间更方便的提取我们所需要的颜色,下面是方法程序执行步骤:

  • 获取视频中的每一帧
  • 从BGR转换为HSV颜色空间
  • 我们为HSV图像设定一系列的蓝色阈值
  • 单独提取蓝色对象并显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv
import numpy as np

cap = cv.VideoCapture(0)

while(1):

# Take each frame
_, frame = cap.read()

# Convert BGR to HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)

# define range of blue color in HSV
lower_blue = np.array([110,50,50])
upper_blue = np.array([130,255,255])

# Threshold the HSV image to get only blue colors
mask = cv.inRange(hsv, lower_blue, upper_blue)

# Bitwise-AND mask and original image
res = cv.bitwise_and(frame,frame, mask= mask)

cv.imshow('frame',frame)
cv.imshow('mask',mask)
cv.imshow('res',res)
k = cv.waitKey(5) & 0xFF
if k == 27:
break

cv.destroyAllWindows()

frame

查找确定HSV值

你可以使用相同的函数cv.cvtColor()。你只需传递所需的BGR值,而不是传递图像。

1
2
3
green = np.uint8([[[0,255,0 ]]])
hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
print( hsv_green ) # [[[ 60 255 255]]]

几何变换

缩放

cv.resize(img, size, fx, fy, interpolation)

  • size: 缩放后的像素大小(x, y)(不需要设置fx, fy)
  • fx, fy: x, y缩放倍数(不需要设置size)
  • interpolation: 插值方法
    • cv.INTER_AREA: 用于缩小
    • cv.INTER_CUBIC: 用于缩放(慢)
    • cv.INTER_LINEAR: 用于缩放(默认)

平移

cv.warpAffine(img, M, (width,height))

如果你知道像素点(x,y)要位移的距离,让它为变为($t_x$,$t_y$),你可以创建变换矩阵M,如下所示:

$$ M= \begin{bmatrix} 1&0&t_x \ 0&1&t_y \end{bmatrix} $$

下面的示例演示图像像素点整体进行(100,50)位移:

1
2
3
4
5
6
7
8
9
img = cv.imread('messi5.jpg',0)
rows,cols = img.shape

M = np.float32([[1,0,100],[0,1,50]])
dst = cv.warpAffine(img,M,(cols,rows))

cv.imshow('img',dst)
cv.waitKey(0)
cv.destroyAllWindows()

旋转

cv.warpAffine(img, M, (width,height))

变换矩阵M由下式给出:

$$ \begin{bmatrix} \alpha & \beta & \left ( 1-\alpha \right )\cdot center.x-\beta \cdot center.y \ -\beta & \alpha & \beta \cdot center.x\left ( 1-\alpha \right )\cdot center.y \end{bmatrix} $$

为了找到这个转换矩阵,OpenCV提供了一个函数cv.getRotationMatrix2D((cols/2,rows/2), rotation, scale)

以下示例将图像相对于中心旋转90度而不进行任何缩放

1
2
3
4
5
img = cv.imread('messi5.jpg',0)
rows,cols = img.shape

M = cv.getRotationMatrix2D((cols/2,rows/2),90,1)
dst = cv.warpAffine(img,M,(cols,rows))

仿射变换

在仿射变换中,原始图像中的所有平行线在输出图像中依旧平行。为了找到变换矩阵,我们需要从输入图像中得到三个点,以及它们在输出图像中的对应位置。然后cv.getAffineTransform将创建一个2x3矩阵,最后该矩阵将传递给cv.warpAffine

参考以下示例:

1
2
3
4
5
6
7
8
9
img = cv.imread('drawing.png')
rows,cols,ch = img.shape

pts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[10,100],[200,50],[100,250]])

M = cv.getAffineTransform(pts1,pts2)

dst = cv.warpAffine(img,M,(cols,rows))

affine

透视变换

对于透视变换,需要一个3x3变换矩阵。即使在转换之后,直线仍是直线。要找到此变换矩阵,需要在输入图像上找4个点,以及它们在输出图像中的对应位置。在这4个点中,其中任意3个不共线。然后可以通过函数cv.getPerspectiveTransform找到变换矩阵,将cv.warpPerspective应用于此3x3变换矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
img = cv.imread('sudoku.png')
rows,cols,ch = img.shape

pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pts2 = np.float32([[0,0],[300,0],[0,300],[300,300]])

M = cv.getPerspectiveTransform(pts1,pts2)

dst = cv.warpPerspective(img,M,(300,300))

plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')

perspective

阈值处理

简单阈值

cv.threshold(img(gray), value, maxVal, mode)

  • value:阈值
  • maxVal:变换值
  • mode:阈值模式
    • cv.THRESH_BINARY
    • cv.THRESH_BINARY_INV
    • cv.THRESH_TRUNC
    • cv.THRESH_TOZERO
    • cv.THRESH_TOZERO_INV
1
2
3
4
5
6
img = cv.imread('gradient.png',0)
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)

image6

自适应阈值

cv.adaptiveThreshold(img, maxVal, method, mode, block, C)

  • method: 自适应方法,决定如何计算阈值。
    • cv.ADAPTIVE_THRESH_MEAN_C:阈值是邻域的平均值。
    • cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域值的加权和,其中权重是高斯窗口。
  • block: 邻域大小,它决定了阈值区域的大小。
  • C:从计算的平均值或加权平均值中减去的常数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
img = cv.imread('sudoku.png',0)
img = cv.medianBlur(img,5)

ret,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
th2 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_MEAN_C,\
cv.THRESH_BINARY,11,2)
th3 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv.THRESH_BINARY,11,2)

titles = ['Original Image', 'Global Thresholding (v = 127)',
'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]

for i in xrange(4):
plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])

Otsu’s 二值化

在全局阈值处理中,我们使用任意值作为阈值,那么,我们如何知道我们选择的值是好还是不好?答案是,试错法。但如果是双峰图像(简单来说,双峰图像是直方图有两个峰值的图像)我们可以将这些峰值中间的值近似作为阈值,这就是Otsu二值化的作用。简单来说,它会根据双峰图像的图像直方图自动计算阈值。

由于我们正在使用双峰图像,因此Otsu的算法试图找到一个阈值(t),它最小化了由关系给出的加权类内方差

它实际上找到了一个位于两个峰之间的t值,这样两个类的方差都是最小的。

使用cv.threshold()函数,但是需要多传递一个参数cv.THRESH_OTSU。这时要吧阈值设为零。然后算法找到最佳阈值并返回第二个输出retVal。如果未使用Otsu二值化,则retVal与你设定的阈值相同。

输入图像是嘈杂的图像。在第一种情况下,我将全局阈值应用为值127。在第二种情况下,我直接应用了Otsu的二值化。在第三种情况下,我使用5x5高斯卷积核过滤图像以消除噪声,然后应用Otsu阈值处理。来看看噪声过滤如何改善结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

img = cv.imread('noisy2.png',0)

# global thresholding
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)

# Otsu's thresholding
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

# Otsu's thresholding after Gaussian filtering
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)

# plot all the images and their histograms
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]

for i in xrange(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])

plt.show()

otsu

图像滤波

二维卷积(图像过滤)

cv.filter2D

图像也可以使用各种低通滤波器(LPF),高通滤波器(HPF)等进行滤波。

  • LPF有助于消除噪声,模糊图像等
  • HPF滤波器有助于找到图片的边缘

下面我们将尝试对图像进行平均滤波使用的是一个5x5平均滤波器的核:

$$ K=\frac{1}{25}\begin{bmatrix} \ 1\ \ 1 \ \ 1 \ \ 1\ \ 1\ \ 1\ \ 1 \ \ 1 \ \ 1\ \ 1\ \ 1\ \ 1 \ \ 1 \ \ 1\ \ 1\ \ 1\ \ 1 \ \ 1 \ \ 1\ \ 1\ \ 1\ \ 1 \ \ 1 \ \ 1\ \ 1 \end{bmatrix} $$

1
2
kernel = np.ones((5,5),np.float32)/25
dst = cv.filter2D(img,-1,kernel)

图像模糊(图像平滑)

通过将图像与低通滤波器卷积核卷积来实现平滑图像。它有助于消除噪音,从图像中去除了高频内容(例如:噪声,边缘)。因此在此操作中边缘会模糊一点。

均值滤波

cv.blur(img, size)cv.boxFilter(img, size)

由一个归一化卷积框完成的。它取卷积核区域下所有像素的平均值并替换中心元素。卷积核为全为1的矩阵。3x3标准化的盒式过滤器如下所示:

$$ K=\frac{1}{9}\begin{bmatrix} \ 1 \ \ 1\ \ 1\ \ 1 \ \ 1\ \ 1\ \ 1 \ \ 1\ \ 1 \end{bmatrix} $$

高斯滤波

cv.GaussianBlur(img, size, sigmaX, sigmaY)

高斯模糊在从图像中去除高斯噪声方面非常有效。

把卷积核换成高斯核。卷积核的宽度和高度应该是正数并且是奇数。还应该分别指定X和Y方向的标准偏差sigmaX和sigmaY。如果仅指定了sigmaX,则sigmaY与sigmaX相同。如果两者都为零,则根据卷积核大小计算它们。

中值滤波

cv.medianBlur(img, size(int))

取卷积核区域下所有像素的中值,并用该中值替换中心元素。这对去除图像中的椒盐噪声非常有效。有趣的是,在上述滤波器中,中心元素是新计算的值,其可以是图像中的像素值或新值。但在中值模糊中,中心元素总是被图像中的某个像素值替换,它有效地降低了噪音。其卷积核大小应为正整数。

双边过滤

cv.bilateralFilter(img, dst, d, sigmaColor, sigmaSpace, borderType)

双边滤波器在空间中也采用高斯滤波器,但是还有一个高斯滤波器是像素差的函数。空间的高斯函数确保仅考虑附近的像素用于模糊,而强度差的高斯函数确保仅考虑具有与中心像素相似的强度的像素用于模糊。因此它保留了边缘,因为边缘处的像素将具有较大的强度变化。

1
blur = cv.bilateralFilter(img,9,75,75)

性能评估

cv.getTickCount函数返回参考事件(如机器开启时刻)到调用此函数的时钟周期数。

cv.getTickFrequency函数返回时钟周期的频率,或每秒钟的时钟周期数。

想获得函数的执行时间,你可以执行以下操作:

1
2
3
4
e1 = cv.getTickCount()
# your code execution
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()

你可以使用time模块的函数执行相同操作来替代cv.getTickCount,使用time.time()函数,然后取两次结果的时间差。


饭饭的OpenCV-Python笔记
https://asteriscus.cat/posts/7ce9751c/
作者
Asterisk
发布于
2021年7月22日
许可协议