https://github.com/matterport/Mask_RCNN
测试
1. 利用samples/demo.py进行测试, 加载训练好的模型mask_rcnn_coco.h5, 然后预测图像的输出. 模型输出格式为:
使用的是results[0], results[0]是一个dict, content如下:
key为rois, value为numpy array. 表示图像目标的位置, 二维矩阵[[], [], …], 每一个[]表示一个目标的bbox info, 具体为: ymin, xmin, ymax, xmax.
key为class_ids, value为numpy array, 一维向量, []. 每一个元素表示目标的class id. id从0开始, 0表示背景.
key为scores, value为numpy array, 一维向量, []. 每一个元素表示目标的置信度. scores和class_id是一一对应的.
key为masks, value为numpy array, 三维矩阵, [[[]], [[]], [[]], …]. 每一个二维矩阵[[]]表示一个目标的mask matrix.
2. 使用GPU
修改./samples/demo.py中InferenceConfig class的参数即可, 另外需要保证Batch size = GPU_COUNT * IMAGES_PER_GPU, 否则会报错! 是同时输入两个图像, 如在results = model.detect([image], verbose=1)时, len([image]) == Batch size = GPU_COUNT * IMAGES_PER_GPU, 其中image是numpy array数组.
3. 将模型输出结果保存至txt files, 然后计算mAP, Recall, IoU等值. 比较结果.
现在先进行两个子实验: 车辆检测和行人检测. <class_name> confidence
- 车辆检测, 车辆使用vehicle.
- 行人检测, 行人使用person.
python main.py --ground_truth /home/ly/git/darknet/results/vehicle/ground_truth --predicted /home/ly/git/Mask_RCNN/samples/results/txt/vehicle --images /mnt/dataresource/53万张街景标框数据_12万/data/day/cloudy/city
python main.py --ground_truth /home/ly/git/darknet/results/person/ground_truth --predicted /home/ly/git/Mask_RCNN/samples/results/txt/person --images /mnt/dataresource/53万张街景标框数据_12万/data/day/cloudy/city
另外, 这个项目在保存IoU的结果时, 是这样保存的: 每个图像仅保存一个目标的IoU值, 在图像中将pred和GT的bbox画出来, 然后显示IoU. 因为, 数据集一共有多少个object, 那么就保存多少的图像.
4. 对一个正在执行的线上图像的目标标注任务进行测试, 2D街景数据标注.
代码见./samples/demo_2D_street_view.py
- 有Ground Truth的测试. 保存图像标注结果, 计算目标检测评价指标!
利用./samples/scripts/extract_json_to_txt.py提取JSON文件中的<class_name> , 存放在txt文件中.
由于2D街景图像标注的标注文件有些不一样, 所以需要修改python文件. 标注文件为:
一行代表一张图像的标注文件, 是一个{}, 里面有markResult, 是bbox信息. title是图像文件. 标注的信息是:
5个坐标点, 5个(x, y)坐标.
将图像检测结果以及txt文件存放在./samples/results/2D_street_view_data_annotated下.
<1> 车辆检测, 车辆使用vehicle.
<2> 行人检测, 行人使用person.
python main.py --ground_truth /home/ly/git/Mask_RCNN/samples/results/2D_street_view_data_annotated/ground_truth/vehicle --predicted /home/ly/git/Mask_RCNN/samples/results/2D_street_view_data_annotated/txt/vehicle --images /home/ly/git/Mask_RCNN/datasets/2018第1432-1433期图片标注任务_2D街景标注_行人_车辆/DT_part1_11
python main.py --ground_truth /home/ly/git/Mask_RCNN/samples/results/2D_street_view_data_annotated/ground_truth/person --predicted /home/ly/git/Mask_RCNN/samples/results/2D_street_view_data_annotated/txt/person --images /home/ly/git/Mask_RCNN/datasets/2018第1432-1433期图片标注任务_2D街景标注_行人_车辆/DT_part1_11 - 没有Ground Truth
4. 训练
如果想只训练目标检测分支的话, 就要将Mask分支以及相应的loss函数去掉. 然后准备好数据集就可以训练了. 需要修改的地方有两个:
- 加载数据集的子类需要重新定义! 这个代码可以借鉴https://blog.youkuaiyun.com/l297969586/article/details/79140840/, 其实数据集子类只需要写一个load_***()即可, 加载Image和其标注文件.
按照./samples/shapes/train_shapes.py重修改写一下数据集子类和主程序, 并参考./samples/coco.coco.py, 程序见./samples/train_street_view.py. - 修改./mrcnn/model.py下的class Mask_RCNN, 将模型修改一下.
- 另外, ./samples/shapes/train_shapes.py训练和测试写在了一起, 目前train_street_view.py只包含训练代码!
- 修改: <1> 如果目标的size比较小, 就选择比较小的anchor; 目标的size比较大, 就选择比较打的anchor. <2> 每张图像产生几个ROIs, 这个要和数据集相匹配. 所以对这两个指标, 要先对数据集进行一定的统计, 可以画出一些直方图, 计算平均值, 最大最小值等指标, 确定这些参数. 调试技巧!
想简单了, 如果采用这套代码只进行目标检测. 那么修改的代码量很大! 不仅要重写一个sub class, 还要将涉及到的所有关于数据集的代码都修改一下! 这样的话, 还不如将代码重现一下! 可以这样处理: 借鉴YOLO V3 Kears+Tensorflow版本的, 数据集借鉴这的东西. 模型的建立和训练借鉴Mask RCNN Keras+Tensorflow版本的! 好麻烦啊!
5. demo_2D_street_view_save_result.py
利用pillow库保存bbox和text! matplotlib保存时, 图像大小会改变!! 利用pillow库保存时会好一些!
demo_2D_street_view_save_result_to_json.py, 将检测结果写入json文件, 写入json文件时, 我们只需要保持json的格式正确即可, 至于格式化的事情可以不用考虑.
6. 利用Mask RCNN模型仅做目标检测实验, 即数据集的标注只有bbox info, 没有mask info. 两种思路解决这个问题:
- 修改Mask RCNN源代码, 将有关于mask分支的部分去掉, 但是任务较重, 不太好修改.
- 修改数据集的标注情况. 将mask标注部分改为bbox的标注部分, 即mask不再是objects的边缘, 而是bbox的边缘!
对于第二种方案, 需要查询mask是如何制作的, 以此来制作自己的mask标注.
训练Mask-RCNN用的到的文件有三种: 原图像(jpg, 即训练图像), mask(png, 即mask图像), info.yaml(info.yaml里存放的是label的名字: 分为背景, 物体1, 物体2, … 的name). - 利用Labelme制作mask图像.
根据Labelme制作mask图像的流程先制作一个mask图像, 看一看Labelme制作后的JSON文件格式. Labelme官方网址:
https://github.com/wkentaro/labelme
python3
conda create --name=labelme python=3.6
source activate labelme
conda install -c conda-forge pyside2
conda install pyqt
pip install pyqt5 # pyqt5 can be installed via pip on python3
pip install labelme
Labelme制作mask流程可以参考https://blog.youkuaiyun.com/l297969586/article/details/79140840/, 制作流程很简单.
1> 安装完Labelme后, 命令行输入labelme, 就可以打开labelme的图形化界面 --> 打开图像 --> create polygon --> 画出多边形的点, 最后形成一个封闭的图像 --> 输出object的名字 --> save. Save之后, 就会在图像目录下生成*.json. 我们就可以按照这个json文件的格式, 将53万街景图像标注info制作成labelme生成的json文件.
2> 利用labelme生成json文件后, 我们需要生成mask图像. 命令行输入: labelme_json_to_dataset *.json(刚才使用labelme生成的json文件), 这样就会在图像目录下生成一个和图像同名的文件夹, 其中包括img.png(原始图像), info.yaml(记录的是labels names), label_names.txt(记录的是labels names), label.png(maks图像, 网络的输入), label_viz.png(mask图像显示图像, 将mask部分在原图像中进行显示的结果).
这样, 接下来的工作就是53万街景标注图像的标注文件制作成labelme生成的json文件格式即可. 详细代码见./samples/scripts/transform_label_to_labelme_json.py. 加载53外街景标注图像的标注文件(也是json文件)时可以参考~/git/YOLOV3/scripts/generate_train_data.py代码, 介绍了如何解析53外街景标注图像的标注文件(也是json文件).
3> 在改写53万街景标注图像的标注文件前, 我们首先要获取所有的标注的class names, 这样在设置3类的时候才方便设置我们要设定的vehicle, person.
4> 对53万街景标注图像的标注文件的改写, 我们设置的类别和Mask RCNN的要求必须一致, 不然检测和分割的结果应该不会太好. 即对于同一个class name的object, 我们再进行细分, 细分为不同的实例. 如: car_1, car_2, …
在实验的时候, 是直接给class name加上了一个_j, 这样设置可能不对, 应该是同一个class name的_j是连续的. 这样设置可能更好一点!
如box1, box2, fruit1, fruit2… 然后再训练时会使用str1.find(str2), 如果str2再str1中, 返回0, 如果不存在返回-1!
设置一个list即可, 存一下class names!
5> Labelme标注JSON文件解析可见/home/ly/git/Mask_RCNN/samples/labelme/2014-08-19120207.json.
6> 有一个小问题, 图像数据再转换为字符串时, 字符串不能包含’b’, 修改一下! 利用base64转换后的结果类型为bytes, 即输出为b’asd’, 而我们最终需要的是字符串, 所以要将b’'去掉. 在对bytes进行decode()即可!
7. 先拿1000张图像的进行初步的测试! 1000张图像的比较少, 先看看效果再说!
-
运行transform_label_to_labelme_json.py, 首先生成Labelme需要的JSON格式文件.
-
写一个shell脚本, 批量将转换完的JSON文件, 生成mask图像, yaml文件! 代码可以见./samples/scripts/generate_mask_info.sh. 代码借鉴https://blog.youkuaiyun.com/l297969586/article/details/79140840/. 在相同的目录下产生mask图像等数据(同名文件夹).
-
训练代码, 可借鉴https://blog.youkuaiyun.com/l297969586/article/details/79140840/写训练代码, 同时还可以借鉴./samples下的shapes, balloon, nucleus等试验的设置. 使用数据集为1000张图像数据集, 具体的class names在./datasets/Street_View_classes_1000.names下! 详细代码见./samples/street_view/street_view_1000_datasets.py.
-
将数据集分为两部分: 训练集和验证集. 训练集990样本, 测试集10样本!
python street_view_1000_datasets.py --command train -
IndexError: boolean index did not match indexed array along dimension 0; dimension is 2 but corresponding boolean dimension is 1
这个错误出现在model.py 1270行左右:
_idx = np.sum(mask, axis=(0, 1)) > 0 # numpy
mask = mask[:, :, _idx] # numpy
print(_idx)
class_ids = class_ids[_idx] # numpy. 这个地方出错!
需要保证, len(_idx)要和class_ids的长度一样.
这里_idx是一个一维的numpy, 元素为True或False. 例如
class_ids = np.array([1,2,3,4,5])
_idx = np.array([True, False, True, False, True, True])
class_ids[_idx]只会提取_idx为True时的class_ids位置的元素! 则结果为:
np.array([1,3,4,5]) -
加一步操作: 将Lableme生成的mask图像, 即*.png使用OpenCV C++代码转换一下再看看! 应该是这个错误!
labelme生成的掩码标签label.png为16位存储, opencv默认读取8位, 需要将16位转8位. 如果该图为16位, 则读取为全0, 导致程序出错. 可以借鉴https://blog.youkuaiyun.com/l297969586/article/details/79154150代码(C++代码).
1> 首先获取所有所有的Labelme文件夹, 即*_json文件夹, 代码可见./samples/street_view_train/get_all_mask_folders.py
2> 运行C++程序, 将16位存储label.png转换为8位图像.
g++pkg-config --cflags opencv
16bits_2_8bits.cpppkg-config --libs opencv
# --cflags和–libs分开!! WHAT!!!
不是这个错误!! 做尼玛这个干啥!! 网上有说编译时加上-lpthread即可! https://blog.youkuaiyun.com/lovebyz/article/details/80138261.
做这个操作有错误! 做完这个操作后, 一张图像的实例个数会增加! 错误! 而且Labelme生成的label.png就是8位存储的!
这个操作没用! -
IndexError: boolean index did not match indexed array along dimension 0; dimension is 2 but corresponding boolean dimension is 1
错误原因应该在load_mask()函数中, labels_form的生成有误! 这个东西的长度比num_obj大1, 就是这个错误!
卧槽! 这个错误是Street_View_classes_1000.names中的问题, 因此class names有的是大写, 有的是小写. 所以会返回找, 出现重复, 因此labels_form的长度比num_obj大!
应该换种方法设置标签才对! 不能使用str.find()方法, 使用find()方法会出错, 例如electric cyclist/motorcyclist_0在electric cyclist/motorcyclist可以找到, 在cyclist中也可以找到. 这样就重复了, 导致labels_form的长度比_idx的大. 程序才出错!!
str1.find(str2): 字符串str1是否包含字符串str2, 如果包含返回第一次出现的位置, 否则返回-1!
这里取_0, _1, 2前面所以的字符即可, 判断两个字符串是否一样! str(labels[i][:-2]) == str(self.values[j].strip("\n")) 出错!
这样处理不对, 因为有些字符串是: ‘electric cyclist/motorcyclist_3’, 有些字符串是’mini car_10’, 如果仅取str[:-2], 会出错!
这样处理吧: str(labels[i][:-(len(labels[i].split("")[-1]) + 1)]) == str(self.values[j].strip("\n"))
- 处理完这个问题, 还有问题, 问题在于制作Labelme所需的JSON文件的错误! 标签给定的时候出了错误! 各个实例的标签设置错了, 具体的可以找几张label_viz.png图像便知!
这个问题好像不会引发错误, 因为假设我们标注的是pedestrian _0和pedestrian 100000, 但其实这两个实例的真正的标签都是pedestrian . 即我们会这样取: str(labels[i][:-(len(labels[i].split("")[-1]) + 1)]), 即取最后一个_之前所有的字符串作为该实例的标签!
这个问题可以利用list的count()方法解决, 即使用count()函数, 统计某个元素在list中出现的次数. 从而设置class name的count数, 这样就对了!
-
有的数据集没有标注, JSON文件中bbox信息是空的, 即mask图像是全黑的. 对于这样的图像要跳过的!
通过yaml文件简单判断一下. -
利用Labelme生成mask图像的时候, 有时候不稳定出错了. 例如2015-04-01165204_json, 在生成mask图像时, 少了dontcare_0这个!
这个问题出现的还不少! 或者去掉dontcare这个东西!! 在生成Labelme所需的JSON数据集时, 去掉即可!
8. 制作53万街景数据集的mask图像.
- 首先获取all class names. 代码可见./samples/scripts/generate_class_names.py. 使用glob递归查找文件.
jiangroot用户下的文件权限改为777即可, 即/mnt/dataresource下的文件. 这样, 普通用户就有读写权限了! - 将标注的json文件转换为Labelme需要的JSON文件, 存放在/mnt/dataresource同文件夹下.
- 生成mask图像, 这个可以用程序来做. 程序见/home/ly/miniconda3/envs/labelme/lib/python3.6/site-packages/labelme/cli下的json_to_dataset.py, 即labelme_json_to_dataset调用的程序就是json_to_dataset.py. 在python中, 加入for循环即可. 详细代码见./samples/scripts/labelme_json_to_dataset.py.
可以试试使用Python多线程/多进程, 这样加速效果很明显!
source activate labelme
python labelme_json_to_dataset.py # 在Labelme json文件同目录下生成mask图像和yaml文件.
9. 使用CPU时, 如果for循环比较大, 可以采用多进程加速, 很好用.
10. 将./mrcnn/config.py下的参数看一下, 看看哪些参数是可以修改的.
11. 53万街景标注图像
全部的class names可见./datasets/CLassNames/Street_View_classes_530thousand.names, 去掉dontcare类, 一共有85类. 训练代码见./samples/street_view_train/street_view_530thousand_datasets.py.
训练集可见/mnt/datasets/apy170301415_0_53万张街景图片_53万张标框数据, 测试数据采用1000张的53万街景标框数据.
1> 53万原始图像, 有的文件夹如data/day/cloudy/crossroads为空, 这样原始图像为空, 有的mask图像可能生成错误, 所以我们需要按照mask图像/yaml文件, 反向寻找原始图像!!! 即mask图像/yaml文件存在, 那么原始图像一定存在. 通过yaml文件寻找!!! 训练集和验证集都需要recursive=True.
训练集:
原始图像如: /mnt/dataresource/apy170301415_0_53万张街景图片_53万张标框数据/完整数据包/data/day/cloudy/city/2014-08-19120207.jpg
maks图像如: /mnt/dataresource/apy170301415_0_53万张街景图片_53万张标框数据/Labelme_json_files/data/day/cloudy/city/2014-08-19120207_json/label.png
yaml文件如: /mnt/dataresource/apy170301415_0_53万张街景图片_53万张标框数据/Labelme_json_files/data/day/cloudy/city/2014-08-19120207_json/info.yaml
验证集:
原始图像如: /home/ly/DATASETS/53万张街景标框数据1000样例/2014-08-19120207.jpg
maks图像如: …/datasets/530_thousand_street_view_dataset_mask/2014-08-19120207_json/label.png
yaml文件如: …/datasets/530_thousand_street_view_dataset_mask/2014-08-19120207_json/info.yaml
12. 训练加速:
1> 根据mask图像生成mask matrix, 3D matrix的过程比较慢. 可以简单测试一下, 另外可以将mask matrix保存至本地, 再加载, 测试时间.
详细代码见./samples/scripts/test_mask_matrix_generate.py.
通过简单地测试, 根据mask图像生成mask matrix(800 * 1280 * num_obj)的时间大约在8s. 而生成完mask matrix后, 使用np.load()读取, 只需要约0.007s. 多进程一起跑即可.
基于上面的mask matrix的生成时间, 目前想做的事情就是首先外部生成mask matrix, 然后保存为npy文件. 训练Mask RCNN模型时, 读取npy文件即可. 详细代码见./samples/scripts/test_mask_matrix_generate.py. mask matrix npy的结构和mask图像的结构一致. 即*/data/
# day/cloudy/city/2014-08-19120207_json/*.
2> 读取yaml文件时, 不再使用glob()进行递归读取, 速度太慢(大约需要2.5个小时!!!), 我们可以这样处理. 将yaml的路径写进一个txt文件, 在写入txt文件时, 我们就可以指定train和val数据集了. 详细代码见./samples/scripts/generate_yaml_to_txt.py. 这样再读取yaml的文件时, 可以直接读取这样的txt文件即可.
在将yaml文件生成txt文件时, 已经进行了train和val的划分, 因此就不必再使用1000样例作为val了!
3> 53万次for循环self.add_image()循环调用, 占用时间巨大这不太好改, 如果想中途重启训练程序, 等待的时间太长(6926s). 调用add_image(), 其实就是想更新self.image_info, 是一个列表. 这样的话, 我们可以将这个列表先生成存为JSON文件. 这样就简单多了! 详细代码见./samples/scripts/generate_image_info.py.
顺利生成了train_530thousand_image_info.json和val_530thousand_image_info.json, 修改一下./mrcnn/utils.py下的add_image()即可. 在Dataset基类的add_image()中, 直接加载保存好的json文件至list中即可.
4> 如果可以顺利生成mask matrix npy文件, 那么就修改load_mask()函数, 修改mask matrix的加载方式, 直接从npy文件中加载mask matrix. 详细代码见./samples/street_view_train/street_view_530thousand_datasets_txt_mask_npy.py.
./samples/street_view_train/street_view_530thousand_datasets_txt_mask_npy.py代码虽然修改了很多, 但是版本一直在更新. 为了使得重启速度更快, 以及训练速度更快.
和Tensorflow1.9版本, 相匹配的cuDNN版本为v7.1.4, v7.1.2也不可以!
而且不需要将cuDNN加进/usr/local/cuda下, 这个可能和其他的深度学习库相冲! 如果其他的DL框架需要其他版本的cuDNN, 下载指定lib路径即可.
运行Tensorflow1.9的程序:
export LD_LIBRARY_PATH=/home/ly/CUDA/cudnn7.1.4/cuda/lib64:$LD_LIBRARY_PATH