OpenCV-Part1——基础操作

[TOC]

OpenCV

简介

  • OpenCV是计算机视觉中经典的专用库。
  • OpenCV-Python为OpenCV提供了Python接口,使得使用者在Python中能够调用C/C++,在保证易读性和运行效率的前提下,实现所需的功能。
  • 所有OpenCV数组结构都与Numpy数组相互转换,所以要使用OpenCV-Python编写优化的代码,必须先明白Numpy。
  • 本笔记以OpenCV4为主,会有个别版本上的差异,可能的话会提供解决方案。

安装

1
pip install opencv-python

GUI特性

图像入门

读取图像

  • 鉴于兼容windows和linux,文件路径请用/分隔
  • 文件路径中不得出现中文
  • 可以选择图像加载方式:
    • cv2.IMREAD_COLOR:默认加载彩色图像。任何图像的透明度都会被忽视。
    • cv2.IMREAD_GRAYSCALE:以灰度模式加载图像。
    • cv2.IMREAD_UNCHANGED:加载图像,包括alpha通道。
1
img = cv2.imread("validation/origin/circle/4101035072410.jpg", cv2.IMREAD_COLOR)

显示图像

  • 图像窗口名称参数不建议使用中文
  • 如果创建同名窗口两次,则第二张图片会替换前一张图片
1
cv2.imshow("img", img)

等待事件

  • 如果想要暂停并显示图片,必须使用cv2.waitKey(0),不能用input替代
  • 可以选择等待时间参数:
    • 非零:单位为毫秒,期间如果按下键,则忽略剩余时间、直接运行
    • 0:无限期地等待一次敲击键
1
cv2.waitKey(0)
  • 返回参数为keyCode,通过此可以设置检测特定的按键
1
2
while cv2.waitKey(1000) != ord("q"):
cv2.imshow("img", img)

销毁窗口

  • cv2.destroyAllWindows():会销毁创建的所有窗口
  • cv2.destroyWindow():销毁特定窗口

创建窗口

  • 手动创建窗口,并指定窗口是否可以调整大小
    • cv2.WINDOW_AUTOSIZE:默认,按照图像大小固定窗口大小
    • cv2.WINDOW_NORMAL:可以自动调整大小,对于大分辨率图像的查看有帮助
1
cv2.namedWindow('image', cv2.WINDOW_NORMAL)

写入图像

  • 同样路径不得出现中文,否则无输出
1
cv2.imwrite("test.jpg", img)

OpenCV与其他库

OpenCV与Matplotlib互转
  • OpenCV加载的彩色图像处于BGR模式,但是Matplotlib以RGB模式显示。
1
2
3
4
5
6
7
import cv2
from matplotlib import pyplot as plt

img = cv2.imread("validation/origin/circle/4101035072410.jpg")
img = img[:, :, ::-1]
plt.imshow(img)
plt.show()
PIL.Image转换成OpenCV
1
2
3
4
5
6
7
8
9
import cv2
from PIL import Image
import numpy

image = Image.open("test.jpg")
image.show()
img = cv2.cvtColor(numpy.asarray(image), cv2.COLOR_RGB2BGR)
cv2.imshow("img", img)
cv2.waitKey(0)
OpenCV转换成PIL.Image
1
2
3
4
5
6
7
8
import cv2
from PIL import Image

img = cv2.imread("test.jpg")
cv2.imshow("img", img)
image = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
image.show()
cv2.waitKey(0)

视频入门

读取摄像头视频

  • 需要创建一个 VideoCapture 对象。它的参数可以是设备索引或视频文件的名称。
  • cap.read():返回是否正确读取帧。可以通过检查此返回值来判断是否到达视频的结尾。
  • cap.isOpened():返回是否初始化捕获。否则,使用cap.open()打开它。
  • 当打开视频文件时,播放速度会不受限制,最好是从fps属性动态计算每帧之间相隔的时间,作为参数传入cv2.waitKey()
  • 使用视频捕获必须确保安装了正确的 ffmpeg 或 gstreamer 版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2

cap = cv2.VideoCapture(0)
if not cap.isOpened():
exit("Cannot open camera")
while True:
ret, frame = cap.read() # 逐帧读取
if not ret: # 如果成功读取帧,ret为True
print("Can't receive frame (stream end?). Exiting ...")
break
cv2.imshow('frame', frame) # 显示当前帧
if cv2.waitKey(1) == ord('q'):
break
cap.release() # 完成所有操作后,释放捕获器
cv2.destroyAllWindows()

视频属性

属性方法
  • cap.get(propId):查看视频的属性。
  • cap.set(propId,value):修改视频的属性。
  • cap画布的长宽上下限似乎和摄像头本身有关。
1
2
3
4
5
6
7
8
9
10
11
cap = cv2.VideoCapture(0)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, cap.get(cv2.CAP_PROP_FRAME_WIDTH) * 2)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, cap.get(cv2.CAP_PROP_FRAME_WIDTH) * 0.1)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
"""
640.0 480.0
1280.0 720.0
320.0 240.0
"""
propId对应表
propid 属性 视频流属性
0 CV_CAP_PROP_POS_MSEC 视频文件以毫秒为单位的当前位置或视频捕获时间戳(就是说你这个位置是视频当中的第几秒第几毫秒)
1 CV_CAP_PROP_POS_FRAMES 基于索引的帧解码/捕获(就是说你现在的位置是视频的第几帧位置)
2 CV_CAP_PROP_POS_AVI_RATIO 视频文件的相对位置:0 -电影,开始1 -电影结束(就是说输出0表示视频刚开始,输出1表示视频结束了)
3 CV_CAP_PROP_FRAME_WIDTH 视频流画面的宽度
4 CV_CAP_PROP_FRAME_HEIGHT 视频流画面的长度
5 CV_CAP_PROP_FPS 视频流的帧频
6 CV_CAP_PROP_FOURCC 编辑器的四字符编码
7 CV_CAP_PROP_FRAME_COUNT 视屏文件的帧数
8 CV_CAP_PROP_FORMAT Format of the Mat objects returned by retrieve()
9 CV_CAP_PROP_MODE Backend-specific value indicating the current capture mode
10 CV_CAP_PROP_BRIGHTNESS Brightness of the image (only for cameras)摄像机图像的亮度(只有摄像头才可以)
11 CV_CAP_PROP_CONTRAST Contrast of the image (only for cameras).摄像机图像的对比度(只有摄像头才可以)
12 CV_CAP_PROP_SATURATION Saturation of the image (only for cameras)摄像机图像的饱和度(只有摄像头才可以)
13 CV_CAP_PROP_HUE 图片的色调(只有摄像头才可以)
14 CV_CAP_PROP_GAIN Gain of the image(只有摄像头才可以)
15 CV_CAP_PROP_EXPOSURE Exposure 曝光(只有摄像头才可以)
16 CV_CAP_PROP_CONVERT_RGB 布尔值指示是否应该转换为RGB图
17 CV_CAP_PROP_WHITE_BALANCE_U The U value of the whitebalance setting (note: only supported by DC1394 v 2.x backend currently)
18 CV_CAP_PROP_WHITE_BALANCE_V The V value of the whitebalance setting (note: only supported by DC1394 v 2.x backend currently)
19 CV_CAP_PROP_RECTIFICATION Rectification flag for stereo cameras (note: only supported by DC1394 v 2.x backend currently)
20 CV_CAP_PROP_ISO_SPEED The ISO speed of the camera (note: only supported by DC1394 v 2.x backend currently)
21 CV_CAP_PROP_BUFFERSIZE Amount of frames stored in internal buffer memory (note: only supported by DC1394 v 2.x backend currently)

保存视频

  • 需要创建一个 VideoWriter 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2

cap = cv2.VideoCapture(0)
# 定义编解码器并创建VideoWriter对象
fourcc = cv2.VideoWriter_fourcc(*'XVID')
writer = cv2.VideoWriter('output.avi', fourcc, 30.0, (640, 480))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# 写翻转的框架
writer.write(frame)
cv2.imshow('frame', frame)
if cv2.waitKey(1) == ord('q'):
break
# 完成工作后释放所有内容
cap.release()
writer.release()
cv2.destroyAllWindows()

绘图功能

  • color:对于bgr颜色要传递元组,例如(255,0,0),传递int则视为b值。
  • thickness:线条厚度,默认为1;对于封闭图形,-1位填充。
  • lineType:线的类型,默认为为8连接线;对于曲线可以设置为抗锯齿线cv2.LINE_AA

画线

1
cv2.line(img, pt1=(0, 0), pt2=(511, 511), color=(255, 0, 0), thickness=5, lineType=cv2.LINE_AA)

画矩形

  • 两个点分别为左上角和右下角
1
cv2.rectangle(img, pt1=(384, 0), pt2=(510, 128), color=(0, 255, 0), thickness=3)

画圆

1
cv2.circle(img, center=(447, 63), radius=63, color=(0, 0, 255), thickness=-1)

画椭圆

  • angle:按顺时针偏转的角度
  • startAngle、endAngle:按顺时针计算区间。[0, 0]则无椭圆,[0, 360]则完整椭圆。
1
cv2.ellipse(img, center=(256, 256), axes=(100, 50), angle=0, startAngle=0, endAngle=180, color=255, thickness=-1)

image-20210816075339107

画多边形

  • 必须转换成int32
  • 注意pts要用列表装饰
1
2
3
pts = [[10, 5], [20, 30], [70, 20], [50, 10]]
pts = np.array(pts).astype(np.int32).reshape((-1, 1, 2))
cv2.polylines(img, pts=[pts], isClosed=True, color=(0, 255, 255), thickness=3)

添加文本

1
2
cv2.putText(img, text='OpenCV', org=(10, 350), fontFace=cv2.FONT_HERSHEY_SIMPLEX, 
fontScale=1, color=(255, 255, 255), thickness=2, lineType=cv2.LINE_AA)

鼠标作为画笔

可用事件与回调函数

  • 列出所有可用事件
1
2
3
import cv2 as cv
events = [i for i in dir(cv) if 'EVENT' in i]
print( events )
  • setMouseCallback:鼠标回调函数,参数是固定的,会自动与窗口绑定
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
34
35
import numpy as np
import cv2

drawing = False # 如果按下鼠标,则为真
mode = True # 如果为真,绘制矩形。按 m 键可以切换到曲线
ix, iy = -1, -1

# 鼠标回调函数
def draw_circle(event, x, y, flags, param):
global ix, iy, drawing, mode

if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
ix, iy = x, y
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
if mode:
cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1)
else:
cv2.circle(img, (x, y), 5, (0, 0, 255), -1)
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
if mode:
cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), -1)
else:
cv2.circle(img, (x, y), 5, (0, 0, 255), -1)

if __name__ == '__main__':
# 创建一个黑色的图像,一个窗口,并绑定到窗口的功能
img = np.zeros((512, 512, 3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image', draw_circle)
while not cv2.waitKey(25) & 0xFF == 27:
cv2.imshow('image', img)
cv2.destroyAllWindows()

核心操作

图像基本操作

访问和修改像素值

  • 对于 BGR 图像,它返回一个由蓝色、绿色和红色值组成的数组。对于灰度图像,只返回相应的灰度。

  • 通常会使用numpy切片图像,不建议使用numpy索引来访问、修改单个像素值。

  • 访问、修改单个像素值时,使用方法item()itemset())被认为更好,但是它们始终操作单通道。

1
2
3
4
5
6
7
8
9
10
11
print(img[100, 100])  # 彩色图片返回 [蓝, 绿, 红]
# [157, 166, 200]
print(img[100, 100, 0]) # 仅访问蓝色像素
# 157
img[100, 100] = [157, 166, 200] # 修改像素
img[100, 100, 0] = 157 # 仅修改蓝色像素

# 访问 RED 值
print(img.item(10, 10, 2)) # 59
# 修改 RED 值
img.itemset((10, 10, 2), 100)

访问图像属性

  • img.shape:图像的形状[行数(高height), 列数(宽width), [蓝, 绿, 红]]
  • 如果图像是灰度的,则返回的元组仅包含行数和列数
1
2
print(img.shape)
(342, 548, 3)
  • img.size:像素总数
1
2
print(img.size)
# 562248
  • img.dtype:图像数据类型
  • img.dtype在调试时非常重要,因为OpenCV-Python代码中的大量错误是由无效的数据类型引起的。
1
2
print( img.dtype )
# uint8

图像区域切片

  • 注意,索引顺序是[行数, 列数, 通道],即[高, 宽, [蓝, 绿, 红]]
1
2
3
# 将球复制到图像中的另一个区域
ball = img[280:340, 330:390]
img[273:333, 100:160] = ball

拆分和合并图像通道

  • cv2.split(img):切分三个通道
1
2
3
4
5
6
7
# split()比较耗时,可以考虑使用np切片
b, g, r = cv2.split(img)
b, g, r = img [:, :]
# 只获取蓝通道,使用numpy更快
b = img [:, :, 0]
# 将所有红色像素都设置为零,则无需拆分通道
img [:, :, 2] = 0
  • cv2.merge((b,g,r)):合并三个通道
  • 可以将灰度图扩展成三通道
1
img = cv2.merge((b,g,r))

填充边框

  • cv2.copyMakeBorder():padding经常使用。
    • src:输入图像
    • topbottomleftright:边界宽度
    • borderType:定义要添加哪种边框的标志。它可以是以下类型:
      • cv2.BORDER_CONSTANT:添加恒定的彩色边框。该值应作为下一个参数给出。
      • cv2.BORDER_REFLECTcv2.BORDER_REFLECT_101cv2.BORDER_DEFAULT:边框将是边框元素的镜像
      • cv2.BORDER_REPLICATE:重复边界最后一个元素被复制
      • cv2.BORDER_WRAP:将图片复制平铺
    • value:边框的颜色,如果边框类型为 cv.BORDER_CONSTANT
1
2
3
4
5
6
7
img = cv2.imread("test.jpg", cv2.IMREAD_COLOR)
# 在右边接一张原图
img = cv2.copyMakeBorder(img, top=0, bottom=0, left=0, right=img.shape[1], borderType=cv2.BORDER_WRAP)
# 底部加一条绿边
img = cv2.copyMakeBorder(img, top=0, bottom=10, left=0, right=0, borderType=cv2.BORDER_CONSTANT, value=[0, 255, 0])
cv2.imshow("img", img)
cv2.waitKey(0)

image-20210818232157400

图像加法

  • 可以通过OpenCV函数cv2.add()或numpy操作res = img1 + img2相加两个图像。
  • OpenCV加法和Numpy加法之间有区别。OpenCV加法是饱和运算,而Numpy加法是模运算。
  • 两个图像需要有相同的深度和类型,或者第二个图像可以只是一个标量值。
  • 一般会使用OpenCV的方法,或直接使用图像融合。
1
2
3
4
5
6
x = np.uint8([250])
y = np.uint8([10])
print(cv2.add(x, y)) # 250 + 10 = 260 => 255
# [[255]]
print( x + y ) # (250 + 10) % 256 = 260 % 256 = 4
# [4]

图像反色

  • 千万不要用循环!
  • 需要在指定区域反色的话,就加入mask参数。比如,将黑色背景反转为白色背景。
  • 因为两者范围都在0-255,所以直接做np减法。
1
img = np.full(img.shape, (255, 255, 255), dtype=np.uint8) - img

图像融合

  • cv2.addWeighted():对图像赋予不同的权重,并相加。
  • 通过动态更改两张的图像权重0->1, 1->0,可以完成一个图像到另一个图像之间的视觉过渡。
  • 并不要求两者权重相加为1
1
2
3
4
5
img1 = cv.imread('ml.png')
img2 = cv.imread('opencv-logo.png')
dst = cv.addWeighted(img1, 0.7, img2, 0.3, 0)
cv.imshow('dst', dst)
cv.waitKey(0)

img

按位运算

  • ANDORNOTXOR :用于提取图像的任意部分、定义和处理非矩形ROI。

  • 当需要将A图像覆盖在B图像上时。如果将两个图像相加,则改变颜色。如果将两个图像混合,则获得透明效果。

    如果是矩形区域,则可以像上一章一样使用ROI切片,重置区域数值。A图像不是矩形。因此,可以进行按位操作。、

  • mask需要为np.uint8类型,否则.astype(np.uint8);mask需要通道数为1,否则mask = mask[:, :, 0]

  • 前景图片大小不能超过背景,否则会报错

  • mask的含义是:仅mask中的白色区域会进行运算,其他部分全部为黑色

  • 最后相加时,要确保相加部分是 0 + img_mask = img_mask;否则依然会出现融合效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 加载两张图片
img1 = cv2.imread('test.jpg')
img2 = cv2.imread('test2.png')
img2 = cv2.resize(img2, (img2.shape[0]//2, img2.shape[1]//2)) # 控制前景大小
# 想把logo放在左上角,所以在左上角创建了ROI
rows, cols, channels = img2.shape
roi = img1[0:rows, 0:cols]
# 现在创建logo的掩码,并同时创建其相反掩码
mask_inv = extract_red(img2, cfg={"debug": False})[:, :, 0] # mask为单通道
mask = cv2.bitwise_not(mask_inv)
# 现在将ROI中logo的区域涂黑
print(roi.dtype, mask_inv.dtype, roi.shape, mask_inv.shape) # uint8 uint8 (533, 548, 3) (533, 548)
img1_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)
# 仅从logo图像中提取logo区域
img2_fg = cv2.bitwise_and(img2, img2, mask=mask) # 仅白色区域会从原图中保留,其他部分全部为黑色
# 将logo放入ROI并修改主图像
dst = cv2.add(img1_bg, img2_fg) # 要确保相加部分是 0 + img_mask = img_mask;否则依然会出现融合效果
img1[0:rows, 0:cols] = dst
cv2.imshow('res', img1)
cv2.waitKey(0)
image-20210819112755771

性能衡量和提升技术

使用OpenCV衡量性能

cv2.getTickCount:函数返回从参考事件(如打开机器的那一刻)到调用此函数那一刻之间的时钟周期数。

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

1
2
3
4
e1 = cv2.getTickCount()
# 你的执行代码
e2 = cv2.getTickCount()
time = (e2 - e1) / cv.getTickFrequency() # 计算两次执行函数的时间差

使用time.time()函数,然后取两次相差,与上述方法是等价的。

OpenCV中的默认优化

许多OpenCV函数都默认使用了SSE2、AVX等进行优化。可以使用 cv2.Useoptimized 检查是否启用和 cv2.Setuseoptimized 以启用/禁用优化。

性能优化技术

首先尝试以一种简单的方式实现算法。然后,分析、找到瓶颈,再进行优化。

使用下面的技巧来充分利用 Python 和 Numpy 的最大性能。

  1. Python标量操作比Numpy标量操作快。因此,对于包含一两个元素的运算,Python标量比Numpy数组好。当数组大小稍大时,Numpy会占优势。
  2. 通常,OpenCV函数比Numpy函数要快。因此,对于相同的操作,首选OpenCV功能。但是,可能会有例外,尤其是当Numpy处理视图而不是副本时。
  3. 尽量避免在Python中使用循环,尤其是双/三重循环等。
  4. 由于Numpy和OpenCV已针对向量运算进行了优化,因此将算法/代码向量化到最大程度。
  5. 利用缓存一致性。
  6. 除非需要,否则切勿创建数组的副本。尝试改用视图。数组复制是一项昂贵的操作。

执行了所有这些操作后,如果仍然很慢,或者不可避免地需要使用大循环,请使用Cython等其他库来使其更快。

其他资源

  1. Python优化技术:http://wiki.python.org/moin/PythonSpeed/PerformanceTips
  2. Scipy讲义- 高级Numpy:http://scipy-lectures.github.io/advanced/advanced_numpy/index.html#advanced-numpy
  3. IPython中的时序和性能分析:http://pynash.org/2013/03/06/timing-and-profiling/