当前位置:   article > 正文

基于人脸特征点实现疲劳检测_基于人脸识别的疲劳驾驶检测

基于人脸识别的疲劳驾驶检测

为了有效监测驾驶员是否疲劳驾驶、避免交通事故的发生,提出了一种利用人脸特征点进行实时疲劳驾驶检测的新方法。对驾驶员驾驶时的面部图像进行实时监控,首先检测人脸,并利用ERT算法定位人脸特征点;然后根据人脸眼睛区域的特征点坐标信息计算眼睛纵横比EAR来描述眼睛张开程度,根据合适的EAR阈值可判断睁眼或闭眼状态;最后基于EAR实测值和EAR阈值对监控视频计算闭眼时间比例(PERCLOS)值度量驾驶员主观疲劳程度,将其与设定的疲劳度阈值进行比较即可判定是否疲劳驾驶。

一、 人脸特征点检测

      人脸特征点检测基于该类库实现(https://github.com/610265158/Peppa_Pig_Face_Engine), 我尝试过很多开源框架,包括 dlib, openface,pfld,clnf 等,在闭眼检测方面,表现都不是十分理想,后来发现这个类库,哇,眼前一亮,检测的很牛逼,而且很稳定。在 i7 八代的cpu上,识别一帧大概平均在40ms左右( 因为项目主机上没有gpu ,所以没有测试过gpu的检测速度 ,但应该很快),  详细参考该作者文章:人脸关键点检测 face keypoint detect_小羊苏西的博客-CSDN博客_keypoint 检测, 写的真的很牛逼,不牛逼你找我。

在我的数据集上检测闭眼的效果图:

   

                            


二、训练自己的数据集( 基于 tf1 )

因为开源数据集中包含闭眼的数据太少了,所以需要我们自己手动增加 ,这里我使用的是 dlib 的标注工具。

    1. 标注数据集 , 因为主要检测眼睛和头部姿态,所以我标注了37个点。(大概标注了5000多张)

                    

    ** 标注工具: (由于标注过程中经常出错,所以增加了撤销等功能)

    2. 标注完成后,会生成一个xml文件,里面包含所有的标注信息 (最好检测下,不要标记少点或者多点情况),然后做下面操作

        1.  打乱顺序

imglab --shuffle dataset_0402.xml

         2. 分隔数据集 ( 训练集 和 测试集 )

imglab --split-train-test 0.95 dataset_0402.xml

        3. 还可以 翻转数据集 、去除相似样本等操作

  1. imglab --rmdupes xml/mydataset.xml ## 去除相似样本
  2. imglab --flip xml/mydataset.xml ## 翻转图片

        4. 更多详细操作参考: 【AI】dlib中图像标注工具 imglab 详细说明_郭老二-CSDN博客_imglab

    3. 将训练集和测试集转换成作者提供的格式

  1. import json
  2. from xml.dom.minidom import parse
  3. from tqdm import tqdm
  4. def json_to_txt(json_file, txt_file):
  5. txt_file = open(txt_file, mode='w')
  6. with open(json_file, 'r') as f:
  7. data = json.load(f)
  8. tmp_str = ""
  9. for sub_data in data:
  10. file_name = sub_data['image_path']
  11. tmp_str += file_name + '|'
  12. key_points = sub_data['keypoints']
  13. for points in key_points:
  14. tmp_str = tmp_str + str(points[0]) + ' ' + str(points[1]) + ' '
  15. tmp_str = tmp_str + '\n'
  16. txt_file.write(tmp_str)
  17. def read_xml_to_json(path, out_file_path):
  18. domTree = parse(path)
  19. # 文档根元素
  20. rootNode = domTree.documentElement
  21. images = rootNode.getElementsByTagName("image")
  22. with open(out_file_path, 'w') as f:
  23. train_json_list = []
  24. for image in tqdm(images):
  25. one_image_ann = {}
  26. if image.hasAttribute("file"):
  27. info = ""
  28. # 文件路径
  29. file_path = image.getAttribute("file")
  30. print("path:" + file_path)
  31. one_image_ann['image_path'] = file_path
  32. box = image.getElementsByTagName("box")
  33. top = box[0].getAttribute("top")
  34. left = box[0].getAttribute("left")
  35. width = box[0].getAttribute("width")
  36. height = box[0].getAttribute("height")
  37. print("top:" + top + " left:" + left + " width:" + width + " height:" + height)
  38. bbox = [float(top), float(left), float(width), float(height)]
  39. parts = box[0].getElementsByTagName("part")
  40. if len(parts) == 0:
  41. continue
  42. key = []
  43. for part in parts:
  44. key.append([float(part.getAttribute("x")), float(part.getAttribute("y"))])
  45. print("x:" + part.getAttribute("x") + " y:" + part.getAttribute("y"))
  46. one_image_ann['keypoints'] = key
  47. one_image_ann['bbox'] = bbox
  48. one_image_ann['attr'] = None
  49. train_json_list.append(one_image_ann)
  50. json.dump(train_json_list, f, indent=2)
  51. def read_xml_to_txt(path, out_txt_file_path):
  52. domTree = parse(path)
  53. # 文档根元素
  54. rootNode = domTree.documentElement
  55. images = rootNode.getElementsByTagName("image")
  56. with open(out_txt_file_path, 'w') as f:
  57. txt_str = ""
  58. for image in tqdm(images):
  59. if image.hasAttribute("file"):
  60. # 文件路径
  61. file_path = image.getAttribute("file")
  62. txt_str += file_path + '|'
  63. # print("path:" + file_path)
  64. box = image.getElementsByTagName("box")
  65. parts = box[0].getElementsByTagName("part")
  66. if len(parts) == 0:
  67. continue
  68. key = []
  69. for part in parts:
  70. key.append([float(part.getAttribute("x")), float(part.getAttribute("y"))])
  71. txt_str = txt_str + str(float(part.getAttribute("x"))) + ' ' + str(
  72. float(part.getAttribute("y"))) + ' '
  73. # print("x:" + part.getAttribute("x") + " y:" + part.getAttribute("y"))
  74. txt_str = txt_str + '\n'
  75. f.write(txt_str)
  76. if __name__ == '__main__':
  77. data_path = ["data/test.xml", "data/train.xml"]
  78. out_path = ["data/test.txt", "data/train.txt"]
  79. for path, out in zip(data_path, out_path):
  80. read_xml_to_txt(path, out)

    4.  配置训练参数

        1. 修改特征点下标,因为作者使用的数据集是基于68点的,所以说下标肯定是不同的,如果你标注点的顺序和作者使用的数据              集一样,则不需要更改。

        2. 配置数据集训练和测试路径以及其他的一些参数,参考 train_config.py。

    5.  修改数据读取方式

        1. 如果直接开始训练,会报这个错: Can't pickle local object 'DataFromGenerator.init..'  , 因为作者是在linux下训练的,而我是在windows下训练,可能window不支持这个多线程预加载。如果我们将这个 ds = MultiProcessPrefetchData(ds, self.prefetch_size, self.process_num) 注释掉,重新训练,会发现训练速度很慢,迭代10次,大概需要耗时 1min , gpu 利用率大概在 2% 左右,我猜可能是因为我们没有做预加载处理,导致大部分时间都耗时在读取数据上。

        2. 通过 tfrecord 方式来读取数据,这样可以大大加快我们的训练速度,在 mx150的gpu上测试,10迭代耗时大概在5s左右,提高了至少10倍的速度,但唯一难受的是,生成的 record 文件太大了(解决文件太大,看后面)。 下面是实现过程:

               1. 生成 tfrecord 文件

  1. # 写这段代码的时候,只有上帝和我知道它是干嘛的
  2. # 现在,只有上帝知道
  3. # @File : generate_tfrecord.py
  4. # @Time : 2020/4/24 14:10
  5. # @Author : J.
  6. # @desc : 生成 tfrecord 文件
  7. import tensorflow as tf
  8. from lib.dataset.dataietr import FaceKeypointDataIter
  9. from train_config import config as cfg
  10. from tqdm import tqdm
  11. import argparse
  12. import sys
  13. def int64_feature(value):
  14. return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
  15. def bytes_feature(value):
  16. return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
  17. def create_tf_example(image_file, is_train):
  18. crop_image, label = _train_data._map_func(image_file, is_train)
  19. tf_example = tf.train.Example(
  20. features=tf.train.Features(
  21. feature={
  22. 'image': bytes_feature(crop_image.tobytes()),
  23. 'label': bytes_feature(label.tobytes())
  24. }
  25. ))
  26. return tf_example
  27. def generate_tfrecord(images_files, record_path, is_train=True):
  28. num_tf_example = 0
  29. writer = tf.python_io.TFRecordWriter(record_path)
  30. with tqdm(images_files, ncols=100) as files:
  31. for image in files:
  32. tf_example = create_tf_example(image, is_train)
  33. writer.write(tf_example.SerializeToString())
  34. num_tf_example += 1
  35. # if num_tf_example % 100 == 0:
  36. # print("Create %d TF_Example" % num_tf_example)
  37. writer.close()
  38. print("{} tf_examples has been created successfully, which are saved in {}".format(num_tf_example, record_path))
  39. def main(_):
  40. global _train_data
  41. global _val_data
  42. _train_data = FaceKeypointDataIter(cfg.TRAIN.batch_size, cfg.TRAIN.epoch, cfg.DATA.root_path,
  43. FLAGS.train_data,
  44. True)
  45. _val_data = FaceKeypointDataIter(cfg.TRAIN.batch_size, cfg.TRAIN.epoch, cfg.DATA.root_path,
  46. FLAGS.val_data,
  47. False)
  48. print("================== generate train tf_record start ===================")
  49. train_images_files = _train_data.get_parse_file()
  50. generate_tfrecord(train_images_files, FLAGS.train_save_path, True)
  51. print("================== generate train tf_record end ===================")
  52. print("================== generate val tf_record start ===================")
  53. val_images_files = _val_data.get_parse_file()
  54. generate_tfrecord(val_images_files, FLAGS.val_save_path, False)
  55. print("================== generate val tf_record end ===================")
  56. if __name__ == '__main__':
  57. parser = argparse.ArgumentParser()
  58. parser.add_argument(
  59. '--train_data',
  60. type=str,
  61. default='data/train.txt',
  62. help='训练数据.')
  63. parser.add_argument(
  64. '--val_data',
  65. type=str,
  66. default='data/test.txt',
  67. help='验证数据.')
  68. parser.add_argument(
  69. '--train_save_path',
  70. type=str,
  71. default='record/train.record',
  72. help='生成训练数据路径.')
  73. parser.add_argument(
  74. '--val_save_path',
  75. type=str,
  76. default='record/val.record',
  77. help='生成验证数据路径.')
  78. FLAGS, unparsed = parser.parse_known_args()
  79. tf.app.run(main=main, argv=[sys.argv[0]] + unparsed)

               2. 读取 record 数据

  1. class read_face_data():
  2. def __init__(self, tfrecord_path, batch_size, out_channel, win, hin, num_threads):
  3. self.tfrecord_path = tfrecord_path
  4. self.batch_size = batch_size
  5. self.win = win
  6. self.hin = hin
  7. self.num_threads = num_threads
  8. self.out_channel = out_channel
  9. def read_and_decode(self):
  10. filename_queue = tf.train.string_input_producer([self.tfrecord_path], shuffle=False)
  11. reader = tf.TFRecordReader()
  12. _, serialized_example = reader.read(filename_queue)
  13. features = tf.parse_single_example(serialized_example,
  14. features={
  15. 'image': tf.FixedLenFeature([], tf.string),
  16. 'label': tf.FixedLenFeature([], tf.string)
  17. })
  18. images = tf.decode_raw(features['image'], tf.float32)
  19. images = tf.reshape(images, [self.win, self.hin, 3])
  20. labels = tf.decode_raw(features['label'], tf.float32)
  21. labels = tf.reshape(labels, [self.out_channel])
  22. # capacity:队列中元素的最大数量
  23. # min_after_dequeue出队后队列中元素的最小数量,用于确保元素的混合级别
  24. _images, _labels = tf.train.shuffle_batch([images, labels],
  25. num_threads=self.num_threads,
  26. batch_size=self.batch_size,
  27. capacity=self.batch_size * 2,
  28. min_after_dequeue=self.batch_size)
  29. return _images, _labels

                3. 修改  net_work.py 

                    修改 loop 、_train、_val 方法

  1. def loop(self, ):
  2. self.build()
  3. self.load_weight()
  4. sess = tf.Session()
  5. train_face_data = read_face_data(cfg.DATA.train_txt_path, cfg.TRAIN.batch_size, cfg.MODEL.out_channel,
  6. cfg.MODEL.win, cfg.MODEL.hin, 8)
  7. self.train_image_data, self.train_label_data = train_face_data.read_and_decode()
  8. val_face_data = read_face_data(cfg.DATA.val_txt_path, cfg.TRAIN.batch_size, cfg.MODEL.out_channel,
  9. cfg.MODEL.win, cfg.MODEL.hin, 8)
  10. self.val_image_data, self.val_label_data = val_face_data.read_and_decode()
  11. init_op = tf.global_variables_initializer()
  12. sess.run(init_op)
  13. ## 启动多线程处理输入数据
  14. coord = tf.train.Coordinator()
  15. threads = tf.train.start_queue_runners(sess=sess, coord=coord)
  16. with self._graph.as_default():
  17. # Create a saver.
  18. self.saver = tf.train.Saver(tf.global_variables(), max_to_keep=None)
  19. # Build the summary operation from the last tower summaries.
  20. self.summary_op = tf.summary.merge(self.summaries)
  21. self.summary_writer = tf.summary.FileWriter(cfg.MODEL.model_path, self._sess.graph)
  22. # epoch 2000
  23. min_loss_control = 1000.
  24. for epoch in range(cfg.TRAIN.epoch):
  25. self._train(epoch, sess)
  26. val_loss = self._val(epoch, sess)
  27. logger.info('**************'
  28. 'val_loss %f ' % (val_loss))
  29. # tmp_model_name=cfg.MODEL.model_path + \
  30. # 'epoch_' + str(epoch ) + \
  31. # 'L2_' + str(cfg.TRAIN.weight_decay_factor) + \
  32. # '.ckpt'
  33. # logger.info('save model as %s \n'%tmp_model_name)
  34. # self.saver.save(self.sess, save_path=tmp_model_name)
  35. if 1:
  36. min_loss_control = val_loss
  37. low_loss_model_name = cfg.MODEL.model_path + \
  38. 'epoch_' + str(epoch) + \
  39. 'L2_' + str(cfg.TRAIN.weight_decay_factor) + '.ckpt'
  40. logger.info('A new low loss model saved as %s \n' % low_loss_model_name)
  41. self.saver.save(self._sess, save_path=low_loss_model_name)
  42. self._sess.close()
  43. sess.close()
  44. coord.request_stop()
  45. coord.join(threads)
  46. def _train(self, _epoch, sess):
  47. # config.TRAIN.train_set_size // config.TRAIN.num_gpu // config.TRAIN.batch_size
  48. for step in range(cfg.TRAIN.iter_num_per_epoch):
  49. self.ite_num += 1
  50. start_time = time.time()
  51. # 64 * 160 * 160 *3 64*143
  52. example_images, example_labels = sess.run([self.train_image_data, self.train_label_data])
  53. # example_images, example_labels = next(self.train_ds)
  54. # example_images = train_iter_data['image']
  55. # example_labels = train_iter_data['label']
  56. ########show_flag check the data
  57. if cfg.TRAIN.vis:
  58. for i in range(cfg.TRAIN.batch_size):
  59. example_image = example_images[i, :, :, :] / 255.
  60. example_label = example_labels[i, :]
  61. Landmark = example_label[0:136]
  62. cla = example_label[136:]
  63. # print(np.max(example_image))
  64. # print(np.min(example_image))
  65. # print(Landmark)
  66. print(cla)
  67. Landmark = Landmark.reshape([-1, 2])
  68. _h, _w, _ = example_image.shape
  69. for _index in range(Landmark.shape[0]):
  70. x_y = Landmark[_index]
  71. cv2.circle(example_image, center=(int(x_y[0] * _w), int(x_y[1] * _w)), color=(122, 122, 122),
  72. radius=1, thickness=1)
  73. # cv2.putText(img_show, 'left_eye:open', (xmax, ymin),
  74. # cv2.FONT_HERSHEY_SIMPLEX, 1,
  75. # (255, 0, 255), 2)
  76. cv2.namedWindow('img', 0)
  77. cv2.imshow('img', example_image)
  78. cv2.waitKey(0)
  79. fetch_duration = time.time() - start_time
  80. feed_dict = {}
  81. for n in range(cfg.TRAIN.num_gpu):
  82. feed_dict[self.inputs[0][n]] = example_images[n * cfg.TRAIN.batch_size:(n + 1) * cfg.TRAIN.batch_size,
  83. :, :, :]
  84. feed_dict[self.inputs[1][n]] = example_labels[n * cfg.TRAIN.batch_size:(n + 1) * cfg.TRAIN.batch_size,
  85. :]
  86. feed_dict[self.inputs[2]] = True
  87. _, total_loss_value, loss_value, leye_loss_value, reye_loss_value, mouth_loss_value, \
  88. leye_cla_accuracy_value, reye_cla_accuracy_value, mouth_cla_accuracy_value, l2_loss_value, learn_rate, = \
  89. self._sess.run([*self.outputs],
  90. feed_dict=feed_dict)
  91. duration = time.time() - start_time
  92. run_duration = duration - fetch_duration
  93. if self.ite_num % cfg.TRAIN.log_interval == 0:
  94. num_examples_per_step = cfg.TRAIN.batch_size * cfg.TRAIN.num_gpu
  95. examples_per_sec = num_examples_per_step / duration
  96. sec_per_batch = duration / cfg.TRAIN.num_gpu
  97. format_str = ('epoch %d: iter %d, '
  98. 'total_loss=%.6f '
  99. 'loss=%.6f '
  100. 'leye_loss=%.6f '
  101. 'reye_loss=%.6f '
  102. 'mouth_loss=%.6f '
  103. 'leye_acc=%.6f '
  104. 'reye_acc=%.6f '
  105. 'mouth_acc=%.6f '
  106. 'l2_loss=%.6f '
  107. 'learn_rate =%e '
  108. '(%.1f examples/sec; %.3f sec/batch) '
  109. 'fetch data time = %.6f'
  110. 'run time = %.6f')
  111. logger.info(format_str % (_epoch,
  112. self.ite_num,
  113. total_loss_value,
  114. loss_value,
  115. leye_loss_value,
  116. reye_loss_value,
  117. mouth_loss_value,
  118. leye_cla_accuracy_value,
  119. reye_cla_accuracy_value,
  120. mouth_cla_accuracy_value,
  121. l2_loss_value,
  122. learn_rate,
  123. examples_per_sec,
  124. sec_per_batch,
  125. fetch_duration,
  126. run_duration))
  127. if self.ite_num % 100 == 0:
  128. summary_str = self._sess.run(self.summary_op, feed_dict=feed_dict)
  129. self.summary_writer.add_summary(summary_str, self.ite_num)
  130. def _val(self, _epoch, sess):
  131. all_total_loss = 0
  132. for step in range(cfg.TRAIN.val_iter):
  133. # example_images, example_labels = next(self.val_ds) # 在会话中取出image和label
  134. example_images, example_labels = sess.run([self.val_image_data, self.val_label_data])
  135. feed_dict = {}
  136. for n in range(cfg.TRAIN.num_gpu):
  137. feed_dict[self.inputs[0][n]] = example_images[n * cfg.TRAIN.batch_size:(n + 1) * cfg.TRAIN.batch_size,
  138. :, :, :]
  139. feed_dict[self.inputs[1][n]] = example_labels[n * cfg.TRAIN.batch_size:(n + 1) * cfg.TRAIN.batch_size,
  140. :]
  141. feed_dict[self.inputs[2]] = False
  142. total_loss_value, loss_value, leye_loss_value, reye_loss_value, mouth_loss_value, \
  143. leye_cla_accuracy_value, reye_cla_accuracy_value, mouth_cla_accuracy_value, l2_loss_value, learn_rate = \
  144. self._sess.run([*self.val_outputs],
  145. feed_dict=feed_dict)
  146. all_total_loss += total_loss_value - l2_loss_value
  147. return all_total_loss / cfg.TRAIN.val_iter

                      4.  重新开始训练,速度绝对飞起,gpu利用率达到 95%, 10个迭代大概在 5s 左右。

                      5.  我训练最终 loss 大概在 5.5左右 ,作者大概在 3 左右,可能训练参数还有待优化。然后将生成的模型转换成pb文件,找几张图片或者视频,验证下识别效果。   

** 关于解决 tfrecord 文件太大问题

当我们数据集增强后,有可能导致tfrecord文件太大,我试过大约写入100万多条的数据,tfrecord 文件大约接近 270G。

解决:

    1. 压缩数据

  1. # 压缩数据
  2. writer_options = tf.python_io.TFRecordOptions(
  3. tf.python_io.TFRecordCompressionType.ZLIB)
  4. writer = tf.python_io.TFRecordWriter(record_path, options=writer_options)
  5. # 解压缩数据
  6. tfrecord_options = tf.python_io.TFRecordOptions(tf.python_io.TFRecordCompressionType.ZLIB)

    2. 将数据分成多个record文件保存,读取时,只需要将多个record文件的路径列表交给“tf.train.string_input_producer”

        参考: TensorFlow高效读取数据的方法——TFRecord的学习 - c# - 皮皮看书

  1. with open(self.tfrecord_path, "r") as f:
  2. lines = f.readlines()
  3. files_list = []
  4. for line in lines:
  5. files_list.append(line.rstrip())
  6. filename_queue = tf.train.string_input_producer(files_list, shuffle=False)

       *** 压缩后,10000条数据,大约 700多M


三、疲劳检测

通过识别的特征点,计算眼睛的最小的距离,来判断是否属于闭眼状态,然后定义单位时间内 (一般取1 分钟或者 30 秒) 眼睛闭合一定比例 (70%或80%) 所占的时间,来判断是否发生了瞌睡,即PERCLOS值。


四、源码地址

 改编后的训练源码(tf1) : face_landmark-tf1.rar-互联网文档类资源-CSDN下载

   *** 如果 tfrecord 文件太大 ,可压缩、拆分数据集等办法解决。

 识别 (参考作者源码): https://github.com/610265158/Peppa_Pig_Face_Engine

 (dlib)标注工具 : FeatureTool.rar-互联网文档类资源-CSDN下载

   *** 增加撤销 ,删除当前图片等功能,方便标注。


五、最后

本人属于小白一枚,很多地方懂的也不是很多,平时喜欢瞎搞搞,所以希望大家有什么好的建议或想法什么的,欢迎在下面留言,不对的地方,大家多多指正。

附几张检测效果图 :

                             

                                           

                                 

   

       ** 在闭眼检测方面,表现十分优秀。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/237862
推荐阅读
相关标签
  

闽ICP备14008679号