目录
介绍
在讨论基于人工智能(AI)或深度学习(DL)的计算机视觉(CV)时,我们通常会想象一台功能强大的台式机或服务器处理图像或视频。但有时,我们需要在便携式设备上运行复杂的CV算法。
例如,要创建一个计算机系统以防止驾驶员分心,最实用的解决方案是配备专用软件的独立设备。然后,驾驶员、车队经理或制造商可以将此类设备放入车辆中,以在驾驶员可能分心时提醒他们。
那么,我们可以在便携式Arm驱动的设备上运行复杂的算法吗?在本文中,我们将演示如何创建分心驾驶员检测器,并展示如何在Raspberry Pi设备上运行它。我们将使用Python开发我们的程序,用于计算机视觉算法的OpenCV,以及用于检测可能的驾驶员分心的卷积神经网络(CNN)。
发明算法
我们将使用一个简单的检测类型来检查眼睛是否在短时间内闭合。我们可以描述许多其他的分心症状,但这个可能是最可靠的。
现代人工智能算法可以以最小的努力完成这项任务。一种方法使用特殊的CNN来检测所谓的面部标志。下图是一个极其常见的68点面部标志图。
使用眼点地标坐标,我们可以计算出眼睛的高宽比。当眼睛闭上时,这个比率会显着降低。通过跟踪这些数据,我们可以检测到潜在的分心时刻。
获取面部标志的常用方法是检测面部边界框(面部周围的框)并在其中定位标志坐标。因此,该算法需要两个成分——人脸检测器和地标评估器。我们将为这两个子任务使用深度神经网络(DNN)。您可以在GitLab上找到人脸检测TensorFlow模型。对于面部地标评估器,我们将使用这个Caffe模型。
检测面部地标
让我们开始为我们的面部标志检测算法编写代码。我们从基于DNN模型的人脸检测器开始。
class TF_FD:
def __init__(self, model, graph, min_size, min_confidence):
self.min_size = min_size
self.min_confidence = min_confidence
self.detector = cv2.dnn.readNetFromTensorflow(model, graph)
l_names = self.detector.getLayerNames()
if len(l_names)>0:
print('Face detector loaded:')
else:
print('Face detector loading FAILED')
def detect(self, frame):
width = frame.shape[1]
height = frame.shape[0]
inputBlob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), \
(104.0, 177.0, 123.0), True, False)
self.detector.setInput(inputBlob, 'data');
detection = self.detector.forward('detection_out');
n = detection.shape[2]
detected = []
for i in range(n):
conf = detection[0, 0, i, 2]
if conf >= self.min_confidence:
x1 = detection[0, 0, i, 3]
y1 = detection[0, 0, i, 4]
x2 = detection[0, 0, i, 5]
y2 = detection[0, 0, i, 6]
# skip faces out of the frame
if Utils.point_is_out(x1,y1) or Utils.point_is_out(x2, y2):
continue
fw = (x2-x1)*width
fh = (y2-y1)*height
if (fw>=self.min_size) and (fh>=self.min_size):
r = (x1, y1, x2, y2)
d = (conf, r)
detected.append(d)
return detected
这个简单的类提供了用于从指定的模型和图形文件加载TensorFlow神经网络的构造函数。OpenCV框架的cv2.dnn模块提供了加载许多流行格式的DNN模型的方法。构造函数有两个附加参数:最小人脸大小和最小检测置信度。
该类的detect方法接收一个参数,frame即图像或视频帧。该函数创建一个blob对象(一个特殊的4D数组,我们将其用作检测器的输入数据)。
请注意,我们为模型blobFromImage函数中的参数使用了一些特定值。如果您使用其他人脸检测模型,请记住根据需要更改值。
接下来,我们运行检测器调用forward方法并为所有满足我们标准(最小尺寸和置信度)的人脸提取数据(检测置信度和边界框)。
我们接下来开发第二类,面部标志检测器:
class CAFFE_FLD:
def __init__(self, model, proto):
self.detector = cv2.dnn.readNetFromCaffe(proto, model)
l_names = self.detector.getLayerNames()
if len(l_names)>0:
print('Face landmarks detector loaded:')
else:
print('Face landmarks detector loading FAILED')
def get_face_rect(self, frame, face):
width = frame.shape[1]
height = frame.shape[0]
(conf, rect) = face
(x1, y1, x2, y2) = rect
fw = (x2-x1)*width
fh = (y2-y1)*height
if fw>fh:
dx = (fw-fh)/(2*width)
x1 = x1+dx
x2 = x2-dx
else:
dy = (fh-fw)/(2*height)
y1 = y1+dy
y2 = y2-dy
x1 = Utils.fit(x1)
y1 = Utils.fit(y1)
x2 = Utils.fit(x2)
y2 = Utils.fit(y2)
rect = (x1, y1, x2, y2)
return rect
def get_frame_points(self, face_rect, face_points):
(x1, y1, x2, y2) = face_rect
fw = (x2-x1)
fh = (y2-y1)
n = len(face_points)
frame_points = []
for i in range(n):
v = face_points[i]
if (i % 2) == 0:
dv = x1
df = fw
else:
dv = y1
df = fh
v = dv+v*df
frame_points.append(v)
return frame_points
def get_face_image(self, frame, face):
width = frame.shape[1]
height = frame.shape[0]
(conf, rect) = face
(x1, y1, x2, y2) = rect
rect = self.get_face_rect(frame, face)
(xi1, yi1, xi2, yi2) = Utils.rect_to_abs(rect, width, height)
roi = frame[yi1:yi2, xi1:xi2]
gray = cv2.cvtColor(roi, cv2.COLOR_RGB2GRAY)
resized = cv2.resize(gray, (60, 60), 0.0, 0.0, interpolation=cv2.INTER_CUBIC)
return (rect, gray, resized)
def detect(self, f_img):
width = f_img.shape[1]
height = f_img.shape[0]
inputBlob = cv2.dnn.blobFromImage(f_img, 1/127.5, (60, 60), (127.5))
self.detector.setInput(inputBlob, 'data');
detection = self.detector.forward();
points = detection[0]
return points
这个类也在初始化时加载了一个DNN模型,但是它使用了另一个函数,因为这个模型是Caffe的特定格式。主要方法detect再次创建一个blob并运行神经网络以获取面部标志。在这种情况下,检测方法接收的不是整个帧,而是包含一个人脸的帧中经过特殊处理的部分。
我们可以使用专门为此目的设计的get_face_image方法生成这个“人脸图像” 。它找到包含人脸的方框,从帧中裁剪,将蓝、绿、红(BGR)数据转换为灰度图像(因为我们已经在灰度图像上训练了DNN模型),并将图像大小调整为使用高质量插值方法的60x60像素。
在Raspberry Pi上运行地标检测
现在我们已经设计了面部标志检测器,我们应该在Arm驱动的设备上对其进行测试,以验证它可以以足够的每秒帧数(FPS)运行算法。我们将在Raspberry Pi 4 Model B设备上执行测试。我们已经使用预编译的二进制包在设备上安装了Python OpenCV框架。如果您使用其他设备,则应按照适当的指南安装其软件包。
在本文中,我们不会使用特殊的AI框架,并且神经网络是在GPU或TPU上不进行加速处理的。因此,所有ML工作负载仅在设备的CPU上运行。
我们将使用视频文件进行所有测试,以确保实验的可重复性。视频设置在办公室,但模仿了开车时的场景。
以下类在视频文件上运行面部标志检测:
class VideoFLD:
def __init__(self, fd, fld):
self.fd = fd
self.fld = fld
def process(self, video):
frame_count = 0
detection_num = 0;
dt = 0
dt_l = 0
capture = cv2.VideoCapture(video)
img = None
dname = 'Arm-Powered Driver Distraction Detection'
cv2.namedWindow(dname, cv2.WINDOW_NORMAL)
cv2.resizeWindow(dname, 720, 720)
# Capture all frames
while(True):
(ret, frame) = capture.read()
if frame is None:
break
frame_count = frame_count+1
# work with square images
width = frame.shape[1]
height = frame.shape[0]
if not (width == height):
dx = int((width-height)/2)
frame = frame[0:height, dx:dx+height]
t1 = time.time()
faces = self.fd.detect(frame)
t2 = time.time()
dt = dt + (t2-t1)
f_count = len(faces)
detection_num += f_count
draw_points = []
if (f_count>0):
for (i, face) in enumerate(faces):
t1 = time.time()
(fi_rect, fi_gray, fi_resized) = self.fld.get_face_image(frame, face)
points = self.fld.detect(fi_resized)
frame_points = self.fld.get_frame_points(fi_rect, points)
t2 = time.time()
dt_l = dt_l + (t2-t1)
draw_points.append(frame_points)
if len(faces)>0:
Utils.draw_faces(faces, (255, 0, 0), 1, frame, True)
if len(draw_points)>0:
for (i, points) in enumerate(draw_points):
Utils.draw_points(points, (0, 0, 255), 1, frame)
# Display the resulting frame
cv2.imshow(dname,frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
capture.release()
cv2.destroyAllWindows()
fps = 0.0
if dt>0:
fps = frame_count/dt
fps_l = 0.0
if dt_l>0:
fps_l = detection_num/dt_l
return (detection_num, fps, fps_l)
在这里,我们使用我们的面部和地标检测器来提供主要功能。我们使用OpenCV库中的VideoCapture类从视频文件中读取帧并将它们提供给检测器。
现在我们可以使用以下代码运行算法:
w_path = '/home/pi/Desktop/PI_DD'
n_path = os.path.join(w_path, 'net')
fd_model = os.path.join(n_path, 'opencv_face_detector_uint8.pb')
fd_graph = os.path.join(n_path, 'opencv_face_detector.pbtxt')
fd = TF_FD(fd_model, fd_graph, 30, 0.5)
fld_model = os.path.join(n_path, 'face_landmarks.caffemodel')
fld_proto = os.path.join(n_path, 'face_landmarks.prototxt')
fld = CAFFE_FLD(fld_model, fld_proto)
v_path = os.path.join(w_path, 'video')
v_name = 'v_1.mp4'
v_file = os.path.join(v_path, v_name)
vfld = VideoFLD(fd, fld)
(detection_num, fps, fps_l) = vfld.process(v_file)
print("Face detections: "+str(detection_num))
print("Detection FPS: "+str(fps))
print("Landmarks FPS: "+str(fps_l))
您可以在以下视频中看到筛选结果:
我们的面部标志检测算法运行良好,并以合理的精度定位参考点。它为我们提供了大约2 FPS的人脸检测速度和大约60 FPS的地标评估速度。考虑到我们只使用Pi的CPU,这绝对是可用的,而且还不错。
这个速度应该足以在一到三秒内检测到闭眼,适用于驾驶员分心的实际情况。因此,对于我们的分心检测任务来说,它应该已经足够好了。
实现驾驶员分心检测
我们距离完成的分心驾驶员检测算法仅一步之遥:编写评估眼睛高宽比的算法并跟踪它以评估可能分心的时刻。
首先,我们在CAFFE_FLD类中添加两个简单的方法:
def get_eye_points(self, face_points, eye_id):
i0 = 72
i1 = i0+12*(eye_id-1)
i2 = i1+12
eye_points = face_points[i1:i2]
return eye_points
def get_eye_ratio(self, eye):
n = int(len(eye)/2)
pts = np.array(eye, dtype=np.float32)
pts = pts.reshape([n, 2])
rect = cv2.minAreaRect(pts)
(w, h) = rect[1]
if (w>h):
ratio = h/w
else:
ratio = w/h
return ratio
该get_eye_points方法从68个面部标志的阵列中提取眼睛的点。该get_eye_ratio方法评估眼睛的高宽比。
现在我们可以编写代码来跟踪比率值并检测可能分心的时刻。
class DERD:
def __init__(self, ratio_thresh, delta_time, eyes=2):
self.ratio_thresh = ratio_thresh
self.delta_time = delta_time
self.eyes = eyes
self.eye_closed_time = 0.0
self.last_time = 0.0
def start(self, time):
self.eye_closed_time = 0.0
self.last_time = time
def detect(self, eye1_ratio, eye2_ratio, time):
dt = time - self.last_time
distraction = False
d1 = (eye1_ratio<self.ratio_thresh)
d2 = (eye2_ratio<self.ratio_thresh)
if self.eyes == 2:
d = d1 and d2
else:
d = d1 or d2
if d:
self.eye_closed_time += dt
else:
self.eye_closed_time -= dt
if self.eye_closed_time<0.0:
self.eye_closed_time = 0.0
print('Eye 1: '+str(eye1_ratio))
print('Eye 2: '+str(eye2_ratio))
print('Eye closed time = '+str(self.eye_closed_time))
if self.eye_closed_time>=self.delta_time:
distraction = True
self.start(time)
self.last_time = time
return distraction
ratio_thresh参数是假设眼睛闭合时的高宽比的最小值。该delta_time参数表示眼睛必须闭上多长时间才能确定是否发生了分心。该eyes参数确定是否必须关闭一只或两只眼睛才能将其视为分心。
最后,我们稍微修改了我们的视频检测器,将这种分心检测算法包含在代码中,并在检测发生时生成警报。
class VideoDDD:
def __init__(self, fd, fld, eye_ratio_thresh=0.2, eyes=2, delta_time=2.0):
self.fd = fd
self.fld = fld
self.derd = DERD(eye_ratio_thresh, delta_time, eyes)
def process(self, video):
frame_count = 0
detection_num = 0;
dt = 0
dt_l = 0
capture = cv2.VideoCapture(video)
img = None
dname = 'Arm-Powered Driver Distraction Detection'
cv2.namedWindow(dname, cv2.WINDOW_NORMAL)
cv2.resizeWindow(dname, 720, 720)
# just suppose FPS=25
delta = 0.040
dd_time = -1000
draw_points = []
faces = []
# Capture all frames
while(True):
frame_t1 = time.time()
(ret, frame) = capture.read()
if frame is None:
break
frame_count = frame_count+1
frame_time = (frame_count-1)*delta
if frame_count==1:
self.derd.start(frame_time)
# work with square images
width = frame.shape[1]
height = frame.shape[0]
if not (width == height):
dx = int((width-height)/2)
frame = frame[0:height, dx:dx+height]
f_count = 0
if (frame_count % 10) == 0:
faces = []
draw_points = []
t1 = time.time()
faces = self.fd.detect(frame)
t2 = time.time()
dt = dt + (t2-t1)
f_count = len(faces)
detection_num += 1
distraction = False
if (f_count>0):
# supposed one face at the camera
face = faces[0]
t1 = time.time()
(fi_rect, fi_gray, fi_resized) = self.fld.get_face_image(frame, face)
points = self.fld.detect(fi_resized)
frame_points = self.fld.get_frame_points(fi_rect, points)
t2 = time.time()
dt_l = dt_l + (t2-t1)
draw_points.append(frame_points)
eye1 = self.fld.get_eye_points(frame_points, 1)
eye2 = self.fld.get_eye_points(frame_points, 2)
#draw_points.append(eye1)
#draw_points.append(eye2)
r1 = self.fld.get_eye_ratio(eye1)
r2 = self.fld.get_eye_ratio(eye2)
distraction = self.derd.detect(r1, r2, frame_time)
if len(faces)>0:
Utils.draw_faces(faces, (255, 0, 0), 1, frame, True)
if len(draw_points)>0:
for (i, points) in enumerate(draw_points):
Utils.draw_points(points, (0, 0, 255), 1, frame)
# Show distraction alarm for 1 second
if distraction:
dd_time = frame_time
if dd_time>0:
text = "ALARM! DRIVER DISTRACTION"
xd1 = 10
yd1 = 50
cv2.putText(frame, text, (xd1, yd1), \
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1, cv2.LINE_AA)
if (frame_time-dd_time)>1.0:
dd_time = -1000
# Display the resulting frame
cv2.imshow(dname,frame)
frame_t2 = time.time()
frame_dt = frame_t2 - frame_t1
if frame_dt<delta:
frame_dt = delta-frame_dt
#print('Sleep='+str(frame_dt))
time.sleep(frame_dt)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
capture.release()
cv2.destroyAllWindows()
fps = 0.0
if dt>0:
fps = detection_num/dt
fps_l = 0.0
if dt_l>0:
fps_l = detection_num/dt_l
return (detection_num, fps, fps_l)
除了使用DERD类,我们稍微改变了帧处理算法。我们添加了帧的时间戳比较来估计可能分心的时间间隔。此外,我们现在只处理每10个帧来模拟近乎实时的处理。
现在我们可以使用以下代码运行完整的驾驶员分心检测算法:
w_path = '/home/pi/Desktop/PI_DD'
n_path = os.path.join(w_path, 'net')
fd_model = os.path.join(n_path, 'opencv_face_detector_uint8.pb')
fd_graph = os.path.join(n_path, 'opencv_face_detector.pbtxt')
fd = TF_FD(fd_model, fd_graph, 30, 0.5)
fld_model = os.path.join(n_path, 'face_landmarks.caffemodel')
fld_proto = os.path.join(n_path, 'face_landmarks.prototxt')
fld = CAFFE_FLD(fld_model, fld_proto)
v_path = os.path.join(w_path, 'video')
v_name = 'v_1.mp4'
v_file = os.path.join(v_path, v_name)
vddd = VideoDDD(fd, fld, 0.3, 1, 2.0)
(detection_num, fps, fps_l) = vddd.process(v_file)
print("Face detections: "+str(detection_num))
print("Detection FPS: "+str(fps))
print("Landmarks FPS: "+str(fps_l))
您可以看到,当眼睛在足够长的时间间隔内似乎是闭合的并生成警报时,该算法正确地处理了这种情况。
这有助于我们找出导致司机分心的一个原因——当司机低头看着他们腿上的移动设备时,我们的睡意检测器会将他们识别为分心,因为它只看到他们的眼睑。随着世界各地的司法管辖区禁止在驾驶时使用移动设备,司机们试图通过将设备放在视线之外来适应。但是我们的分心检测器会通过检测他们的眼睛何时没有完全睁开来捕捉他们。
方便的是,该算法还可以用于检测驾驶员的睡意。我们的设备应该发出警报,无论司机的眼睛只是因为低头看移动设备而显得闭着,还是因为司机昏昏欲睡或睡着而实际上是闭着的。
该算法还可以正确处理闭眼短时间间隔(例如,驾驶员眨眼)或头部短时间轻微转动的情况。
下一步
我们已经使用面部标志实现了一种驾驶员分心算法——但我们可以添加其他算法!例如,我们可能希望通过测量鼻子标志之间的线的角度来检测驾驶员何时转动头部。我们还可以通过比较上下嘴巴标志之间的距离随时间变化来检查驾驶员的嘴巴是否似乎在张开和闭合。如果是,则可能意味着驾驶员在开车时正在说话或吃饭。
更进一步,我们可能会考虑升级到可以进行虹膜检测的ML模型,并尝试确定驾驶员的眼睛何时没有注视道路。
在本文中,我们展示了为便携式Arm供电设备开发AI计算机视觉应用程序是多么简单。我们选择此解决方案是出于实用性,因为我们的驾驶员分心检测系统必须在驾驶汽车中自动运行。我们展示了该应用程序可以在基于Arm的设备上以实时模式运行,达到大约2 FPS的处理速度。
尽管如此,我们仍然可以研究许多方面来改进这种驾驶员分心检测系统。例如,我们可以提高FPS吗?要回答这个问题,我们应该注意应用程序最慢的部分——使用TensorFlow神经网络进行人脸检测。我们可以改进这个模型的性能吗?是的。我们可以使用Arm的Arm NN库,该库是他们专门开发的,用于在Arm驱动的设备上加速处理DNN模型。
借助Arm NN库,我们还可以在连接的GPU或NPU单元上运行NN模型,以实现接近实时的速度。这将使我们更灵活地发明驾驶员分心检测的高级算法或使用其他人脸检测DNN模型,如BlazeFace神经模型。
我们解决方案的其他改进可能涉及生成新的分心标准。例如,我们可以推断,如果司机的眼睛或头部朝向别处超过确定的时间间隔,他们可能会分心。
我们希望这些想法激起了您的兴趣。我们鼓励您扩展此解决方案或在Arm驱动的设备上创建自己的便携式AI解决方案。
https://www.codeproject.com/Articles/5324279/Arm-Powered-Driver-Distraction-Detection