优秀的编程知识分享平台

网站首页 > 技术文章 正文

预防疲劳驾驶系统原来这么简单!从零使用人工智能教你制作

nanyue 2024-08-26 17:59:32 技术文章 6 ℃

Keras,人脸识别,OpenCV和PIL的完整实战

在我们的生活中,因为疲劳驾驶而导致交通事故的案例举不胜举。对于每一个驾驶员来说开车的时候如果感觉到疲劳就需要立即找地方停下来,让身体得到适当的休息后再开车上路。

由于这种严重的错误,我着手开发一个神经网络,如果眼睛是闭着的,可以检测,而当与计算机视觉串联应用,以检测是否自己的眼睛关闭超过一秒钟。对于对提高驾驶安全性感兴趣的任何人,包括商业和日常驾驶员,汽车公司和车辆保险公司,这种技术都是有用的。

主要内容:

  1. 建立卷积神经网络
  2. 网络摄像头应用

数据采集

我们使用了来自多个来源的全脸数据,即来自UMass Amherst的睁眼数据和来自南京大学的闭眼数据。

然后,我们使用一个简单的python函数从该数据集中裁剪出眼睛,从而为我们提供了30,000多张裁剪后的眼睛图像。我们为每种图像裁剪添加了一个缓冲区,不仅可以吸引眼睛,还可以吸引眼睛周围的区域。稍后将在网络摄像头部分重新使用此裁剪功能。

# 从命令行安装
# brew install cmake
# pip install dlib 
# pip install face_recognition
# imports:
from PIL import Image, ImageDraw
import face_recognition
import os
def eye_cropper(folders):
    # 建立计数以进行迭代文件保存
    count = 0

    # For循环遍历每个图像文件
    for folder in os.listdir(folders):
        for file in os.listdir(folders + '/' + folder):
          
            # 在图像上使用面部识别库
            image = face_recognition.load_image_file(folders + '/' + folder + '/' + file)
            # 为面部特征坐标创建变量
            face_landmarks_list = face_recognition.face_landmarks(image)
          
            # 为眼睛坐标创建一个占位符列表
            eyes = []
            try:
                eyes.append(face_landmarks_list[0]['left_eye'])
                eyes.append(face_landmarks_list[0]['right_eye'])
            except:
                continue
            # 建立眼睛的最大x和y坐标
            for eye in eyes:
                x_max = max([coordinate[0] for coordinate in eye])
                x_min = min([coordinate[0] for coordinate in eye])
                y_max = max([coordinate[1] for coordinate in eye])
                y_min = min([coordinate[1] for coordinate in eye])
              # 确定x和y坐标的范围
                x_range = x_max - x_min
                y_range = y_max - y_min
              
                #确保捕捉到全眼,
                #计算具有50%的正方形的坐标
                #在更大范围的轴上增加了缓冲
                if x_range > y_range:
                    right = round(.5*x_range) + x_max
                    left = x_min - round(.5*x_range)
                    bottom = round(((right-left) - y_range))/2 + y_max
                    top = y_min - round(((right-left) - y_range))/2
                else:
                    bottom = round(.5*y_range) + y_max
                    top = y_min - round(.5*y_range)
                    right = round(((bottom-top) - x_range))/2 + x_max
                    left = x_min - round(((bottom-top) - x_range))/2
              
                #使用缓冲坐标裁剪原始图像
                im = Image.open(folders + '/' + folder + '/' + file)
                im = im.crop((left, top, right, bottom))
                
                #调整图片大小以输入模型
                im = im.resize((80,80))
              
                # 将文件保存到输出文件夹
                im.save('yourfolderforcroppedeyes')
              
                # 增加迭代文件保存的数量
                count += 1
                # 每200张照片打印一次以监视进度
                if count % 200 == 0:
                    print(count)
    
# 调用功能以裁剪全脸眼睛图像
eye_cropper('yourfullfaceimagefolder')

警告:手动清理此数据集,除非你希望人们眨眼或让200张Bill Clinton的照片训练模型。这是我们用于训练模型的数据示例:


创建卷积神经网络

确定指标

因为预测积极的等级(睡眠的驾驶员)比预测负面的等级(清醒的驾驶员)对我们更重要,所以我们最重要的指标将是召回率(敏感度)。召回率越高,模型错误地预测处于唤醒状态的睡眠驱动程序(假阴性)越少。

这里唯一的问题是,我们的积极阶层远远超过我们的消极阶层。因此,最好使用F1分数或Precision-Recall AUC分数,因为它们还考虑了我们猜测驾驶员睡着了但真正醒着的时间(精度)。否则,我们的模型将始终预测我们处于睡眠状态并且无法使用。我们不用于处理不平衡图像数据的另一种方法是使用图像增强。

准备图像数据

下一步是导入图像并对模型进行预处理。

本节所需的导入:

import cv2
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
from keras.models import Sequential
from keras.layers import Dense,Flatten,Conv2D,MaxPooling2D,Dropout

导入我们之前创建的图像并调整图像的大小,以便它们都匹配。对于此项目,我们将尺寸调整为80x80像素。这是使用OS库的简单导入功能:

def load_images_from_folder(folder, eyes = 0):
    count = 0
    error_count = 0
    images = []
    for filename in os.listdir(folder):
        try:
            img = cv2.imread(os.path.join(folder,filename))
            img = cv2.resize(img, (80,80)) ## Resizing the images
            ## 表示眼睛是否为0:睁开; 1:闭眼
            images.append([img, eyes])
        except:
            error_count += 1
            print('ErrorCount = ' + str(error_count))
            continue
        
        count += 1
        if count % 500 == 0:
            print('Succesful Image Import Count = ' + str(count))

    return images

folder="../data/train/new_open_eyes"
open_eyes = load_images_from_folder(folder, 0)

folder="../data/train/New_Closed_Eyes"
closed_eyes = load_images_from_folder(folder, 1)
eyes = close_eyes + open_eyes

设置变量,其中独立的X是图像,而相关的y是相应的标签(闭眼为1,睁眼为0):

X = [] 
y = [] 
for features, label in eyes: 
     X.append(features)
     y.append(label)

将图像转换为数组,以便可以输入模型。此外,将数据除以255即可缩放数据。

X = np.array(X).reshape(-1, 80, 80, 3)
y = np.array(y)
X = X/255.0

使用scikit Learn's将数据分为训练集和验证集train_test_split重要提示:使 一定要分层y方法,因为我们已经不平衡类。

X_train, X_test, y_train, y_test = train_test_split(X, y, stratify = y)

创建模型架构


卷积层:

该层创建像素的子集而不是整个图像,并允许更快的模型。根据您设置的滤镜数量,其密度可能会比原始图像高或低,但是它们将使模型能够使用更少的资源来学习更复杂的关系。我使用了32个过滤器。使用至少一个卷积层,通常需要两个或多个。对我来说,最佳设置是将两个3x3池合并在一起,然后将三个3x3池合并在一起。CNN的总体趋势是使用更小的过滤器尺寸。实际上,双重3x3层与5x5层基本相同,但是速度更快,并且通常会获得更高的得分,正如Arnault Chazareix在这篇精彩文章中所解释的。之后的合并并非总是必要或更好。尝试使用模型,如果可能,选择是否使用。

展平

确保展平图像阵列,以使其可以进入密集层。

致密层

层越密集,模型训练所需的时间就越长。随着这些层中神经元数量的增加,网络学习到的关系复杂性将增加。通常,卷积层的想法是避免必须进行过深的密集层方案。对于我们的模型,我们使用了三层具有relu激活的层,神经元速率逐渐降低(256、128、64)。我们还在每一层之后使用了30%的辍学率

输出层

最后,由于这是一个二进制分类问题,请确保对您的外层使用S形激活。

编译模型

在中model.compile(),您需要将指标设置为PR AUC(tf.keras.metrics.AUC (curve = 'PR')在tensorflow中)或调用(tf.keras.metrics.recall在tensorflow中)。设置损失等于,binary_crossentropy因为通常这是一个二进制分类模型和一个好的优化程序adam

拟合模型

通常将批次大小设置得尽可能大,但不要在此过程中炸毁机器!我在Google Colab的32 GB TPU上进行了网格搜索,并轻松地运行了1000多个批次。如有疑问,请尝试32个批次,如果不使内存过载,请增加批次。就时代而言,在20个时代之后收益递减,所以我不会比这个特定的CNN高很多。

这是Tensorflow Keras的完整设置:

#实例化模型
model = Sequential()

#添加前三个卷积层
model.add(Conv2D(
                filters = 32, #过滤器数量
                kernel_size = (3,3), #高度/过滤器的宽度
                activation = 'relu', #激活功能
                input_shape = (80,80,3) # 输入的图像形状
                ))
model.add(Conv2D(
                filters = 32, #过滤器数量
                kernel_size = (3,3), #高度/过滤器的宽度
                activation = 'relu' #激活功能 
                ))
model.add(Conv2D(
                filters = 32, #过滤器数量
                kernel_size = (3,3), #高度/过滤器的宽度
                activation = 'relu' #激活功能
                ))

#在卷积层之后添加池
model.add(MaxPooling2D(pool_size = (2,2))) # 正在合并的区域的尺寸

#添加第二组卷积层
model.add(Conv2D(
                filters = 32, #过滤器数量
                kernel_size = (3,3), #高度/过滤器的宽度
                activation = 'relu' #激活功能
                ))
model.add(Conv2D(
                filters = 32, #过滤器数量
                kernel_size = (3,3), #高度/过滤器的宽度
                activation = 'relu' #激活功能
                ))

#添加最后一个池化层。
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())

#添加第一个具有256个节点的密集层
model.add(Dense(256, activation='relu'))

#添加一个辍学层以避免过度拟合
model.add(Dropout(0.3))

model.add(Dense(124, activation='relu'))
model.add(Dropout(0.3)) 

model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))

#添加输出层
model.add(Dense(1, activation = 'sigmoid'))

#编译模型
model.compile(loss='binary_crossentropy',
                optimizer='adam',
                metrics=[tf.keras.metrics.AUC(curve = 'PR')])

#拟合模型
model.fit(X_train,
            y_train,
            batch_size=800,
            validation_data=(X_test, y_test),
            epochs=24)

#评估模型 
model.evaluate(X_test, y_test, verbose=1)

曲线分数下的最终精度召回区域:

0.981033

创建网络摄像头应用

拥有满意的模型后,请使用保存它model.save('yourmodelname.h5')。在保存生产模型时,请确保在没有验证数据的情况下运行生产模型。导入时,这会在旅途中引起问题。

安装和导入:

这些都是Mac优化的,尽管也可以在Windows上使用相同的脚本。

#网络摄像头应用程序所需的安装
install opencv-python
#如果要播放警报声音:
#pip install -U PyObjC 
#pip install playsound
import cv2
from playsound import playsound
# import model saved above
eye_model = keras.models.load_model(‘best_model.h5’)

使用OpenCV访问网络摄像头

使用cv2.VideoCapture(0)启动摄像头捕获。如果您要根据相对帧大小而不是绝对坐标来确定文本位置,请确保使用来保存网络摄像头的宽度和高度cap.get(cv2.CAP_PROP_FRAME_WIDTH)。您还可以每秒查看帧数。可在此处找到OpenCV的捕获属性的完整列表。

cap = cv2.VideoCapture(0)
w = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
h = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
print(cap.get(cv2.CAP_PROP_FPS))
if not cap.isOpened():
 raise IOError(‘Cannot open webcam’)

使用OpenCV捕获帧并裁剪它们

如果打算对被框框闭上的眼睛进行计数,请务必设置一个计数器。一个while True:循环将保持在相机上,直到你的脚本完成。在该while循环中,使用ret, frame = cap.read()格式捕获网络摄像头视频的帧。最后,在框架上调用该函数。它应该从帧中返回一个裁剪的眼睛,如果在帧中找不到眼睛,该函数将返回None无法被255除的眼睛,并跳至下一帧。

counter = 0
# 创建在使用网络摄像头时运行的while循环
while True:
  # 捕获网络摄像头输出的帧
  ret, frame = cap.read()
  # 帧上调用的函数
  image_for_prediction = eye_cropper(frame)
  try:
     image_for_prediction = image_for_prediction/255.0
  except:
     continue

通过模型运行框架

然后,我们可以通过模型运行图像并获得预测。如果预测值更接近于0,则我们在屏幕上显示“打开”。否则(即接近1),我们显示“已关闭”。请注意,如果模型检测到睁开的眼睛,计数器将重置为0,如果睁开的眼睛将计数器增加1。我们可以使用显示一些基本文字来指示眼睛是闭合还是睁开cv2.putText()

prediction = eye_model.predict(image_for_prediction)
if prediction < 0.5:
  counter = 0
  status = ‘Open’
  cv2.putText(frame, status, (round(w/2)-80,70),
cv2.FONT_HERSHEY_SIMPLEX, 2, (0,255,0), 2, cv2.LINE_4)
  
else:
  counter = counter + 1
  status = ‘Closed’
  cv2.putText(frame, status, (round(w/2)-104,70), cv2.FONT_HERSHEY_SIMPLEX, 2, (0,0,255), 2, cv2.LINE_4)

如果连续6帧闭着眼睛(“睡觉”),我们也要显示警报。可以使用简单的if语句完成此操作:

if counter > 5:
  cv2.putText(frame, ‘DRIVER SLEEPING’, (round(w/2)-136,round(h) — 146), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2, cv2.LINE_4)
  counter = 5

最后,我们需要显示框架并为while循环提供退出键。在cv2.waitKey(1)确定多长时间的帧将被显示。括号中的数字是显示帧的毫秒数,除非按下“ k”键(在这种情况下为27)或退出键:

cv2.imshow(‘Drowsiness Detection’, frame)
k = cv2.waitKey(1)
if k == 27:
  break

在循环之外,释放网络摄像头并关闭应用程序:

cap.release()
cv2.destroyAllWindows()

最终产品

有了一些风格上的补充,这就是最终的产品。

如您所见,该模型非常有效,尽管训练时间很长,但在几毫秒内即可返回预测。经过一些进一步的改进并导出到外部计算机,该程序可以很容易地在实际情况下应用,甚至可以挽救生命。

原创:Dustin Stewart

链接:https://towardsdatascience.com/drowsiness-detection-using-convolutional-neural-networks-face-recognition-and-tensorflow-56cdfc8315ad

Tags:

最近发表
标签列表