从OCR到(旋转)⽬标检测:四点标注转VOC和roLabelImg
0 项⽬背景
本项⽬来源于⼀个PaddleOCR垂类场景,该场景对检测模型准确率需求较⾼,由于担⼼PaddleOCR的检测器模型效果可能不能满⾜需求,因此希望尝试通过PaddleDetection模型库提⾼对⽬标框的检测效果。
1 PaddleOCR模型原理
PP-OCR是⼀个实⽤的超轻量OCR系统。主要由DB⽂本检测、检测框矫正和CRNN⽂本识别三部分组成。该系统从⾻⼲⽹络选择和调整、预测头部的设计、数据增强、学习率变换策略、正则化参数选择、预训练模型使⽤以及模型⾃动裁剪量化8个⽅⾯,采⽤19个有效策略,对各个模块的模型进⾏效果调优和瘦⾝(如绿框所⽰),最终得到整体⼤⼩为3.5M的超轻量中英⽂OCR和2.8M的英⽂数字OCR。
因此,我们知道PP-OCR其实是三个模型的串接,它们分别是:
⽂本检测模型
⽂本识别模型
⽂本⽅向分类器模型
2 场景分析
本项⽬⾯临的是⼀个电表读数和编号识别场景,在该垂类场景中,由于并不是需要全部的⽂本,同时场景情况⼜⽐较复杂,⽐如存在反光、甚⾄电表读数也不是全部需要识别的(只需识别整数位)。
使⽤PPOCRLabel半⾃动标注⼯具标注后,其中⼀条结果如下所⽰:
M2021/IMG_20210712_101222.jpg [{"transcription":"02995","points":[[1151.0,1394.0],[1898.0,1411.0],[1894.0,1558.0],[1146.0,1541.0]],"difficult": fa l},{"transcription":"2002-00053452","points":[[1152.0,2543.0],[1801.0,2543.0],[1801.0,2651.0],[1152.0,2651.0]],"difficult": fal}]
所以,⽤的是四点标注模式,显然,是⼀个不规则的四边形。
但是我们知道,如果不使⽤PP-OCR的检测器,⽽希望⽤PaddleDetection模型库的话,需要的是标准的矩形框(⽆论是否旋转),因此本⽂主要解决从四点标注到矩形标注转换的问题,希望能减少标注⼯作量,避免标两次。
(标注⼀次⽅案)PPOCRLabel四点模式半⾃动标注,通过规则和格式转换,构建检测数据集
⽬标检测数据集标注⼀次,裁剪后构成新的的数据集,再⽤OCR标注⼀次
使⽤PaddleDetection替代PP-OCR的检测器还有另⼀个好处,因为在该场景需要区分表号和电表读数,⽤检测模型⾃然会带出⽂字框的类别,这样就不需要对PP-OCR输出的结果再进⾏后处理了。
3 数据格式转换
3.1 四点标注转VOC数据集
3.1.1 VOC数据集格式要求
⾸先还是先来看看VOC数据集的⽬录结构。
.
├── Annotations
├── ImageSets(不需要)
│├── Action
│├── Layout
│├── Main
│└── Segmentation
├── JPEGImages
├── SegmentationClass(不需要)
└── SegmentationObject(不需要)
因为我们只考虑构成基础的矩形框,不打算做实例分割,所以这⾥其实很多⽬录都不需要,只要有图⽚、对应标注的xml⽂件,其实就可以了。
数据集切分的话,完全可以使⽤PaddleX⾃带的⼀键划分⼯具,所以,也不需要太操⼼。
接下来我们需要关⼼下标注⽂件的内容,当然,对⽐标准的VOC格式,仍然有很多字段可以删掉。
<annotation>
<filename>2012_004331.jpg</filename>
<folder>VOC2012</folder>
<object>
<name>person</name>
<actions>
<jumping>1</jumping>
<other>0</other>
<phoning>0</phoning>
<playinginstrument>0</playinginstrument>
<reading>0</reading>
<ridingbike>0</ridingbike>
<ridinghor>0</ridinghor>
<running>0</running>
<takingphoto>0</takingphoto>
<usingcomputer>0</usingcomputer>
<walking>0</walking>
</actions>
<bndbox>
<xmax>208</xmax>
<xmin>102</xmin>
<ymax>230</ymax>
<ymin>25</ymin>
</bndbox>
<difficult>0</difficult>
<po>Unspecified</po>
<point>
<x>155</x>
<y>119</y>
</point>
</object>
<gmented>0</gmented>
<size>
<depth>3</depth>
<height>375</height>
<width>500</width>
</size>
<source>
<annotation>PASCAL VOC2012</annotation>
<databa>The VOC2012 Databa</databa> <image>flickr</image>
课堂评价语
</source>
</annotation>
我们的极简版⽬标检测数据集,只需要:
<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>
3.1.2 定位标注框位置
这步的转换还是⽐较简单的,对于VOC数据集,需要找到xmin, ymin, xmax, ymax,⽽四点标注格式为(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1),其实只需要稍加计算就能解决边框点的位置。
3.1.3 确定打标逻辑
从OCR到Detection,还有⼀个需要解决的就是标签问题,我们需要把电表读数和表号区分开来,分别打标。在该场景中,主要是通过规则进⾏判定,原因很简单,⼀般读数超过8位的都是电表编号,相对⽽⾔,电表读数位数还是⽐较少的。当然,也是刚好,这个场景可以区分开来。
如果是其它场景,也需要做OCR到Detection的打标,那么可能就要看OCR识别的具体内容。
3.1.4 关键代码
在下⾯这段代码中,我们就成功整理出了需要构建VOC数据集的基础元素。
import json
import cv2
with open('./','r',encoding='utf8')as fp:
s =[i[:-1].split('\t')for i adlines()]
for i in enumerate(s):
# 四点标注的第⼀个字符串,表⽰⽂件相对路径
path = i[1][0]
# 解析标注内容,需要import json
anno = json.loads(i[1][1])
# 通过规则筛选出⽂件名
filename = i[1][0][6:-4]
# 读取图⽚
img = cv2.imread(path)
钢琴的指法
# 读取图⽚的⾼、宽,因为构造VOC的格式需要
height, weight = img.shape[:-1]
# 有的电表有表号,有的没有,需要逐⼀遍历
for j in range(len(anno)):
# 识别结果超过8位的,被判定为是电表编号
if len(anno[j-1]['transcription'])>8:
label ='No.'
# 其它标注为读数
el:
label ='indicator'
# xmin, xmax, ymin, ymax的计算逻辑
x1 =min(int(anno[j-1]['points'][0][0]),int(anno[j-1]['points'][1][0]),int(anno[j-1]['points'][2][0]),int(anno[j-1]['points'][3][0]))
x2 =max(int(anno[j-1]['points'][0][0]),int(anno[j-1]['points'][1][0]),int(anno[j-1]['points'][2][0]),int(anno[j-1]['points'][3][0]))
y1 =min(int(anno[j-1]['points'][0][1]),int(anno[j-1]['points'][1][1]),int(anno[j-1]['points'][2][1]),int(anno[j-1]['points'][3][1]))
y2 =max(int(anno[j-1]['points'][0][1]),int(anno[j-1]['points'][1][1]),int(anno[j-1]['points'][2][1]),int(anno[j-1]['points'][3][1])) # 打印出结果印证
print(path, filename, label, x1, x2, y1, y2)
M2021/IMG_20210712_101215.jpg IMG_20210712_101215 No. 1038 1636 2554 2702
M2021/IMG_20210712_101215.jpg IMG_20210712_101215 indicator 1043 1769 1453 1612
M2021/IMG_20210712_101222.jpg IMG_20210712_101222 No. 1152 1801 2543 2651
M2021/IMG_20210712_101222.jpg IMG_20210712_101222 indicator 1146 1898 1394 1558
3.1.5 ⽣成数据集
清热润燥import os
from collections import defaultdict
import cv2
# import misc_utils as utils # pip3 install utils-misc==0.0.5 -i /simple/
import json
早晨起来口苦
os.makedirs('./Annotations', exist_ok=True)
print('建⽴Annotations⽬录',3)
# os.makedirs('./PaddleOCR/train_data/ImageSets/Main', exist_ok=True)
# print('建⽴ImageSets/Main⽬录', 3)
mem = defaultdict(list)
with open('./','r',encoding='utf8')as fp:
s =[i[:-1].split('\t')for i adlines()]
for i in enumerate(s):
path = i[1][0]
print(path)
anno = json.loads(i[1][1])
filename = i[1][0][6:-4]
img = cv2.imread(path)
height, width = img.shape[:-1]
for j in range(len(anno)):
if len(anno[j-1]['transcription'])>8:
label ='No.'
el:
label ='indicator'
x1 =min(int(anno[j-1]['points'][0][0]),int(anno[j-1]['points'][1][0]),int(anno[j-1]['points'][2][0]),int(anno[j-1]['points'][3][0])) x2 =max(int(anno[j-1]['points'][0][0]),int(anno[j-1]['points'][1][0]),int(anno[j-1]['points'][2][0]),int(anno[j-1]['points'][3][0])) y1 =min(int(anno[j-1]['points'][0][1]),int(anno[j-1]['points'][1][1]),int(anno[j-1]['points'][2][1]),int(anno[j-1]['points'][3][1])) y2 =max(int(anno[j-1]['points'][0][1]),int(anno[j-1]['points'][1][1]),int(anno[j-1]['points'][2][1]),int(anno[j-1]['points'][3][1])) mem[filename].append([label, x1, y1, x2, y2])
# for i, filename in enumerate(mem):
# img = cv2.imread(os.path.join('train', filename))
# height, width, _ = img.shape
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>word格式排版
<depth>3</depth>
</size>
<gmented>0</gmented>\n""")
for label, x1, y1, x2, y2 in mem[filename]:
f.write(f""" <object>
<name>{label}</name>
<po>Unspecified</po>
<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>")
这样,就轻松⽣成了Annotations⽬录,可以核实下转换效果。
>八宝粥