OpenCV Recipes:检测和跟踪不同的身体部位

In this post, we are going to learn how to detect and track different body parts in a live video stream.

用哈尔级联(Haar cascades)来检测物体

当我们说 Haar 级联时,我们实际上是在谈论基于 Haar 特征的级联分类器。早在 2001 年,Paul Viola 和 Michael Jones 就在他们的开创性论文中提出了一种非常有效的物体检测方法。在他们的论文中,他们描述了一种机器学习技术,其中使用增强级联的简单分类器来获得性能非常好的整体分类器。

假设我们想要检测一个像菠萝这样的物体。我们需要建立一个机器学习系统来学习菠萝的样子。它能够告诉我们未知图像是否包含菠萝。要实现这样的目标,我们需要训练我们的系统。在机器学习领域,我们有很多方法可用于训练系统。为了训练我们的系统,我们需要获取大量的菠萝和非菠萝图像,然后将它们送入系统。这里,菠萝图像称为正图像,非菠萝图像称为负图像。

就训练而言,有很多路线可供选择。但是所有传统技术都是计算密集型的,并产生复杂的模型。我们不能使用这些模型来构建实时系统。因此,我们需要保持分类器简单。但是如果我们保持分类器简单,那么模型就不准确了。速度和准确度之间的权衡在机器学习中很常见。我们通过构建一组简单的分类器然后将它们级联在一起以形成一个强大的统一分类器来克服这个问题。为了确保整体分类器运行良好,我们需要在级联步骤创新。这是 Viola-Jones 方法如此有效的主要原因之一。

谈到人脸检测的主题,让我们看看如何训练系统来检测人脸。如果我们想要构建机器学习系统,我们首先需要从所有图像中提取特征。在我们的例子中,机器学习算法将使用这些特征来理解面部。我们使用 Haar 特征来构建我们的特征向量。 Haar 特征是简单地对图像进行求和、相减。我们对不同尺寸的图像执行此操作,确保系统具有尺度不变性。

一旦我们提取了这些特征,我们就会将其传给级联分类器。我们只检查所有不同的矩形子区域,并持续丢弃那些没有面部的子区域。这样,我们快速得出最终结果。

什么是积分图像?

如果我们想要计算 Haar 特征,我们将不得不计算图像中许多不同矩形区域的和。如果我们想要有效地构建特征集,我们需要在多个尺度上计算这些求和。这是一个计算密集的过程!如果我们想要构建一个实时系统,我们就不能花这么多时间来计算这些总和。所以我们使用一种称为积分图像的东西:

要计算图像中任何矩形的总和,我们不需要遍历该矩形区域中的所有元素。假设 AP 表示由左上角点与图像中的 P 点形成的矩形中所有元素的总和。如果我们想要计算矩形 ABCD 的面积,我们可以使用以下公式:

矩形区域 ABCD = AC - (AB + AD - AA)

为什么我们要关心这个特殊的公式呢?如前所述,提取 Haar 特征涉及在多个尺度上计算图像中大量矩形的区域。很多这些计算都是重复的,整个过程非常缓慢。事实上,它是如此之慢,以至于无法负担实时运行。这种方法的好处是我们不必重新计算。

检测和跟踪人脸

OpenCV 提供了一个很好的人脸检测框架。我们只需要加载级联文件并使用它来检测图像中的人脸。

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
import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml')

cap = cv2.VideoCapture(0)
scaling_factor = 0.5

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

face_rects = face_cascade.detectMultiScale(frame, scaleFactor=1.3,
minNeighbors=3)

for (x, y, w, h) in face_rects:
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 3)

cv2.imshow('Face Detector', frame)

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

cap.release()
cv2.destroyAllWindows()

背后的细节

我们需要一个分类器模型,用于检测图像中的人脸。OpenCV 提供了一个可用于此目的的 XML 文件。我们使用函数 CascadeClassifier 来加载 XML 文件。我们从摄像头捕获输入帧并使用 detectMultiScale 函数获取当前图像中所有人脸的边框。detectMultiScale 函数中的第二个参数指定缩放因子,如果我们在当前比例中找不到图像,则在下一个要检查的大小将是当前大小的 1.3 倍。最后一个参数是一个阈值,指定保持当前矩形所需的最小相邻矩形数。它可以用于增加人脸检测器的健壮性,为了防面部识别未按预期工作,需要降低阈值以获得更好的识别效果。在图像由于处理检测而受到延迟的情况下,将缩放帧的尺寸减小 0.4 或 0.3。

面部识别好玩的应用

既然我们知道如何检测和跟踪人脸,让我们来进行一些有趣的应用吧。当我们从摄像头捕捉视频流时,我们可以在脸上覆盖一层面具(点此获取素材)。

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
import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml')

face_mask = cv2.imread('./images/mask_hannibal.png')
h_mask, w_mask = face_mask.shape[:2]

if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')

cap = cv2.VideoCapture(0)
scaling_factor = 0.5

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

face_rects = face_cascade.detectMultiScale(frame, scaleFactor=1.3,
minNeighbors=3)

for (x, y, w, h) in face_rects:
if h <= 0 or w <= 0:
pass
# Adjust the height and weight parameters depending on
# the sizes and the locations
# You need to play around with these to make sure you get it right
h, w = int(h), int(w)
y -= int(-0.2*h)
x = int(x)
# Extract the region of interest from the image
frame_roi = frame[y:y+h, x:x+w]
face_mask_small = cv2.resize(face_mask, (w, h),
interpolation=cv2.INTER_AREA)
# Convert color image to grayscale and threshold it
gray_mask = cv2.cvtColor(face_mask_small, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray_mask, 180, 255, cv2.THRESH_BINARY_INV)

# Create an inverse mask
mask_inv = cv2.bitwise_not(mask)
try:
# Use the mask to extract the face mask region of interest
masked_face = cv2.bitwise_and(face_mask_small, face_mask_small,
mask=mask)
# Use the inverse mask to get the remaining part of the image
masked_frame = cv2.bitwise_and(frame_roi, frame_roi,
mask=mask)
except cv2.error as e:
print('Ignoring arithmentic exceptions:'+str(e))

# add the two images to get the final output
frame[y:y+h, x:x+w] = cv2.add(masked_face, masked_frame)

cv2.imshow('Face Detector', frame)

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

cap.release()
cv2.destroyAllWindows()

背后的细节

就像之前一样,我们首先加载人脸级联分类器 XML 文件。面部检测步骤照常工作。我们开始循环,在每一帧中继续检测面部。一旦我们知道了面部的位置,需要修改坐标以确保面罩适合。这种操纵过程是主观的,取决于所使用的面具。

不同的面具需要不同级别的调整才能使其看起来更自然。我们通过下面代码从输入帧中提取感兴趣区域:

1
frame_roi = frame[y:y+h, x:x+w]

现在我们已经拥有了所需的感兴趣区域,我们需要在此基础上覆盖蒙版。因此,我们调整输入面具的大小以确保它适合此感兴趣区域。输入面具具有白色背景。因此,如果我们将其叠加在感兴趣区域之上,由于白色背景,它看起来会不自然。我们只需要覆盖特定的区域,剩下的区域应该是透明的。

因此,在下一步中,我们通过对面具图像进行阈值处理来创建蒙版。由于背景是白色的,我们对图像进行阈值处理,使得强度值大于 180 的任何像素都变为零,其他所有像素都变为 255。一旦我们有了面具图像的蒙版和输入感兴趣区域,我们只需将它们添加起来即可获得最终图像。

从叠加图像中删除 Alpha 通道

由于使用了叠加图像,这可能会在黑色像素上构建一个图层,会对代码的结果产生不良影响。为了避免这个问题,下面的代码从叠加图像中删除了 alpha 通道层,因此我们可以获得良好的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
import cv2

def remove_alpha_channel(source, background_color):
source_img = cv2.cvtColor(source[:,:,:3], cv2.COLOR_BGR2GRAY)
source_mask = source[:,:,3] * (1 / 255.0)
bg_part = (255 * (1 / 255.0)) * (1.0 - source_mask)
weight = (source_img * (1 / 255.0)) * (source_mask)
dest = np.uint8(cv2.addWeighted(bg_part, 255.0, weight, 255.0, 0.0))
return dest

orig_img = cv2.imread('./images/overlay_source.png', cv2.IMREAD_UNCHANGED)
dest_img = remove_alpha_channel(orig_img)
cv2.imwrite('images/overlay_dest.png', dest_img,
[cv2.IMWRITE_PNG_COMPRESSION])

检测眼睛

现在我们已经了解了如何检测面部,我们可以将概念概括为检测其他身体部位。Viola-Jones 框架可以应用于任何对象,其准确性和稳健性取决于对象的唯一性。 例如,人脸具有非常独特的特征,因此很容易训练得到健壮的系统 。另一方面,像毛巾,衣服或书籍这样的物体过于通用,并且没有这样的明显特征,因此构建强大的检测器更加困难。

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
import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml')
eye_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_eye.xml')

if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')
if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')

cap = cv2.VideoCapture(0)
ds_factor = 0.5

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor,
interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3,
minNeighbors=1)
for (x,y,w,h) in faces:
roi_gray = gray[y:y+h, x:x+w]
roi_color = frame[y:y+h, x:x+w]
eyes = eye_cascade.detectMultiScale(roi_gray)
for (x_eye,y_eye,w_eye,h_eye) in eyes:
center = (int(x_eye + 0.5*w_eye), int(y_eye + 0.5*h_eye))
radius = int(0.3 * (w_eye + h_eye))
color = (0, 255, 0)
thickness = 3
cv2.circle(roi_color, center, radius, color, thickness)

cv2.imshow('Eye Detector', frame)

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

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
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
81
82
83
84
85
86
87
88
import cv2
import numpy as np

face_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml')
eye_cascade = cv2.CascadeClassifier(
'/usr/local/share/OpenCV/haarcascades/haarcascade_eye.xml')

if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')
if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')

cap = cv2.VideoCapture(0)
sunglasses_img = cv2.imread('images/sunglasses.png')

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

vh, vw = frame.shape[:2]
vh, vw = int(vh), int(vw)

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3,
minNeighbors=1)

centers = []
for (x,y,w,h) in faces:
roi_gray = gray[y:y+h, x:x+w]
roi_color = frame[y:y+h, x:x+w]
eyes = eye_cascade.detectMultiScale(roi_gray)

for (x_eye,y_eye,w_eye,h_eye) in eyes:
centers.append((x + int(x_eye + 0.5*w_eye), y + int(y_eye +
0.5*h_eye)))
if len(centers) > 1: # if detects both eyes
h, w = sunglasses_img.shape[:2]
# Extract the region of interest from the image
eye_distance = abs(centers[1][0] - centers[0][0])
# Overlay sunglasses; the factor 2.12 is customizable depending on
# the size of the face
sunglasses_width = 2.12 * eye_distance
scaling_factor = sunglasses_width / w
print(scaling_factor, eye_distance)
overlay_sunglasses = cv2.resize(sunglasses_img, None,
fx=scaling_factor, fy=scaling_factor,
interpolation=cv2.INTER_AREA)

x = centers[0][0] if centers[0][0] < centers[1][0] else centers[1][0]

# customizable X and Y locations; depends on the size of the face
x -= int(0.26*overlay_sunglasses.shape[1])
y += int(0.26*overlay_sunglasses.shape[0])
h, w = overlay_sunglasses.shape[:2]
h, w = int(h), int(w)
frame_roi = frame[y:y+h, x:x+w]
# Convert color image to grayscale and threshold it
gray_overlay_sunglassess = cv2.cvtColor(overlay_sunglasses,
cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray_overlay_sunglassess, 180, 255,
cv2.THRESH_BINARY_INV)

# Create an inverse mask
mask_inv = cv2.bitwise_not(mask)
try:
# Use the mask to extract the face mask region of interest
masked_face = cv2.bitwise_and(overlay_sunglasses,
overlay_sunglasses, mask=mask)
# Use the inverse mask to get the remaining part of the image
masked_frame = cv2.bitwise_and(frame_roi, frame_roi,
mask=mask_inv)
except cv2.error as e:
print('Ignoring arithmentic exceptions: '+ str(e))
#raise e
# add the two images to get the final output
frame[y:y+h, x:x+w] = cv2.add(masked_face, masked_frame)
else:
print('Eyes not detected')
cv2.imshow('Eye Detector', frame)

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

cap.release()
cv2.destroyAllWindows()

检测耳朵

通过使用 Haar 级联分类器文件,下面的代码将识别耳朵,需要两个不同的分类器,因为每个耳朵的坐标不同:

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
import cv2
import numpy as np

left_ear_cascade = cv2.CascadeClassifier(
'data/cascades/haarcascade_mcs_leftear.xml')
right_ear_cascade = cv2.CascadeClassifier(
'data/cascades/haarcascade_mcs_rightear.xml')

if left_ear_cascade.empty():
raise IOError('Unable to load the left ear cascade classifier xml file')
if right_ear_cascade.empty():
raise IOError('Unable to load the right ear cascade classifier xml file')

cap = cv2.VideoCapture(0)
scaling_factor = 0.5

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor,
interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
left_ear = left_ear_cascade.detectMultiScale(gray, scaleFactor=1.3,
minNeighbors=3)
right_ear = right_ear_cascade.detectMultiScale(gray, scaleFactor=1.3,
minNeighbors=3)

for (x,y,w,h) in left_ear:
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)

for (x,y,w,h) in right_ear:
cv2.rectangle(frame, (x,y), (x+w,y+h), (255,0,0), 3)

cv2.imshow('Ear Detector', frame)

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

cap.release()
cv2.destroyAllWindows()

检测嘴巴

使用 Haar 分类器,从输入视频流中提取嘴部位置。

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
import cv2
import numpy as np

mouth_cascade = cv2.CascadeClassifier(
'data/cascades/haarcascade_mcs_mouth.xml')

if mouth_cascade.empty():
raise IOError('Unable to load the mouth cascade classifier xml file')

cap = cv2.VideoCapture(0)
ds_factor = 0.5

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor,
interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mouth_rects = mouth_cascade.detectMultiScale(gray, scaleFactor=1.7,
minNeighbors=11)

for (x,y,w,h) in mouth_rects:
y = int(y - 0.15*h)
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)
break

cv2.imshow('Mouth Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break

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
42
43
44
45
46
47
48
49
import cv2
import numpy as np

mouth_cascade = cv2.CascadeClassifier(
'data/cascades/haarcascade_mcs_mouth.xml')
moustache_mask = cv2.imread('images/moustache.png')

h_mask, w_mask = moustache_mask.shape[:2]
if mouth_cascade.empty():
raise IOError('Unable to load the mouth cascade classifier xml file')

cap = cv2.VideoCapture(0)
scaling_factor = 0.5

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor,
interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mouth_rects = mouth_cascade.detectMultiScale(gray, 1.3, 5)
if len(mouth_rects) > 0:
(x,y,w,h) = mouth_rects[0]
h, w = int(0.6*h), int(1.2*w)
x -= int(0.05*w)
y -= int(0.55*h)

frame_roi = frame[y:y+h, x:x+w]
moustache_mask_small = cv2.resize(moustache_mask, (w, h),
interpolation=cv2.INTER_AREA)

gray_mask = cv2.cvtColor(moustache_mask_small, cv2.COLOR_BGR2GRAY)

ret, mask = cv2.threshold(gray_mask, 50, 255,
cv2.THRESH_BINARY_INV)

mask_inv = cv2.bitwise_not(mask)
masked_mouth = cv2.bitwise_and(moustache_mask_small,
moustache_mask_small, mask=mask)

masked_frame = cv2.bitwise_and(frame_roi, frame_roi, mask=mask_inv)
frame[y:y+h, x:x+w] = cv2.add(masked_mouth, masked_frame)
cv2.imshow('Moustache', frame)

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

cap.release()
cv2.destroyAllWindows()

检测瞳孔

瞳孔太普通了,无法采用 Haar 级联方法,在这里我们将采取不同的方法。 我们还将了解如何根据形状检测物体。

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
import math
import cv2

eye_cascade = cv2.CascadeClassifier('data/cascades/haarcascade_eye.xml')

if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')

cap = cv2.VideoCapture(0)
ds_factor = 0.5
ret, frame = cap.read()
contours = []

while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor,
interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
eyes = eye_cascade.detectMultiScale(gray, scaleFactor=1.3,
minNeighbors=1)

for (x_eye, y_eye, w_eye, h_eye) in eyes:
pupil_frame = gray[y_eye:y_eye + h_eye, x_eye:x_eye + w_eye]
ret, thresh = cv2.threshold(pupil_frame, 80, 255, cv2.THRESH_BINARY)

cv2.imshow("threshold", thresh)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
print(contours)

for contour in contours:
area = cv2.contourArea(contour)
rect = cv2.boundingRect(contour)
x, y, w, h = rect
radius = 0.15 * (w + h)
area_condition = (100 <= area <= 200)
symmetry_condition = (abs(1 - float(w)/float(h)) <= 0.2)
fill_condition = (abs(1 - (area / (math.pi *
math.pow(radius, 2.0)))) <= 0.4)
cv2.circle(frame, (int(x_eye + x + radius), int(y_eye +
y + radius)), int(1.3 * radius), (0, 180, 0), -1)

cv2.imshow('Pupil Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break

cap.release()
cv2.destroyAllWindows()

背后的细节

如前所述,我们不能使用 Haar 级联来检测瞳孔。如果我们不能使用预先训练好的分类器,那么我们如何检测呢?我们可以使用形状分析来检测。

我们知道瞳孔是圆形的,因此我们可以使用这些信息。我们将输入图像反转,然后将其转换为灰度图像,因为瞳孔是黑色的,而黑色对应于低像素值。然后我们对图像进行阈值处理,以确保只有黑白像素。

现在,我们必须找出所有形状的边界,而 OpenCV 提供了一个很好的函数,findContours

下一步是确定瞳孔的形状并丢弃其余部分。我们将使用圆的某些属性来对齐此形状。让我们考虑边界矩形的宽高比。如果形状是圆形,则该比率为 1。我们可以使用 boundingRect 函数来获取边界矩形的坐标。如果我们计算这个形状的半径并使用圆面积公式,那么它应该接近该轮廓的面积。我们可以使用 contourArea 函数计算图像中轮廓的面积。因此,我们可以使用这些条件并过滤掉形状。我们可以通过将搜索区域限制在面部或眼睛来进一步优化。

GreatX wechat
Subscribe to my blog by scanning my public wechat account.