实验目的
使用 TensorFlow Object Detection API 进行实时目标检测(基于 SSD 模型)
任务列表:
- 行人识别
- 人脸识别
- 交通灯识别
- 实时检测(平均 FPS>15)
- 使用 tflite 将模型移植到嵌入式设备
实验环境安装
下载 Tensorflow 对象检测 API
Tensorflow/models 的 GitHub 地址:https://github.com/tensorflow/models
对象检测 API 的目录:tensorflow/models/research/object_detection
通过以下命令将其克隆到本地工作目录
git clone https://github.com/tensorflow/models.git
安装依赖库
本次实验使用的是 TensorFlow Object Detection API,根据官方文档介绍,需要安装以下库:
- Protobuf 3.0.0
- Python-tk
- Pillow 1.0
- lxml
- tf Slim (which is included in the “tensorflow/models/research/“ checkout)
- Jupyter notebook
- Matplotlib
- Tensorflow (>=1.9.0)
- Cython
- contextlib2
- cocoapi
其中,关于 Tensorflow 的详细安装步骤,可以按照 Tensorflow 官方安装说明进行操作。一般安装好 python 之后可以使用以下命令之一安装 Tensorflow:
# CPU版本
pip install tensorflow
# GPU版本
pip install tensorflow-gpu
安装 GPU 版本的 tensorflow 可以参考这篇文章
其余的库可以通过 apt-get 安装在 Ubuntu 16.04 上:
sudo apt-get install protobuf-compiler python-pil python-lxml python-tk
pip install --user Cython
pip install --user contextlib2
pip install --user jupyter
pip install --user matplotlib
pip 默认将 Python 包安装到系统目录(例如 /usr/local/lib/python3.6),这需要 root 访问权限。
--user 选项的意思是:在你的主目录中创建 pip 安装包,而不需要任何特殊权限。
或者,你也可以使用 pip 安装依赖项:
pip install --user Cython
pip install --user contextlib2
pip install --user pillow
pip install --user lxml
pip install --user jupyter
pip install --user matplotlib
Protobuf 编译
Tensorflow 对象检测 API 使用 Protobufs 配置模型和训练参数。在使用框架之前,必须编译 Protobuf 库,可以通过在目录 tensorflow/models/research/ 运行以下命令来完成:
# From tensorflow/models/research/
protoc object_detection/protos/*.proto --python_out=.
如果在编译时遇到错误,则可能使用的是不兼容的 Protobuf 编译器。如果是这种情况,可以使用手动安装来解决。
手动 Protobuf 编译器安装和使用
如果你使用的是 Linux:
下载并安装 protoc 的 3.0 版本,然后解压缩该文件。在目录 tensorflow/models/research/ 运行以下命令:
# From tensorflow/models/research/
wget -O protobuf.zip https://github.com/google/protobuf/releases/download/v3.0.0/protoc-3.0.0-linux-x86_64.zip
unzip protobuf.zip
再次运行编译过程,但使用刚才我们自己下载的 protoc 版本,在目录 tensorflow/models/research/ 运行以下命令:
# From tensorflow/models/research/
./bin/protoc object_detection/protos/*.proto --python_out=.
MacOS 可以参考这个链接
将库添加到 PYTHONPATH
在本地运行时,tensorflow/models/research/ 和 tensorflow/models/research/slim 目录需要添加到 PYTHONPATH,可以通过在目录 tensorflow/models/research/ 运行以下命令来完成:
# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
每次运行新终端时,都要重新输入此命令,如果希望避免繁琐的操作,可以将其作为新行追加到 ~/ .bashrc 文件的末尾,将“pwd”替换此项目目录的绝对路径。
测试安装
到现在所有的环境配置就完成了,可以通过运行以下命令来测试是否已正确安装 Tensorflow Object Detection API:
python object_detection/builders/model_builder_test.py
如果出现下面的结果就表示安装成功:
建立自己的工作空间
因为 tensorflow/models/* 里面有太多其他的模块了,我们这次实验只需要用到其中的 tensorflow/models/research/object_detection,所以在环境搭建好之后我们建立一个自己的项目文件夹,让整个工程更简洁。
首先新建文件夹 Object-Detection-USTC,然后将刚才目录 tensorflow/models/research/ 中的 object_detection 文件夹和 slim 文件夹分别复制一份到 Object-Detection-USTC 文件夹中。
此时的工程目录结如下:
Object-Detection-USTC
├── object_detection
└── slim
数据集及数据预处理方案
数据集
- 数据集 1:Penn-Fudan Database来源:https://www.cis.upenn.edu/~jshi/ped_html/**内容:**这是一个图像数据集,其中包含可以用于行人检测的图片。图片来自校园和城市街道周围的场景,我们对这些图片中感兴趣的对象是行人,每张图片中至少有一个行人。此数据集共有 170 张图片,345 个行人标注,其中 96 张图像来自宾夕法尼亚大学,其余 74 张图片来自复旦大学。目录结构:
PennFudanPed
├── added-object-list.txt
├── Annotation # 标注文件
│ ├── FudanPed00001.txt
│ ├── FudanPed00002.txt
│ └── ...
├── PedMasks # 遮罩文件,用于做像素级别的图像分割,本次实验不涉及
│ ├── FudanPed00001_mask.png
│ ├── FudanPed00002_mask.png
│ └── ...
├── PNGImages # 图片
│ ├── FudanPed00001.png
│ ├── FudanPed00002.png
│ └── ...
└── readme.txt
**缺点:**不含红绿灯、不含人脸标注
- 数据集 2**来源:**监控视频 2.mp4**内容:**此数据集为我们将视频中的帧截取保存(每 3 帧截取一张)后,使用标注精灵工具人工标注而成,一共有大约 600 张标注有行人、人脸、红灯、绿灯的图片,最后标注的保存格式为 PascalVoc,方便后面将其转换成 tfrecord 格式。
转换数据集格式
根据官方文档,要在 Tensorflow Object Detection API 中使用我们自己的数据集,必须将其转换为 TFRecord 文件格式。
- 数据集 1
- 建立标签地图(Label Maps)每个数据集都需要具有与之关联的标签映射,此标签映射定义了从字符串类名(如:‘person’)到整数类 Ids(如:1)的映射,所以我们首先建立数据集的标签地图(Label Maps)。
注意:标签地图的 id 号应该从 1 开始,不能从 0 开始。
- 在目录 Object-Detection-USTC/object_detection/data/ 下新建文件 PennFudanPed_label_map.pbtxt,并写入以下内容:
item {
id: 1
name: 'person'
}
因为数据集 1 中的标注只有行人,所以只用在 Label Maps 中写一个 item。
- 编写转换脚本将数据集 1(Penn-Fudan Database)下载解压后放入目录 Object-Detection-USTC/object_detection/data/。因为此数据集的标注文档(如:PennFudanPed/Annotation/FudanPed00001.txt)不规则,所以我们需要自己写一个脚本来将这个数据集转换成 tfrecord 格式。
- 数据集 2
- 建立标签地图(Label Maps)将数据集 2(文件夹名字为:VOC2007)格式组织好后放入目录 Object-Detection-USTC/object_detection/data/。接下来建立数据集 2 的标签地图(Label Maps),在目录 Object-Detection-USTC/object_detection/data/ 下新建文件 pascal_label_map.pbtxt,并写入以下内容:
item {
name: "person"
id: 1
display_name: "person"
}
item {
name: "face"
id: 2
display_name: "face"
}
item {
name: "red_light"
id: 3
display_name: "red_light"
}
item {
name: "green_light"
id: 4
display_name: "green_light"
}
因为数据集 2 中标注了 4 类对象,所以只用在 Label Maps 中有 4 个 item。
- 运行转换脚本此数据集为我们自己人工标注,保存格式为 PascalVoc(与 ImageNet 采用的格式相同),而 tensorflow 提供了将 PascalVoc 格式的数据集转换成 tfrecord 格式的脚本,路径为 Object-Detection-USTC/object_detection/dataset_tools/create_pascal_tf_record.py,所以我们不用自己写了。接下来写一个 shell 脚本来执行上述文件,在 Object-Detection-USTC/ 目录下新建文件 create_pascal_tfrecord.sh 并写入如下命令:
python object_detection/dataset_tools/create_pascal_tf_record.py \
--label_map_path=object_detection/data/pascal_label_map.pbtxt \
--data_dir=object_detection/data --year=VOC2007 --set=train \ # 生成训练集
--output_path=object_detection/data/pascal_train.record # 训练集输出路径
python object_detection/dataset_tools/create_pascal_tf_record.py \
--label_map_path=object_detection/data/pascal_label_map.pbtxt \
--data_dir=object_detection/data --year=VOC2007 --set=val\ # 生成验证集
--output_path=object_detection/data/pascal_val.record # 验证集输出路径
PascalVoc 格式的数据集 2 存放目录结构为:
Object-Detection-USTC
└── object_detection
└── data
└── VOC2007
├── Annotations
├── ImageSets
├── JPEGImages
└── readme.txt
上面的 shell 脚本中,参数意义如下:
- 参数 data_dir 指定数据目录 object_detection/data,目录里面有 VOC2007 文件夹,VOC2007 文件夹里面有存放图片的 JPEGImages,存放标注分类坐标信息 Annotations。
- ImageSets/Main 目录下有四个文件:test.txt 是测试集,train.txt 是训练集,val.txt 是验证集,trainval.txt 是训练和验证集。
- 参数 set 指定要使用的数据集是那个,这里使用了 train 和 val 两个。
然后在 Object-Detection-USTC/ 目录下行运行这个脚本就可以转换成功:
./create_pascal_tfrecord.sh
转换完成后会在目录 Object-Detection-USTC/object_detection/data/ 下生成两个 tfrecord 文件:pascal_train.record 和 pascal_val.record ,它们分别是数据集 2 对应的 tfrecord 格式的训练集和验证集。
此处之所以要写 shell 脚本的原因是:上面的命令太长了,每次执行都要在命令行敲一遍太麻烦,写 shell 脚本的好处是以后每次执行上面的命令的时候只用在命令行输入这个脚本的名字就行了
转换数据集格式到此结束,总结一下,在 Object-Detection-USTC/object_detection/data 目录下有以下文件:
- pascal_label_map.pbtxt # 数据集 2 生成的 tfrecord 格式的训练集
- pascal_train.record # 数据集 2 生成的 tfrecord 格式的训练集
- pascal_val.record # 数据集 2 生成的 tfrecord 格式的验证集
- PennFudanPed # 数据集 1(Penn-Fudan Database)
- PennFudanPed_label_map.pbtxt # 数据集 1 的 Label Maps
- PennFudanPed_train.record # 数据集 1 生成的 tfrecord 格式的训练集
- PennFudanPed_val.record # 数据集 1 生成的 tfrecord 格式的验证集
- VOC2007 # 数据集 2
模型训练
下载预训练模型
为了加快训练速度,我们需要基于谷歌提供的预训练模型来微调参数(即迁移学习),官方提供了不少预训练模型,点击查看。
我们对比尝试了以下训练模型:
模型 | 平均帧率 |
faster_rcnn_inception_v2_coco | 4.92 |
SSD-MobileNet-v1-2017 | 14.57 |
SSD-MobileNet-v1-2018 | 18.88 |
SSD-MobileNet-v2-2018 | 14.33 |
上表的数据是在视频实时检测实现之后直接拿这些预训练模型测试所得
最终选择了 SSD-MobileNet-v2-2018 预训练模型进行迁移学习训练,因为它能相对较好的兼顾实时性和准确性。
在工程目录 Object-Detection-USTC/object_detection/ 下新建文件夹 ssd_mobilenet(此文件夹的用途是存放与模型相关的文件,包括待会儿我们自己训练出来的模型),将下载好的 SSD-MobileNet-v2-2018 压缩包解压后,放入目录 Object-Detection-USTC/object_detection/ssd_mobilenet/,会得到如下目录结构:
Object-Detection-USTC
└── object_detection
└── ssd_mobilenet
└── ssd_mobilenet_v2_coco_2018_03_29
├── checkpoint
├── frozen_inference_graph.pb # 冻结图
├── model.ckpt.data-00000-of-00001 # 检查点
├── model.ckpt.index # 检查点
├── model.ckpt.meta # 检查点
├── pipeline.config # 用于训练和生成模型的管道配置文件
└── saved_model
修改管道配置文件
将目录 Object-Detection-USTC/object_detection/ssd_mobilenet/ssd_mobilenet_v2_coco_2018_03_29 中的管道文件 pipeline.config 复制到上级目录(ssd_mobilenet 目录)下,为了好辨别,将此文件更名为 pipeline_ssd_mobilenet_v2_coco_2018_03_29.config,再修改如下几个位置:
(以下配置以使用数据集 2 训练为例,使用数据集 1 训练同理也可做相应的配置)
- 修改类别数
num_classes:5
- 修改预训练模型位置
fine_tune_checkpoint: "/home/jhchen/Desktop/Object-Detection-USTC/object_detection/ssd_mobilenet/ssd_mobilenet_v2_coco_2018_03_29/model.ckpt"
为避免找不到文件的位置,此处使用绝对路径,下同
- 修改训练次数
num_steps: 1000
- 修改训练数据位置
train_input_reader {
# 标签地图(Label Maps)配置文件路径
label_map_path: "/home/jhchen/Desktop/Object-Detection-USTC/object_detection/data/pascal_label_map.pbtxt"
tf_record_input_reader {
# 训练集径
input_path: "/home/jhchen/Desktop/Object-Detection-USTC/object_detection/data/pascal_train.record"
}
}
- 修改验证评估数据位置(可选)
eval_input_reader {
label_map_path: "/home/jhchen/Desktop/Object-Detection-USTC/object_detection/data/pascal_label_map.pbtxt"
shuffle: false
num_readers: 1
tf_record_input_reader {
input_path: "/home/jhchen/Desktop/Object-Detection-USTC/object_detection/data/pascal_val.record"
}
}
开始训练
Tensorflow 官方提供了训练的脚本,路径为 Object-Detection-USTC/object_detection/legacy/train.py,接下来,我们要调用它来开始我们的迁移学习训练。
在工程目录 Object-Detection-USTC/ 下新建一个训练的脚本文件 train.sh,并写入以下内容:
# 如果不存在这个路径,就递归地创建它
if [ ! -d "object_detection/ssd_mobilenet/train_logs" ]; then
mkdir -p object_detection/ssd_mobilenet/train_logs
fi
# 设置管道配置文件的路径
PIPELINE_CONFIG_PATH=object_detection/ssd_mobilenet/pipeline_ssd_mobilenet_v2_coco_2018_03_29.config
# 设置模型训练过程中产生的记录文件的存放位置
TRAIN_LOGS=object_detection/ssd_mobilenet/train_logs
# 运行train.py文件
python object_detection/legacy/train.py \
--logtostderr \
--train_dir=$TRAIN_LOGS \
--pipeline_config_path=$PIPELINE_CONFIG_PATH
文件夹 Object-Detection-USTC/object_detection/ssd_mobilenet/train_logs 中存放模型训练过程中产生的记录文件,后面需要通过这个文件夹内的文件来导出我们可以使用的模型。
然后在 Object-Detection-USTC/ 目录下行运行这个脚本就可以开始训练:
./train.sh
看到如下的提示信息就表示开始训练了
可视化训练过程
在训练的过程中,可以同时通过 tensorboard 来查看训练的情况。新开一个终端,在 Object-Detection-USTC/ 目录下,运行如下命令:
tensorboard --logdir=object_detection/ssd_mobilenet/train_logs
在浏览器打开 http://localhost:6006,即可看到 Loss 等信息的实时变化情况。
实时检测
实时检测方案
实时检测的意思是:将视频中的每一帧图像送入刚才训练好的模型中进行对象检测,再将模型识别出的对象信息标注(画边界框,置信度等)到这一帧图像上并显示出来,速度非常快时就能达到像播放视频一样的效果。
但是如果我们的程序真的向上述的过程一样,每读取一帧图像,再经过模型检测,再显示出来,这样效率显然很低,实时性极差,我们可以引入线程和队列来提升实时性:
整个实时检测方案的流程如下图所示:
主进程做的事情图中用深绿色标出,线程 1 做的事情图中用黄色标出。
运行过程:
主程序读入视频并不断将这些帧按顺序塞入输入队列,线程 1 读入我们刚才训练好的模型,然后按顺序不断地从输入队列获取图片,将图片进行优化处理(调整对比度和亮度),再送入模型进行检测,如此,我们就能得到模型的检测结果,接下来线程 1 会将模型检测出的对象信息(bbox、置信度等)标注到图片上,然后将此标注好的图片塞入输出队列。主进程除了读入视频并将之入输入队列外,同时还会不断地从输出队列里面取图片并显示。
**注意:**上述过程中,主进程和线程 1 是并行执行的。
总结一下上图的运行过程,其中包含以下几个对象:
- **输入队列:**从主进程接收原视频帧,传给线程 1
- **输出队列:**接收经线程 1 处理后的视频帧,回传给主进程,由主进程来显示输入队列和输出队列的作用是方便主进程和线程 1 之间进行通信
- **主进程:**负责以下任务
- 读入视频帧
- 将视频帧塞入输入队列
- 显示识别后的视频帧
- 检测按键
- 计算系统运行信息(播放速度、已处理帧数、已花时间等信息)
最后两项任务上图中未体现,下文会详细介绍
- **线程 1:**负责以下任务
- 读入训练好的模型
- 从输入队列获取图片
- 图片预处理
- 送入模型检测
- 得到检测结果
- 标注 bbox 等信息
- 将标注后的视频帧塞入输出队列
改进实时检测效果
使用上面的实时检测方案后,能达到较好的实时检测效果,但是我们还可以通过下面的方法来改进我们的检测准确率和实时性。
准确率改进方案
- 改变图片的对比度和亮度(OpenCV 基础知识)观察下面这张图片中的红框处,我们发现交通灯的颜色几乎快要与背景色融为一体了,所以我们可以在把图片送入模型检测之前,对其做适当的预处理,从而使得模型能更容易地检测出这些对象。
本次实验中,我们使用 opencv 中的基础知识改变了图片的对比度和亮度来达到上述效果。
- 使用类似于集成学习的方案:模型 1+ 模型 2 同时检测在测试过程中我们发现,我们自己训练出的模型(模型 1)对人脸(face)和行人(person)的识别效果很好,对交通灯(traffic_light)的识别效果欠佳,而模型 2——预训练模型 SSD-MobileNet-v2-2018(微软 coco 数据集训练过)对交通灯的识别效果很好,所以我们尝试将每帧图片都送入两个模型检测,最终识别结果取模型 1 的人脸、行人和模型 2 的交通灯综合的结果。
模型 1 对交通灯识别效果欠佳的原因是:模型 1 经数据集 2 训练而得,而数据集 2 中交通灯的位置始终不变,相当于整个数据集只有一个交通灯对象,最终导致训练出的模型对交通灯不敏感。
- 使用多线程 + 多队列的系统运行过程如下图所示:
运行过程与之前分析的线程 + 队列方案类似,只是现在有多个线程来同时跑模型处理图像了。
测试结果显示,多线程 + 多队列的方法似乎不能很好地提高实时性了,分析其原因,有以下几点:
- 查了资料发现,python 中的线程似乎有点儿鸡肋,多线程更适合做一些并行 I/O 操作,而并行计算操作似乎效率并不会提升…
- 线程的工作原理是通过操作系统来分时间片轮转调度他们,类似于通信中的“时分复用”,所以实际上这么多线程还是运行在这 1 颗核心上
- 开启多个线程会增加操作系统调度和切换线程时的开销,占用计算机资源
在主进程中,我们添加了通过按键来控制播放速度的功能,其中 speed 变量的作用就是用来存放播放速度的,它表示主程序每次运行到此处要延时 speed 这么长时间,也相当于是多长时间从输出队列读取一次图片显示,通过此方式间接控制了播放速度,speed 的值越小表示播放速度越快,反之越慢:
- w 键:speed 加大,播放速度减慢
- s 键:speed 减小,播放速度加快
- r 键:恢复初始速度
主进程中的 FRAME 表示到目前为止已经识别的总帧数,TIME 表示系统总运行时间,平均帧率 AVE_FPS 的计算公式如下:
[Math Processing Error]AVE_FPS=FRAMETIME
最后调用 main() 函数:
if __name__ == '__main__':
main()
测试运行效果
运行效果如下图所示:
- 测试结果显示,我们的模型能实时有效检测 person、face、traffic_light 对象
- 可通过键盘按键实时调节视频播放速度
- 增加了检测实时帧数、视频时长及帧率的显示功能
- 平均帧率如下表所示
模型 | 平均帧率 |
模型 1 | 30+ |
模型 2 | 30+ |
模型 1+ 模型 2 | 20+ |
模型 1:通过迁移学习得到的模型
模型 2: SSD-MobileNet-v2-2018(coco 数据集训练过)
以上数据测试于 NVIDIA GeForce GTX 850M 平台