【项目016】基于目标检测的摔倒检测 返回首页

作者:欧新宇(Xinyu OU)
当前版本:Release v1.1
开发平台:Paddle 2.3.2
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。

最后更新:2022年2月4日


一、项目简介

当前摔倒检测是智能家居的重要研究方向,它可以有效地降低家庭成员因独自摔倒后因救治不及时而导致的死亡率。本项目所用数据集为网上收集而得,包含7782张图片,举例如下:

该数据集的部分图片和标签存在问题,无法正确读取,需要经过数据清洗和标签数据重写后才能使用。其中不可用图像包含9个,可用图像数据为7773个,按照7:1:2的比例进行划分, 其中训练验证集样本6217(80%),训练集样本5439个(70%), 验证集样本778个(10%), 测试集样本1556个(20%)。数据集提供了voc格式的标签,数据类型为 *.xml,可用类别为:person和Down,但xml标签文件中还存在两个无用的类别“10+”和“dog”,在训练时需要进行合适的处理。

二、PaddleDetection环境的安装与调试

PaddleDetection环境的安装只需要执行一次,确认服务器或本地环境已经具备相关依赖文件后,可跳过该步。

2.1 PaddleDetection的安装

解压 PaddleDetection-release-2.3 工具包到项目文件夹,实现方法有两种:

  1. 使用Windows文件管理方法进行解压,并将文件夹名改为 DetectionRoadSign(便于项目管理)或直接使用带版本的命名 PaddleDetection-release-2.3(便于版本管理)
  2. 使用下列Python代码实现解压
  3. PaddleDetection的具体安装配置方法,请参考 【项目01】Python机器学习环境的安装和配置。
import os
import zipfile

# 设置工作路径
project_name = 'DetectionRoadSign'
workspace = 'D:\\Workspace\\MyProjects'
if not os.path.exists(workspace):
    os.makedirs(workspace)
os.chdir(workspace)

# 将PaddleDetection解压到工作路径的更目录
f = zipfile.ZipFile('D:\Workspace\安装包\Paddle\PaddleDetection-release-2.3.zip', 'r')
for file in f.namelist():
    f.extract(file, './')
f.close()

# 修改文件名为PaddleDetection(如果需要)
# os.rename('PaddleDetection-release-2.3', project_name)

2.2 PaddleDetection的测试

>> python ppdet/modeling/tests/test_architectures.py

验证通过将得到如下提示:

Ran 7 tests in 12.816s
OK

三、数据准备

3.1 数据集下载

本项目数据集已上传至百度AIStudio,下载地址为: https://aistudio.baidu.com/aistudio/datasetdetail/127208

3.2 数据清洗

初次训练时,系统会提示部分图片/标签无法读取,从而因报错而导致训练停止,经观察发现收集到的数据集有如下问题:

  1. 部分图像数据损坏,无法读取;
  2. 部分图像色彩通道不一致,同时存在彩色图像及灰度图像;
  3. 部分图像的标签存在问题。
<annotation>
  <folder>高雅</folder>
  <filename>people(10).jpg</filename>
  <path>C:\Users\ehualu\Desktop\高雅\people(10).jpg</path>
  <source>
    <database>Unknown</database>
  </source>
  <size>
    <width>0</width>
    <height>0</height>
    <depth>3</depth>
  </size>
  <segmented>0</segmented>
  <object>
    <name>down</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>371</xmin>
      <ymin>312</ymin>
      <xmax>578</xmax>
      <ymax>380</ymax>
    </bndbox>
  </object>
</annotation>

对于以上三个问题,我们给出了如下的数据清洗方案(注意每个数据集存在的问题不尽相同,因此数据清洗工程性很强,需要针对性设置)。

##################################################################################
# 数据清洗
# 作者: Xinyu Ou (http://ouxinyu.cn)
# 数据集名称:摔倒检测(FallDown)
# 本程序功能:
# 1. 对图像坏样本,进行检测,并保存到bad.txt中
# 2. 对xml标签文件中缺失(width, height)及错误信息(folder,filename,path)进行重新写入
###################################################################################

import os
import cv2
import codecs
from xml.dom.minidom import parse
import xml.dom.minidom

# 1. 定义数据集路径和数据统计信息
dataset_name = 'FallDown'
dataset_path = 'D:\\Workspace\\ExpDatasets\\'
dataset_root_path = os.path.join(dataset_path, dataset_name)
excluded = ['.DS_Store']    # 被排除的文件
num_bad = 0
num_good = 0

# 检测坏数据列表是否存在,如果存在则先删除。
bad_list = os.path.join(dataset_root_path, 'badfile_list.txt')
if os.path.exists(bad_list):
    os.remove(bad_list)

# 执行数据清洗
count = 0
with codecs.open(bad_list, 'a', 'utf-8') as f_bad:
    images = os.listdir(os.path.join(dataset_root_path, 'JPEGImages'))                # 获取图像路径
    for image in images:                                                              # 循环遍历图像文件夹中的所有图像
        file_name, ext_name = os.path.splitext(image)
        img_path = os.path.join(dataset_root_path, 'JPEGImages', image)
        xml_path = os.path.join(dataset_root_path, 'Annotations', file_name+'.xml')
        try:
            img = cv2.imread(img_path, 1)
            img_shape = img.shape                                                     # 获取图像的形态[WHC]
            DOMTree = xml.dom.minidom.parse(xml_path)                                 # 使用Dom解析xml文件
            collection = DOMTree.documentElement                                      # 将解析后的信息保存到变量collection中
            width = collection.getElementsByTagName('width')                          # 获取xml文件中width标签的值
            height = collection.getElementsByTagName('height')                        # 获取xml文件中height标签的值
            folder = collection.getElementsByTagName('folder')                        
            filename = collection.getElementsByTagName('filename')
            path = collection.getElementsByTagName('path')
            width[0].firstChild.data = img_shape[1]                                   # 将图像的宽的值赋值给xml文件的width标签
            height[0].firstChild.data = img_shape[0]                                  # 将图像的宽的值赋值给xml文件的height标签
            folder[0].firstChild.data = 'JPEGImages'                                  # 将xml文件的folder标签赋值为: JPEGImages
            filename[0].firstChild.data = image
            path[0].firstChild.data = image
            with open(xml_path, 'w') as f_xml:                                        # 保存修改后的xml信息到xml标签文件中
                DOMTree.writexml(f_xml)
            # cv2.imwrite(img_path, img)
            num_good += 1
            pass
        except:            
            f_bad.write('{}\n'.format(image))
            num_bad += 1
        if count % 10 == 0:
            print('\r 当前清洗进度:{}/{},移除损坏文件{}个。'.format(count, len(images), num_bad), end='')     # 输出清洗过程信息
        count += 1
 
print('数据集清洗完成, 损坏文件{}个, 正常文件{}.'.format(num_bad, num_good))                                    # 输出统计信息

当前清洗进度:7780/7782,移除损坏文件9个。数据集清洗完成, 损坏文件9个, 正常文件7773.

四、模型训练

4.1 准备配置文件

本项目以 faster_rcnn_r50_fpn_1x_voc 模型为例,该模型以ResNet50为主干模型构建fasterRCNN,使用了特征金字塔FPN结构实现特征提取。该模型主要涉及6个配置文件,相互关系如下图所示:

本项目在配置模型超参数时,需要先需要复制/创建如下文件:

  1. ./configs 目录下创建 FallDown 文件夹,并从 configs/faster_rcnn 文件夹中拷贝 faster_rcnn_r50_fpn_1x_voc 文件至 configs/FallDown 文件夹中。
  2. ./configs/FallDown 下创建 _base_ 文件夹,并从 configs/faster_rcnn/_base_ 文件夹中拷贝如下文件至 configs/FallDown/_base_ 文件夹中,包括: optimizer_12e.yml, faster_fpn_reader.yml, faster_rcnn_r50_fpn.yml,。
  3. ./configs/datasets 文件夹中复制 voc.ymlFallDown_voc.yml

4.2 模型关键参数配置 (*.yml)

4.2.1 目标项目的配置文件 (faster_rcnn_r50_fpn_1x_coco.yml)

_BASE_: [                                  # 需要继承的相关配置文件
  '../datasets/FallDown_voc.yml',          # 路标数据集配置文件
  '../runtime.yml',                        # 全局配置文件
  '_base_/optimizer_12e.yml',              # 优化器配置文件
  '_base_/faster_rcnn_r50_fpn.yml',        # 主干网络配置文件
  '_base_/faster_fpn_reader.yml',          # 数据读取器配置文件
]

weights: output/FallDown/faster_rcnn_r50_fpn_1x_voc/best_model   # 最终模型保存的位置
save_dir: output/FallDown                                        # 模型的输出文件夹

4.2.2 全局配置文件 (configs/runtime.yml)

use_gpu: true         # 根据硬件选择是否使用GPU
log_iter: 10          # 设置多少次迭代显示一次训练日志
save_dir: output      # 模型保存的文件夹
snapshot_epoch: 1     # 每隔多少个周期保存一次模型
print_flops: true    # 是否显示计算吞吐量,注意启用该选项需要安装paddleslim

4.2.3 数据集配置文件 (configs/datasets/roadsign_coco.yml)

metric: VOC              # 数据集的结构形态:MSCOCO
map_type: integral       # [11points|integral],前者为VOC2010以前的版本,计算[0,0.1,..1] 11个点的mAP,integral为VOC2010后的版本,计算每个Recall的mAP
num_classes: 3           # 类别数量

TrainDataset:        # 训练集
  !VOCDataSet
    dataset_dir: D:/Workspace/ExpDatasets/FallDown               # 图像的根目录
    anno_path: trainval.txt                                        # 图像的标注文件列表,格式类似分类任务,如:img1.png img1.xml
    label_list: label_list.txt                                     # label的列表按照自然数进行排序,每个类别名称一行,例如:car, person, table,其中预测索引0表示car,索引1表示person,索引2表示table
    data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']     # xml文件的关键数据域,包括[图像,groundTurth边界框坐标,groundTurth类别名称]

EvalDataset:         # 验证集
  !VOCDataSet
    dataset_dir: D:/WorkSpace/ExpDatasets/FallDown
    anno_path: test.txt
    label_list: label_list.txt
    data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']

TestDataset:         # 测试集
  !ImageFolder
    dataset_dir: D:/WorkSpace/ExpDatasets/FallDown
    anno_path: label_list.txt

4.2.4 主干网络配置文件 (_base_/faster_rcnn_r50_fpn.yml)

architecture: FasterRCNN                     # 模型架构 FasterRCNN
pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/ResNet50_cos_pretrained.pdparams

FasterRCNN:                                  # 定义FasterRCNN各模块名称
  backbone: ResNet                           # 主干网络:ResNet
  neck: FPN                                  # neck模块(特征提取):FPN特征金字塔
  rpn_head: RPNHead                          # RPN头:区域建议网络
  bbox_head: BBoxHead                        # BBox头:BoundingBox 区域回归模块
  # post process                             
  bbox_post_process: BBoxPostProcess         # 后处理模块


ResNet:
  # index 0 stands for res2
  depth: 50                    # 模型深度,此处标识为ResNet50
  norm_type: bn                # 设置是否启用batch-norm, bn正则化
  freeze_at: 0                 # 微调训练时冻结模块的起点,index=0表示的是res2,因此,第一组卷积在微调时将永久冻结 (fastRCNN)
  return_idx: [0,1,2,3]        # 实际参与微调的层为[2,3,4,5]
  num_stages: 4

FPN:
  out_channel: 256             # FPN输出的通道数为 256

RPNHead:
  anchor_generator:            ## Anchor采样的基本原则
    aspect_ratios: [0.5, 1.0, 2.0]                     # 采样长宽比例  
    anchor_sizes: [[32], [64], [128], [256], [512]]    # FPN采样基准尺度
    strides: [4, 8, 16, 32, 64]                        # 采样步长
  rpn_target_assign:           ## RPN采样规则
    batch_size_per_im: 256     # 共采样256个Anchor
    fg_fraction: 0.5           # 正样本比例为 0.5,即 512*0.5=256个
    negative_overlap: 0.3      # 与所有真实框的IoU<0.3的Anchor为负样本
    positive_overlap: 0.7      # 与任意真实框的IoU>0.7的Anchor为正样本
    use_random: True           # 使用随机方式进行采样
  train_proposal:
    min_size: 0.0
    nms_thresh: 0.7            # nms阈值 0.7
    pre_nms_top_n: 2000        # 使用RPN采样2000个RoIs用于后续的处理
    post_nms_top_n: 1000       # 经过对象性排序后保留1000个样本
    topk_after_collect: True
  test_proposal:
    min_size: 0.0
    nms_thresh: 0.7            # nms阈值 0.7
    pre_nms_top_n: 1000        # 使用RPN采样1000个RoIs用于后续的处理
    post_nms_top_n: 1000       # 经过对象性排序后保留1000个样本,原作为300个


BBoxHead:                     ## BBox头的超参数
  head: TwoFCHead             # 输出头为TwoFCHead,通道数为 1024
  roi_extractor:
    resolution: 7             # RoI的输出特征为 7×7,也即输入BBox的特征为7×7
    sampling_ratio: 0
    aligned: True             # 启用RoI Align模式
  bbox_assigner: BBoxAssigner

BBoxAssigner:                 ## BBox回归的超参数
  batch_size_per_im: 512      # 采样总数为 512
  bg_thresh: 0.5              # IoU<0.5为负样本
  fg_thresh: 0.5              # IoU>0.5为正样本
  fg_fraction: 0.25           # 正样本比率为0.25,即 512*0.25=128个
  use_random: True            # 使用随机方式进行采样

TwoFCHead:
  out_channel: 1024           # 输入BBoxHead的特征通道数


BBoxPostProcess:              ## 后处理方法为:多类非极大抑制NMS
  decode: RCNNBox
  nms:
    name: MultiClassNMS
    keep_top_k: 100          # 保留评分最高的100个patch(Anchor)
    score_threshold: 0.05    # 评分最低阈值
    nms_threshold: 0.5       # nms阈值

4.2.5 数据读取器配置文件 (_base_/faster_fpn_reader.yml)

worker_num: 2
TrainReader:                         # 训练阶段的数据读取器
  sample_transforms:
  - Decode: {}
  # 随机裁剪,训练时采用多尺度进行训练,验证和测试时通常使用固定尺寸/比例进行测试
  - RandomResize: {target_size: [[640, 1333], [672, 1333], [704, 1333], [736, 1333], [768, 1333], [800, 1333]], interp: 2, keep_ratio: True}
  - RandomFlip: {prob: 0.5}         # 按照0.5的概率随机水平翻转
  - NormalizeImage: {is_scale: true, mean: [0.485,0.456,0.406], std: [0.229, 0.224,0.225]}     # 数据标准化:减均值、去方差
  - Permute: {}
  batch_transforms:
  - PadBatch: {pad_to_stride: 32}     
  batch_size: 1                     # 每批次训练样本的数量
  shuffle: true                     # 是否随机打乱数据,通常在训练过程中需要随机打乱
  drop_last: true                   # epoch/batch_size有余数时,是否将末尾的数据进行丢弃,训练中通常进行丢弃
  collate_batch: false


EvalReader:                          # 验证阶段的数据读取器
  sample_transforms:
  - Decode: {}
  - Resize: {interp: 2, target_size: [800, 1333], keep_ratio: True}
  - NormalizeImage: {is_scale: true, mean: [0.485,0.456,0.406], std: [0.229, 0.224,0.225]}
  - Permute: {}
  batch_transforms:
  - PadBatch: {pad_to_stride: 32}
  batch_size: 1
  shuffle: false
  drop_last: false


TestReader:                          # 测试阶段的数据读取器
  sample_transforms:
  - Decode: {}
  - Resize: {interp: 2, target_size: [800, 1333], keep_ratio: True}
  - NormalizeImage: {is_scale: true, mean: [0.485,0.456,0.406], std: [0.229, 0.224,0.225]}
  - Permute: {}
  batch_transforms:
  - PadBatch: {pad_to_stride: 32}
  batch_size: 1
  shuffle: false
  drop_last: false

4.2.6 优化器配置文件 (_base_/optimizer_1x.yml)

epoch: 12                        # 迭代训练的周期数,默认为12个周期

LearningRate:                    # 学习率超参数
  base_lr: 0.005                 # 初始学习率
  schedulers:               
  - !PiecewiseDecay              # 学习率策略:分段学习率
    gamma: 0.1                   # 学习率衰减率超参数:0.1
    milestones: [8, 11]          # 学习率衰减的时刻点,第8个epoch和第11个epoch
  - !LinearWarmup                # 线性热身(学习率慢启动),避免较大的初始学习率引起损失过分振荡
    start_factor: 0.1            # 衰减率为 0.1
    steps: 2000                  # 热身周期为 1000次迭代

OptimizerBuilder:                # 优化方法设置
  optimizer:
    momentum: 0.9                # 动量值 0.9
    type: Momentum               # 优化方法:基于动量的随机梯度下降
  regularizer:
    factor: 0.0001               # 正则化因子  0.0005
    type: L2                     # 正则化方法 L2正则化

4.3 启动训练

下面给出训练模型的基本命令,值得注意的是,在Notebook中运行训练进程,无法实时打印出日志文件(会在运行完一次性打印出所有日志)。鉴于训练时间通常比较长,为了便于观察训练过程,建议使用Anaconda终端的命令行进行执行。PaddleDetection训练过程中若开启了--eval,会将所有checkpoint中评估结果最好的checkpoint保存为best_model

##################### JupyterLab ######################################
import os
workspace = 'D:\\Workspace\\MyProjects\\PaddleDetection-release-2.3'
os.chdir(workspace)

model_template = 'faster_rcnn_r50_fpn_1x_voc'

!python -u tools/train.py -c configs/FallDown/{model_template}.yml --eval

##################### 命令行 ######################################
# 命令行执行路径为项目文件夹:D:\\Workspace\\MyProjects\\PaddleDetection-release-2.3

# python -u tools/train.py -c configs/FallDown/faster_rcnn_r50_fpn_1x_voc.yml --eval

四、评估和预测

PaddleDetection提供了tools/eval.py脚本用于评估模型,该命令可以使用模型的默认参数 weight 所指定的模型进行评估(建议),若使用预先下载的模型也可以通过 -o weights= 命令进行指定。参数 --classwise 可以打印出每个类别的精确度。

4.1 模型评估

##################### JupyterLab ######################################
import os
workspace = 'D:\\Workspace\\MyProjects\\PaddleDetection-release-2.3'
os.chdir(workspace)

model_template = 'faster_rcnn_r50_fpn_1x_voc'

!python -u tools/eval.py -c configs/FallDown/{model_template}.yml --classwise

##################### 命令行 ######################################
# 命令行执行路径为项目文件夹:D:\\Workspace\\MyProjects\\PaddleDetection-release-2.3
# python -u tools/eval.py -c configs/FallDown/faster_rcnn_r50_fpn_1x_voc.yml --classwise
    [02/04 23:50:06] ppdet.utils.checkpoint INFO: Finish loading model weights: output/FallDown/faster_rcnn_r50_fpn_1x_voc/best_model.pdparams
    [02/04 23:50:07] ppdet.engine INFO:  Model FLOPs : 163.633649G. (image shape is [1, 3, 800, 1088])
    [02/04 23:50:08] ppdet.engine INFO: Eval iter: 0
    [02/04 23:50:17] ppdet.engine INFO: Eval iter: 100
    [02/04 23:50:25] ppdet.engine INFO: Eval iter: 200
    [02/04 23:50:34] ppdet.engine INFO: Eval iter: 300
    [02/04 23:50:42] ppdet.engine INFO: Eval iter: 400
    [02/04 23:50:51] ppdet.engine INFO: Eval iter: 500
    [02/04 23:51:00] ppdet.engine INFO: Eval iter: 600
    [02/04 23:51:08] ppdet.engine INFO: Eval iter: 700
    [02/04 23:51:17] ppdet.engine INFO: Eval iter: 800
    [02/04 23:51:27] ppdet.engine INFO: Eval iter: 900
    [02/04 23:51:38] ppdet.engine INFO: Eval iter: 1000
    [02/04 23:51:47] ppdet.engine INFO: Eval iter: 1100
    [02/04 23:51:56] ppdet.engine INFO: Eval iter: 1200
    [02/04 23:52:04] ppdet.engine INFO: Eval iter: 1300
    [02/04 23:52:13] ppdet.engine INFO: Eval iter: 1400
    [02/04 23:52:21] ppdet.engine INFO: Eval iter: 1500
    [02/04 23:52:26] ppdet.metrics.metrics INFO: Accumulating evaluatation results...
    [02/04 23:52:27] ppdet.metrics.map_utils INFO: Per-category of VOC AP: 
    +----------+-------+----------+-------+----------+-------+
    | category | AP    | category | AP    | category | AP    |
    +----------+-------+----------+-------+----------+-------+
    | person   | 0.558 | down     | 0.752 | 10+      | 0.710 |
    +----------+-------+----------+-------+----------+-------+
    [02/04 23:52:27] ppdet.metrics.map_utils INFO: per-category PR curve has output to voc_pr_curve folder.
    [02/04 23:52:27] ppdet.metrics.metrics INFO: mAP(0.50, integral) = 67.31%
    [02/04 23:52:27] ppdet.engine INFO: Total sample number: 1556, averge FPS: 11.08072138936038
    

    W0204 23:50:00.861847  1420 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 6.1, Driver API Version: 11.1, Runtime API Version: 10.2
    W0204 23:50:00.951114  1420 device_context.cc:465] device: 0, cuDNN Version: 7.6.
    libpng warning: iCCP: known incorrect sRGB profile

4.2 模型预测

PaddleDetection提供了tools/infer.py预测工具,可以使用训练好的模型预测图像并可视化,也可以通过-o weights=指定加载训练过程中保存的权重;参数 --infer_img={img_path} 用于指定待预测图片。

####### 预测
import os
import matplotlib.pyplot as plt 
import cv2

# 0. 设置工作路径
workspace = 'D:\\Workspace\\MyProjects\\PaddleDetection-release-2.3'
os.chdir(workspace)

####### 1. 执行预测Inference
# 1.1 设置图片路径
img_root = r'../../Data/Projects/Project016FallDownDetection/people(62).jpg'
img_file = 'people(62).jpg'  
img_path = os.path.join(img_root, img_file)

# 1.2 设置模型路径
model_template = 'faster_rcnn_r50_fpn_1x_voc'

# 1.3 设置Inference图像的输出和读取路径
infer_path = os.path.join(workspace, 'output', 'FallDown')

# 1.4 执行预测脚本
!python tools/infer.py -c configs/FallDown/{model_template}.yml -o --infer_img={img_path} --output_dir={infer_path}

####### 2. 可视化Inference结果
plt.figure(figsize=(10, 6))
infer_img_path = os.path.join(infer_path, img_file)
infer_img = cv2.imread(infer_img_path, 1)
plt.imshow(cv2.cvtColor(infer_img, cv2.COLOR_BGR2RGB))
plt.show()
    [02/05 00:37:49] ppdet.utils.checkpoint INFO: Finish loading model weights: output/FallDown/faster_rcnn_r50_fpn_1x_voc/best_model.pdparams
    [02/05 00:37:51] ppdet.engine INFO:  Model FLOPs : 182.884425G. (image shape is [1, 3, 800, 1216])
    [02/05 00:37:51] ppdet.engine INFO: Detection bbox results save in D:\Workspace\MyProjects\PaddleDetection-release-2.3\output\FallDown\people(62).jpg
    

    W0205 00:37:48.273064  3100 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 6.1, Driver API Version: 11.1, Runtime API Version: 10.2
    W0205 00:37:48.288024  3100 device_context.cc:465] device: 0, cuDNN Version: 7.6.

output_23_2