数据标注懒人包:目标检测小样本手动标注解决方案(一)
目标检测开源半自动标注解决方案系列之一
0 项目背景
在PaddlePaddle系列套件的模型库中,有PPOCRLabel之于PaddleOCR、EISeg之于PaddleSeg的半自动标注解决方案,但是在适用场景相对最广、需求强烈的目标检测领域,一直缺少特别有效的解决方案。
注:EISeg的实例分割可以实现部分目标检测数据的标注,但相对于简单的矩形框标注而言,使用EISeg时间成本又比较高。
诚然,当前商用的目标检测数据标注选择还是比较多的,比如使用EasyDL进行快速标注。但在一些场景下,如因安全、成本、数据量等多种原因,数据上云受限。
因此,本系列项目,希望从目标检测方向切入,给出一种开源、本地及云上皆适用的数据标注解决方案。
当然,如果最后能形成PaddleDetection的半自动标注解决方案,那是再好不过啦,强烈欢迎各位大佬指导。
参考资料
本文有两个关联项目,关于生成VOC格式自动标注结果的关键代码、应用效果更详细的介绍,可参考:
虽然不是直接相关,但是一些思路有继承关系,可以帮助大家更好地理解。
1 整体思路
该项目从笔者面临的实际需求而来,如,笔者目前有2100张图片需要标注,标注工作量非常大。笔者先花了0.5个工作日,完成了6类目标共300张图片的标注,标签也相对平均分布。由于数据授权存在限制,在本项目中,使用平台现有的VOC示例数据集进行展示。
作为全系列的开篇,我们先走极简路线,选的是相对最为简单的标注方式:LabelImg。同时,用的标注框也是最简单的——标准矩形框。
整体思路如下:
- 少量手动标注
- 使用LabelImg,每个目标类别标注约50张图片
- 训练检测模型
- 使用PaddleDetection基于少量标注的数据集进行训练
- 观察训练结果,如果mAP较为满意,保存预测模型
- 生成自动标注
- 使用基于少量标注训练的模型,对未标注数据进行预测,将预测结果保存为VOC格式
- 矫正标注结果
- 基于数据集特性,矫正目标标签
- 导出预测数据的标注文件,回到LabelImg中,修正目标矩形框和标签
2 工具及数据集准备
# 获取示例数据集
!wget https://bj.bcebos.com/paddlex/datasets/insect_det.tar.gz
# 解压数据集
!tar -zxvf insect_det.tar.gz
作为PaddleX目标检测项目的示例,昆虫数据集其实是一个完整标注的数据集。
但是,我们探索的是基于少量标注的解决方案,并且,实际情况中,使用LabelImg逐一标完数据也是非常困难的,因此,这里假设只标注了昆虫数据集的前99张图片。
基于此,整个示例的数据集要进行重新划分。
# 删除100张之后的标注
!rm insect_det/Annotations/01*.xml
!rm insect_det/Annotations/02*.xml
这里,我们马上会遇到第一个问题:数据量和标注量不匹配。在这种情况下,使用PaddleX的数据集切分工具,是不能完成VOC数据划分的。
!pip install paddlex==1.3
!paddlex --split_dataset --format VOC --dataset_dir insect_det --val_value 0.2 --test_value 0.1
[1;31;40m2022-01-17 16:40:44 [ERROR] The annotation file 0196.xml doesn't exist![0m[0m
[0m[0m
因此,我们需要将未标注的数据挪到测试集中。这对后续工作也是有好处的,毕竟到时候预测的时候,直接对测试集做预测、生成标注就行。
# 准备测试集目录
!mkdir insect_det/TESTImages
import os
import re
import shutil
# 获取文件名
def getfiles(ann_path):
filenames = os.listdir(ann_path)
return filenames
# 移动文件
def mymovefile(srcfile,dstfile):
if not os.path.isfile(srcfile):
print("src not exist!")
else:
fpath,fname=os.path.split(dstfile) #分离文件名和路径
if not os.path.exists(fpath):
os.makedirs(fpath) #创建路径
shutil.move(srcfile,dstfile) #移动文件
# 找到没有标注的文件并移动
def move_file(filePath,testPath):
for file_path, empty_list, file_name_list in os.walk(filePath):
# file_name_list该列表是存放目标目录中所有文件名
for file_name in file_name_list:
ann_list = getfiles(r'insect_det/Annotations')
if (file_name[:-3]+'xml') not in ann_list:
src = filePath+'/'+file_name
dst = testPath+'/'+file_name
print(src)
mymovefile(src,dst)
move_file('insect_det/JPEGImages','insect_det/TESTImages')
# 划分手动标注的数据集
!paddlex --split_dataset --format VOC --dataset_dir insect_det --val_value 0.2 --test_value 0.0
Dataset Split Done.[0m
[0mTrain samples: 80[0m
[0mEval samples: 19[0m
[0mTest samples: 0[0m
[0mSplit files saved in insect_det[0m
[0m[0m
3 训练检测模型
这里目标就是模型能尽快收敛,以最快速度跑出一个效果过得去的baseline,比如本文选择用PaddleDetection/configs/yolov3/yolov3_darknet53_270e_voc.yml
,原因也很简单:只需要改改数据集路径、调整下学习率即可,其它现成的配置文件都有了,不需要特别关心。
请注意,我们既然选择小样本标注,这一步就不太需要纠结于“炼丹”,模型效果过得去,能减少后续矫正时工作量就行——当然,如果基于小样本标注的模型效果很好,后面也省力;只是从概率上说,考虑到对于复杂数据集,基于小样本数据集的模型训练恐怕很难达到完美。个人建议,这个步骤还是遵循效率第一、效果第二的思路。
3.1 修改数据集路径配置
具体的配置文件处理上,第一个重点就是PaddleDetection/configs/datasets/voc.yml
中数据集路径的配置。
metric: VOC
map_type: 11point
num_classes: 8
TrainDataset:
!VOCDataSet
dataset_dir: /home/aistudio/insect_det
anno_path: train_list.txt
label_list: labels.txt
data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']
EvalDataset:
!VOCDataSet
dataset_dir: /home/aistudio/insect_det
anno_path: test_list.txt
label_list: labels.txt
data_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']
TestDataset:
!ImageFolder
anno_path: /home/aistudio/insect_det/labels.txt
3.2 修改训练超参数
比如PaddleDetection/configs/yolov3/_base_/optimizer_270e.yml
可以做如下修改:
epoch: 70
LearningRate:
base_lr: 0.001
schedulers:
- !PiecewiseDecay
gamma: 0.1
milestones:
- 50
- 60
- !LinearWarmup
start_factor: 0.
steps: 400
OptimizerBuilder:
optimizer:
momentum: 0.9
type: Momentum
regularizer:
factor: 0.0005
type: L2
3.3 开始训练
没错!没有要改的了,直奔主题,开始训练!
!git clone https://gitee.com/paddlepaddle/PaddleDetection.git
%cd PaddleDetection/
/home/aistudio/PaddleDetection
!pip install -r requirements.txt
!export CUDA_VISIBLE_DEVICES=0
!python tools/train.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml --use_vdl=True --vdl_log_dir=./output --eval
!python tools/eval.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml -o weights=output/yolov3_darknet53_270e_voc/59.pdparams
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/tensor/creation.py:130: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe.
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
if data.dtype == np.object:
Warning: import ppdet from source directory without installing, run 'python setup.py install' to install ppdet firstly
W0114 20:49:31.823413 825 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0114 20:49:31.828456 825 device_context.cc:465] device: 0, cuDNN Version: 7.6.
[01/14 20:49:35] ppdet.utils.checkpoint INFO: Finish loading model weights: output/yolov3_darknet53_270e_voc/59.pdparams
[01/14 20:49:35] ppdet.engine INFO: Eval iter: 0
[01/14 20:49:37] ppdet.metrics.metrics INFO: Accumulating evaluatation results...
[01/14 20:49:37] ppdet.metrics.metrics INFO: mAP(0.50, 11point) = 82.00%
[01/14 20:49:37] ppdet.engine INFO: Total sample number: 63, averge FPS: 28.58731442371075
下面展示笔者在真实的手动标注数据集上训练的效果。对300张手动标注数据集进行划分后,60个epoch的训练总时长约65分钟,验证集和测试集的mAP就达到0.8+。
可以选取一些测试集的图片进行验证,如果感觉检测效果已经不错了,就进入下一个环节:生成数据标注。
!python tools/infer.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml --infer_img=../insect_det/TESTImages/0101.jpg -o weights=output/yolov3_darknet53_270e_voc/59.pdparams --draw_threshold=0.1
!python tools/infer.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml --infer_img=../Dataset/TESTImages/62.jpg -o weights=output/yolov3_darknet53_270e_voc/59.pdparams --draw_threshold=0.1
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/tensor/creation.py:130: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe.
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
if data.dtype == np.object:
Warning: import ppdet from source directory without installing, run 'python setup.py install' to install ppdet firstly
W0117 23:43:50.255735 4436 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0117 23:43:50.265116 4436 device_context.cc:465] device: 0, cuDNN Version: 7.6.
[01/17 23:43:53] ppdet.utils.checkpoint INFO: Finish loading model weights: output/yolov3_darknet53_270e_voc/59.pdparams
[01/17 23:43:54] ppdet.engine INFO: Detection bbox results save in output/62.jpg
4 生成预测标注结果
在这一步,重点就是对PaddleDetection的源代码进行修改。但是,磨刀不误砍柴工,我们在改代码前,先要做到:明确目标、理解代码、定位函数,最后才是大刀阔斧动手改代码。
4.1 明确目标:分析标注文件
在从OCR到(旋转)目标检测:四点标注转VOC和roLabelImg这个项目中,已经提到,极简版的VOC格式标注如下:
<annotation>
<filename>2012_004331.jpg</filename>
<folder>VOC2012</folder>
<object>
<name>person</name>
<bndbox>
<xmax>208</xmax>
<xmin>102</xmin>
<ymax>230</ymax>
<ymin>25</ymin>
</bndbox>
<difficult>0</difficult>
</object>
<size>
<depth>3</depth>
<height>375</height>
<width>500</width>
</size>
</annotation>
从该项目生成VOC格式数据集的关键代码中,我们也可以看出,其实重点就是要解决8个参数的赋值:
- 图片名
filename
- 图片宽
width
- 图片高
height
- 目标标签
label
- 定位标注框的坐标
xmin,ymin,xmax,ymax
或x,y,w,h
with open(os.path.join('./Annotations', filename.rstrip('.jpg')) + '.xml', 'w') as f:
f.write(f"""<annotation>
<folder>JPEGImages</folder>
<filename>{filename}.jpg</filename>
<size>
<width>{width}</width>
<height>{height}</height>
<depth>3</depth>
</size>
<segmented>0</segmented>\n""")
for label, x1, y1, x2, y2 in mem[filename]:
f.write(f""" <object>
<name>{label}</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>{x1}</xmin>
<ymin>{y1}</ymin>
<xmax>{x2}</xmax>
<ymax>{y2}</ymax>
</bndbox>
</object>\n""")
f.write("</annotation>")
另外两个注意事项:
因为我们是希望生成了标注文件还要放回LabelImg去做后续手动调整的,所以本项目在保存VOC标注文件时,还要记得把<path>
写进去,比如<path>F:\windows_v1.8.1\JPEGImages\{filename}.jpg</path>
。
同时,我们肯定也希望,放回自动生成的标注文件后,可以将它们和已经手动标注确认了的文件区分开来,因此,生成标注文件的开头,应该是<annotation verified="no">
。
4.2 理解代码:PPDET是怎么画预测框的
在预测图片时,我们用的是PaddleDetection/tools/infer.py
,很显然,既然能画框(而且框上有标签名),说明预测的时候能正常返回bbox
坐标以及目标的label
;同时,我们会发现输出的预测图片,文件名和输入时是一样的,那么filename
也搞定了;更不用说,本身输入待预测图片的路径,这也意味着width
和height
能获取到……
所以,仔细算一算,上面提到的8个参数的赋值,在PPDETinfer
过程中,都是能比较轻松获取到的。接下来,就是找到它们,并且用最快的速度,将它们重新组装起来,用最快速度生成VOC标注数据格式。
4.3 定位函数:找到要改的文件和函数
接下来,就是一个返查倒推的过程,这里强烈点赞AIStudio的代码跳转功能,网页端也能像IDE一样简单操作。
从PaddleDetection/tools/infer.py
的
def run(FLAGS, cfg):
# build trainer
trainer = Trainer(cfg, mode='test')
# load weights
trainer.load_weights(cfg.weights)
# get inference images
images = get_test_images(FLAGS.infer_dir, FLAGS.infer_img)
# inference
trainer.predict(
images,
draw_threshold=FLAGS.draw_threshold,
output_dir=FLAGS.output_dir,
save_txt=FLAGS.save_txt)
找到PaddleDetection/ppdet/engine/trainer.py
的
def predict(self,
images,
draw_threshold=0.5,
output_dir='output',
save_txt=False):
……
if save_txt:
save_path = os.path.splitext(save_name)[0] + '.txt'
results = {}
results["im_id"] = im_id
if bbox_res:
results["bbox_res"] = bbox_res
if keypoint_res:
results["keypoint_res"] = keypoint_res
save_result(save_path, results, catid2name, draw_threshold)
start = end
最后是PaddleDetection/ppdet/utils/visualizer.py
的
def draw_bbox(image, im_id, catid2name, bboxes, threshold):
def save_result(save_path, results, catid2name, threshold):
其实整个排查路线非常清晰,我们也会发现,我们需要的8个参数赋值,PaddleDetection/ppdet/engine/trainer.py
和PaddleDetection/ppdet/utils/visualizer.py
对应的几个函数里都有覆盖到了。
4.4 动手修改:最小改动、最快效率
上面其实已经拎出了一个关键函数save_result(save_path, results, catid2name, threshold)
,控制它的开关就是--save_txt
,显然,PaddleDetection特别贴心,知道有时候我们需要输出bbox
的具体结果,而不是封装好的绘图效果。
如果要追求最小改动、最快效率,自动生成标注文件,那么直接把生成VOC的代码插入到save_result()
里,并做相应修改即可(传参的时候要传入图片宽、高)。
不过,为了便于理解,本项目没有直接用--save_txt
控制开关,而是依样画葫芦,写了个--save_voc
开关,由于篇幅限制,详情请读者查看修改后的infer.py
,trainer.py
和visualizer.py
这三个文件,用的时候直接替换即可。
注:PPDET预测的
bbox
坐标是float
格式,必须转换成int
格式,否则放回LabelImg识别不出来。
!cp ../infer.py ppdet/utils/visualizer.py
!cp ../trainer.py ppdet/engine/trainer.py
!cp ../visualizer.py ppdet/utils/visualizer.py
# 单文件预测,检查生成的voc格式效果
!python tools/infer.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml --infer_img=../insect_det/TESTImages/0101.jpg -o weights=output/yolov3_darknet53_270e_voc/59.pdparams --draw_threshold=0.1 --save_voc=True
# 批量生成标注文件
!python tools/infer.py -c configs/yolov3/yolov3_darknet53_270e_voc.yml --infer_dir=../insect_det/TESTImages -o weights=output/yolov3_darknet53_270e_voc/59.pdparams --draw_threshold=0.1 --save_voc=True
# 随机抽一个检查标注文件是否正确
# !cat output/62.xml
!cat output/0101.xml
<annotation verified="no">
<folder>JPEGImages</folder>
<filename>62.jpg</filename>
<path>F:\windows_v1.8.1\JPEGImages\62.jpg</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>1280</width>
<height>960</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>R-white</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>453</xmin>
<ymin>771</ymin>
<xmax>572</xmax>
<ymax>919</ymax>
</bndbox>
</object>
</annotation>
5 矫正标注结果
一种矫正标注结果的方式非常直接而简单,就是把生成的标注文件打包下载,放回LabelImg中手动调整即可。
这里主要想说,面对个别特殊数据集,我们还有一种“更懒”的办法。
所谓特殊数据集,指的是,一张图片里,只有一种目标的情形——看似特殊,其实在工业质检的场景中应非常常见。
举个例子,比如布匹的瑕疵检测。按常识想,一块布料同时存在多种瑕疵,概率应该不太高吧?
往往是设备哪里坏了,比如漏油,导致某段时间经过的布料集中存在油污;又或者是哪里过于尖锐,导致经过的布料被划伤……如果是这种情况,其实反而与图像分类会比较类似,一张图里,我们需要找的label只有一种,那么参考图像分类数据集的组织形式,原始数据集可以这样处理:
.
├── 目标一
├── 目标二
│ ├── 001.jpg
│ ├── 002.jpg
│ ├── 003.jpg
│ └── ……
├── 目标三
├── 目标四
└── ……
然后再通过命令行的重命名脚本,将图片统一改成类似A001.jpg
,B001.jpg
,B002.jpg
……的格式,ABC
对照label123
,这样,某张图片中,全部目标label
的信息就出现在了文件名里。接下来我们需要做的,无非是,提取文件名中的label
,直接替换掉文件对应的VOC标注里,目标的label
。
注:之所以用ABC区分,是因为看起来操作比较简单,截取字符串首字母即可——当然,场景不一样,情况也就不一样,如果有几十种目标,可以考虑label+分隔符的形式。
对于上文讲的特殊数据集,经过前面提到的一番“骚操作”处理,我们会发现,这时,手动矫正只需要调整矩形框大小位置了!进一步降低了工作量。
而这个“矫正”的操作,甚至不需要额外进行,直接在save_result()
函数里面改就行!关键代码如下:
filename = os.path.basename(save_path)[:-4]
if filename[0] == 'E':
label = 'labelE'
elif filename[0] == 'F':
label = 'labelF'
else:
label = catid2name[catid]
注:再次强调,能不能进行这种操作得看具体情况!比如昆虫检测这个示例数据集就没法这样写!
6 小结
通过实际操作估算,笔者标注全部数据集时,前300张图片手动标注花了0.5天,然后用了约1.5个小时,训练得到自动生成的VOC标注文件,灌回LabelImg后手动矫正,剩下1800张图片的标注才用了0.5天……效率提高了500%以上!
当然,不同数据集标注提速肯定有差异……
系列项目后续思路:
- 构建pipeline
- 旋转目标检测小样本手动标注解决方案
- EISeg标注+小样本图像分割/实例分割转目标检测
- 远期目标,实现类似vott的解决方案,比如把vott的引擎换成PPDET/paddlejs(大坑)
或者各位读者要是有更好的点子,敬请不吝赐教~~
更多推荐
所有评论(0)