OpenCV Recipes:处理视频流

In this post, we are going to learn how to convert an image into a cartoon-like image.

访问摄像头

我们可以使用摄像头的实时视频流构建非常有趣的应用程序。OpenCV 提供了一个视频捕捉对象,可以处理与摄像头的打开和关闭相关的所有事件。我们需要做的仅仅是创建这个对象,并持续从中读取帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import cv2

cap = cv2.VideoCapture(0)

# Check if the webcam is open correctly
if not cap.isOpened():
raise IOError("Cannot open webcam")


while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5,
interpolation=cv2.INTER_AREA)
cv2.imshow('Input', frame)

c = cv2.waitKey(1)

# if press Esc key
if c == 27:
break

cap.release()
cv2.destroyAllWindows()

背后的原理

如前所述,我们使用 OpenCV 的 VideoCapture 函数来创建视频捕捉对象 cap。一旦它被创建,我们就开始循环,一直从网络摄像头读取帧,直到遇到键盘中断。

while 循环的第一行中,我们有以下代码:

1
ret, frame = cap.read()

这里,retread 函数返回的布尔值,它指示帧是否被成功捕获。如果帧被正确捕获,它将存储在变量frame 中。这个循环将一直运行,直到我们按下 Esc 键。因此,我们用下面代码持续检查键盘中断:

1
if c == 27:

众所周知,Esc 的 ASCII 值是 27。一旦我们遇到它,我们就打破循环,释放视频捕捉对象。cap . release() 很重要,因为它优雅地释放了摄像头资源,以便其他应用程序可以利用它。

扩展捕获选项

如前所述,cv2.VideoCapture(0) 定义了使用默认连接摄像头的使用。使用 OpenCV 3.3.0 没有正确的方法来列出可用的摄像头,因此在运行此代码时,在连接了多个摄像头的情况下,必须增加 VideoCapture 的索引值直到选择到所需的那个。

如果有多个可用,例如 cv2.CAP_FFMPEGcv2.CAP_IMAGES,或者以 cv2.CAP_ * 开头的其他的,也可以强制执行特定的 reder 实现。 例如,可以在索引 1 上使用 QuickTime reader

1
2
cap = cv2.VideoCapture(1+cv2.CAP_QT)
implementation QuickTime

键盘输入

现在我们知道如何从摄像头捕获实时视频流,让我们看看如何使用键盘与显示视频流的窗口进行交互:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
'''
=================
keyboard controls
=================
Change color space of the input video stream
using keyboard controls. The control keys are:
1. Grayscale - press 'g'
2. YUV - press 'y'
3. HSV - press 'h'
'''

import cv2
import numpy as np

print(__doc__)

cap = cv2.VideoCapture(0)

# Check if the webcam is opened correctly
if not cap.isOpened():
raise IOError('Cannot open webcam')

cur_mode = None
while True:
# Read the current frame from webcam
ret, frame = cap.read()

# Resize the captured image
frame = cv2.resize(frame, None, fx=0.5, fy=0.5,
interpolation=cv2.INTER_AREA)

c = cv2.waitKey(1)
if c == 27:
break

# Update cur_mode only in case it is different and key was pressed
# In case a key was not pressed during the iteration result is -1
# or 255, depending on library versions
if c != -1 and c != 255 and c != cur_mode:
cur_mode = c

if cur_mode == ord('g'):
output = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
elif cur_mode == ord('y'):
output = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
else:
output = frame

cv2.imshow('Webcam', output)

cap.release()
cv2.destroyAllWindows()

鼠标输入

在本节中,我们将了解如何使用鼠标与显示窗口进行交互。 让我们从简单的事情开始吧。 我们将编写一个程序来检测检测到鼠标点击的象限。 一旦我们检测到它,我们将突出显示该象限:

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
36
37
38
39
40
41
import cv2
import numpy as np


def detect_quadrant(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
if x > width/2:
if y > height/2:
point_top_left = (int(width/2), int(height/2))
point_bottom_right = (width-1, height-1)
else:
point_top_left = (int(width/2), 0)
point_bottom_right = (width-1, int(height/2))
else:
if y > height/2:
point_top_left = (0, int(height/2))
point_bottom_right = (int(width/2), height-1)
else:
point_top_left = (0, 0)
point_bottom_right = (int(width/2), int(height/2))
img = param['img']
# Repaint all in white again
cv2.rectangle(img, (0, 0), (width-1, height-1), (255, 255, 255), -1)
# paint green quadrant
cv2.rectangle(img, point_top_left, point_bottom_right, (0, 100, 0),
-1)


if __name__ == '__main__':
width, height = 640, 480
img = 255 * np.ones((height, width, 3), dtype=np.uint8)
cv2.namedWindow('Input window')
cv2.setMouseCallback('Input window', detect_quadrant, {"img": img})

while True:
cv2.imshow('Input window', img)
c = cv2.waitKey(1)
if c == 27:
break

cv2.destroyAllWindows()

内部细节

让我们从这个程序的主函数开始。我们创建一个白色图像,然后,我们创建一个命名窗口并将 MouseCallback 函数绑定到此窗口。 MouseCallback 函数是检测到鼠标事件时需调用的函数。鼠标事件有很多种,例如单击、双击、拖动等。在我们的例子中,我们只检测鼠标点击。在 detect_quadrant 函数中,我们检查第一个输入参数 event 以查看执行了哪些操作。 OpenCV 提供了一组预定义事件,我们可以使用特定关键字来调用它们。如果要查看所有鼠标事件的列表,可以转到 Python shel l并键入以下内容:

1
2
import cv2
print([x for x in dir(cv2) if x.startswith('EVENT')])

detect_quadrant函数中的第二个和第三个参数提供鼠标单击事件的 X 和 Y 坐标。一旦我们知道这些坐标,就可以很容易地确定它所在的象限。有了这些信息,我们就可以使用 cv2.rectangle() 绘制一个具有指定颜色的矩形。这是一个非常方便的函数,它取左上角和右下角的点以及指定的颜色来绘制矩形。

与实时视频流交互

让我们看看如何使用鼠标与实时视频流进行交互。 我们可以使用鼠标选择一个区域,然后在该区域上应用负片效果。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import cv2
import numpy as np

def update_pts(params, x, y):
global x_init, y_init
params['top_left_pt'] = (min(x_init, x), min(y_init, y))
params['bottom_right_pt'] = (max(x_init, x), max(y_init, y))
img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]

def draw_rectangle(event, x, y, flags, params):
global x_init, y_init, drawing
# First click initizlize the init rectangle point
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
x_init, y_init = x, y

# Meanwhile mouse button is pressed, updated diagonal rectangle point
elif event == cv2.EVENT_MOUSEMOVE and drawing:
update_pts(params, x, y)

# Once mouse botton is release
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
update_pts(params, x, y)


if __name__ == '__main__':
drawing = False
event_params = {"top_left_pt": (-1, -1), "bottom_right_pt": (-1, -1)}

cap = cv2.VideoCapture(0)

# Check if the webcam is open correctly
if not cap.isOpened():
raise IOError("Cannot open webcam")

cv2.namedWindow('Webcam')
# Bind draw_rectangle function to every mouse event
cv2.setMouseCallback('Webcam', draw_rectangle, event_params)

while True:
ret, frame = cap.read()
img = cv2.resize(frame, None, fx=0.5, fy=0.5,
interpolation=cv2.INTER_AREA)
(x0, y0), (x1, y1) = event_params['top_left_pt'], event_params[
'bottom_right_pt']

img[y0:y1, x0:x1] = 255 - img[y0:y1, x0:x1]
cv2.imshow('Webcam', img)

c = cv2.waitKey(1)
if c == 27:
break

cap.release()
cv2.destroyAllWindows()

背后细节

正如我们在程序的主函数中所看到的,我们初始化了一个视频捕获对象。然后,我们将函数 draw_rectangleMouseCallback 函数绑定。

然后我们开始循环捕获视频流。

让我们看看函数 draw_rectangle:当我们使用鼠标绘制矩形时,我们必须检测三种类型的鼠标事件:鼠标单击、鼠标移动和鼠标按钮释放。这正是我们在这个函数中所做的。当我们检测到鼠标单击事件时,我们都会初始化为矩形的左上角。当我们移动鼠标时,我们通过将当前位置保存为矩形的右下角来选择感兴趣的区域。

一旦我们有了感兴趣的区域,我们只需反转像素即可应用负片效果。我们从 255 中减去当前像素值,这给了我们预期的效果。

当鼠标移动停止并检测到按钮事件时,我们停止更新矩形的右下角位置。继续显示此图像,直到检测到另一个鼠标单击事件。

卡通化图像

现在我们知道如何处理摄像头和键盘/鼠标输入了,让我们继续看看如何使图片具有卡通效果。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
'''
==================
Cartoonizing Image
==================
Change cartoonizing mode of image:
1. Cartoonize without Color -- press 's'
2. Cartoonize with color -- press 'c'
'''


import cv2
import numpy as np

def cartoonize_image(img, ksize=5, sketch_mode=False):
num_repetitions, sigma_color, sigma_space, ds_factor = 10, 5, 7, 4
# Convert image to grayscale
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Apply median filter to the grayscale image
img_gray = cv2.medianBlur(img_gray, 7)

# Detect edges in the image and threshold it
edges = cv2.Laplacian(img_gray, cv2.CV_8U, ksize=ksize)
ret, mask = cv2.threshold(edges, 100, 255, cv2.THRESH_BINARY_INV)

# 'mask' is the sketch of the image
if sketch_mode:
return cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)

# Resize the image to a smaller size for faster computation
img_small = cv2.resize(img, None, fx=1/ds_factor, fy=1/ds_factor,
interpolation=cv2.INTER_AREA)

# Apply bilateral filter the image multiple times
for i in range(num_repetitions):
img_small = cv2.bilateralFilter(img_small, ksize, sigma_color,
sigma_space)

img_output = cv2.resize(img_small, None, fx=ds_factor, fy=ds_factor,
interpolation=cv2.INTER_LINEAR)

dst = np.zeros(img_gray.shape)

# Add the thick boundary lines to the image using 'AND' operator
dst = cv2.bitwise_and(img_output, img_output, mask=mask)

return dst


if __name__ == '__main__':
print(__doc__)

cap = cv2.VideoCapture(0)

cur_mode = None

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5,
interpolation=cv2.INTER_AREA)

c = cv2.waitKey(1)
if c == 27:
break

if c !=-1 and c != 255 and c != cur_mode:
cur_mode = c

if cur_mode == ord('s'):
cv2.imshow('Cartoonize', cartoonize_image(frame, ksize=5,
sketch_mode=True))
elif cur_mode == ord('c'):
cv2.imshow('Cartoonize', cartoonize_image(frame, ksize=5,
sketch_mode=False))
else:
cv2.imshow('Cartoonize', frame)


cap.release()
cv2.destroyAllWindows()

背后细节

让我们看看 cartoonize_image 函数。首先,我们将图像转换为灰度图像,然后进行中值滤波。中值滤波非常适合去除椒盐噪音。我们使用 medianBlur 函数将中值滤波应用于输入图像。此函数中的第二个参数用于指定核的大小。核的大小与我们需要考虑的邻域大小有关,其可能的值只是奇数:1、3、5、7 等等。

回到 cartoonize_image,我们继续检测灰度图像上的边缘。我们需要知道边缘的位置,以便我们可以应用特殊效果。一旦我们检测到边缘,我们就会对它们进行阈值处理。

下一步,我们检查草图模式(sketch mode)是否已启用。如果是,那么我们只需将其转换为彩色图像并将其返回。

下一步,我们使用双边滤波来平滑图像。双边滤波是一个有趣的概念,其性能远优于高斯滤波。双边滤波的好处在于它保留了边缘,而高斯滤波使所有内容平滑。

我们在图像上多次运行此滤镜以使其平滑,使其看起来像卡通。然后,我们将特效蒙版叠加在此彩色图像的顶部,以创建卡通化效果。

GreatX wechat
关注我的公众号,推送优质文章。