赞
踩
本项目来源于一个PaddleOCR垂类场景,该场景对检测模型准确率需求较高,由于担心PaddleOCR的检测器模型效果可能不能满足需求,因此希望尝试通过PaddleDetection模型库提高对目标框的检测效果。
PP-OCR是一个实用的超轻量OCR系统。主要由DB文本检测、检测框矫正和CRNN文本识别三部分组成。该系统从骨干网络选择和调整、预测头部的设计、数据增强、学习率变换策略、正则化参数选择、预训练模型使用以及模型自动裁剪量化8个方面,采用19个有效策略,对各个模块的模型进行效果调优和瘦身(如绿框所示),最终得到整体大小为3.5M的超轻量中英文OCR和2.8M的英文数字OCR。
因此,我们知道PP-OCR其实是三个模型的串接,它们分别是:
本项目面临的是一个电表读数和编号识别场景,在该垂类场景中,由于并不是需要全部的文本,同时场景情况又比较复杂,比如存在反光、甚至电表读数也不是全部需要识别的(只需识别整数位)。
使用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": false}, {"transcription": "2002-00053452", "points": [[1152.0, 2543.0], [1801.0, 2543.0], [1801.0, 2651.0], [1152.0, 2651.0]], "difficult": false}]
所以,用的是四点标注模式,显然,是一个不规则的四边形。
但是我们知道,如果不使用PP-OCR的检测器,而希望用PaddleDetection模型库的话,需要的是标准的矩形框(无论是否旋转),因此本文主要解决从四点标注到矩形标注转换的问题,希望能减少标注工作量,避免标两次。
使用PaddleDetection替代PP-OCR的检测器还有另一个好处,因为在该场景需要区分表号和电表读数,用检测模型自然会带出文字框的类别,这样就不需要对PP-OCR输出的结果再进行后处理了。
首先还是先来看看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> <ridinghorse>0</ridinghorse> <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> <pose>Unspecified</pose> <point> <x>155</x> <y>119</y> </point> </object> <segmented>0</segmented> <size> <depth>3</depth> <height>375</height> <width>500</width> </size> <source> <annotation>PASCAL VOC2012</annotation> <database>The VOC2012 Database</database> <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>
这步的转换还是比较简单的,对于VOC数据集,需要找到xmin, ymin, xmax, ymax
,而四点标注格式为(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)
,其实只需要稍加计算就能解决边框点的位置。
从OCR到Detection,还有一个需要解决的就是标签问题,我们需要把电表读数和表号区分开来,分别打标。在该场景中,主要是通过规则进行判定,原因很简单,一般读数超过8位的都是电表编号,相对而言,电表读数位数还是比较少的。当然,也是刚好,这个场景可以区分开来。
如果是其它场景,也需要做OCR到Detection的打标,那么可能就要看OCR识别的具体内容。
在下面这段代码中,我们就成功整理出了需要构建VOC数据集的基础元素。
import json import cv2 with open('./M2021/Labels.txt','r',encoding='utf8')as fp: s = [i[:-1].split('\t') for i in fp.readlines()] 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.' # 其它标注为读数 else: 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
import os from collections import defaultdict import cv2 # import misc_utils as utils # pip3 install utils-misc==0.0.5 -i https://pypi.douban.com/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('./M2021/Labels.txt','r',encoding='utf8')as fp: s = [i[:-1].split('\t') for i in fp.readlines()] 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.' else: 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> <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>")
这样,就轻松生成了Annotations目录,可以核实下转换效果。
<annotation> <folder>JPEGImages</folder> <filename>IMG_20210712_101215.jpg</filename> <size> <width>3024</width> <height>4032</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>No.</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>1038</xmin> <ymin>2554</ymin> <xmax>1636</xmax> <ymax>2702</ymax> </bndbox> </object> <object> <name>indicator</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>1043</xmin> <ymin>1453</ymin> <xmax>1769</xmax> <ymax>1612</ymax> </bndbox> </object> </annotation>
roLabelImg是基于labelImg改进的,也是用来标注为VOC格式的数据,但是在labelImg的基础上增加了能够使标注的框进行旋转的功能。
<annotation verified="yes"> <folder>hsrc</folder> <filename>100000001</filename> <path>/Users/haoyou/Library/Mobile Documents/com~apple~CloudDocs/OneDrive/hsrc/100000001.bmp</path> <source> <database>Unknown</database> </source> <size> <width>1166</width> <height>753</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <type>bndbox</type> <name>ship</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>178</xmin> <ymin>246</ymin> <xmax>974</xmax> <ymax>504</ymax> </bndbox> </object> <object> <type>robndbox</type> <name>ship</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <robndbox> <cx>580.7887</cx> <cy>343.2913</cy> <w>775.0449</w> <h>170.2159</h> <angle>2.889813</angle> </robndbox> </object> </annotation>
可以看出,其实唯一的差异就在于这里:
<object>
<type>robndbox</type>
<name>ship</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<robndbox>
<cx>580.7887</cx>
<cy>343.2913</cy>
<w>775.0449</w>
<h>170.2159</h>
<angle>2.889813</angle>
</robndbox>
</object>
cx, cy表示bndbox的中心点坐标(坐标系方向和一般的图像坐标系相同,左上角为原点,向右为x正方向,向下为y正方向)
h和w是标记目标的高和宽
angle是旋转角度信息,水平bndbox,angle=0,,顺时针方向旋转,得到的角度值是一个弧度单位的正值,且旋转一周为pi,没有负值
画一个**任意四边形(任意多边形都可以)**的最小外接矩形,显然,这个最小外接矩形就是我们希望找到的旋转矩形框。
函数 cv2.minAreaRect() 返回一个Box2D结构rect:(最小外接矩形的中心(x,y),(宽度,高度),旋转角度),但是要绘制这个矩形,我们需要矩形的4个顶点坐标box, 通过函数 cv2.cv.BoxPoints() 获得,返回形式[ [x0,y0], [x1,y1], [x2,y2], [x3,y3] ]。得到的最小外接矩形的4个顶点顺序、中心坐标、宽度、高度、旋转角度(是度数形式,不是弧度数)。
不过,需要注意的是,不同版本的OpenCV,在cv2.minAreaRect()函数上是有显著变化的,差异就在于最后的旋转角度如何计算,请参见关于不同版本opencv的cv2.minAreaRect函数输出角度范围不同的问题有非常详尽的分析。
由于在AIStudio上,是无法切换OpenCV版本的,因此,我们这里采用的还是OpenCV4.1.1当中的cv2.minAreaRect()函数,它的特点如下:
因此,我们可以发现,通过使用cv2.minAreaRect()函数,roLabelImg格式需要的cx,cy,w,h
直接就可以得到,唯一麻烦的是angle
怎么换算。
接下来,我们先把绘制外接矩形的操作画出来,看看效果。
import math
import numpy as np
import os
from collections import defaultdict
import cv2
import matplotlib.pyplot as plt
import json
%matplotlib inline
def order_points(pts): # sort the points based on their x-coordinates # 将输入的四个顶点进行排序 xSorted = pts[np.argsort(pts[:, 0]), :] # grab the left-most and right-most points from the sorted # x-roodinate points leftMost = xSorted[:2, :] rightMost = xSorted[2:, :] if leftMost[0,1]!=leftMost[1,1]: leftMost=leftMost[np.argsort(leftMost[:,1]),:] else: leftMost=leftMost[np.argsort(leftMost[:,0])[::-1],:] (tl, bl) = leftMost if rightMost[0,1]!=rightMost[1,1]: rightMost=rightMost[np.argsort(rightMost[:,1]),:] else: rightMost=rightMost[np.argsort(rightMost[:,0])[::-1],:] (tr,br)=rightMost # return the coordinates in top-left, top-right, # bottom-right, and bottom-left order # 返回的结果是,左上,右上,右下,左下的顺时针顶点序列 return np.array([tl, tr, br, bl], dtype="float32") # 输入一个四点标注框的坐标,来源于文件 M2021/16号中继站护路生活区.jpg pts = np.array([[1035.8125000000002, 2260.5], [1038.8125000000002, 2400.5], [1760.8125000000002, 2444.5], [1760.8125000000002, 2310.5]]) # 顶点排序 clock_points = order_points(pts) # 获得最小外接矩形 rect = cv2.minAreaRect(clock_points) # 打印下(cx,cy),(w,h),angle print(rect) # 将((cx,cy),(w,h),angle)格式表示的多边形数据转成点集表示 rect_pts = cv2.boxPoints(rect).astype(np.int32) img = np.ones((3456,4608,3)).astype(np.uint8) * 255 cv2.polylines(img,np.int32([clock_points]),True,(255,0,0),2) cv2.polylines(img,np.int32([rect_pts]),True,(0,0,255),1) (img,np.int32([rect_pts]),True,(0,0,255),1) plt.imsave('test.jpg',img)
((1398.3125, 2352.5), (139.55825805664062, 734.8499755859375), -86.51260375976562)
最小外接矩形绘制效果如下:
关于旋转角度的计算,这里参考了coordinate_convert.py,并根据实际测试结果进行了调整。主要还是因为OpenCV的版本问题,最后生成的数据集转换效果如下:
四点标注
roLabelImg转换效果
格式转换是否成功,可以以roLabelImg中查看的实际效果为依据。
def coordinate_present_convert(coords, shift=True): """ :param coords: shape [-1, 5] :param shift: [-90, 90) --> [-180, 0) :return: shape [-1, 5] """ # angle range from [-90, 0) to [0,180) w, h = coords[:, 2], coords[:, 3] remain_mask = np.greater(w, h) convert_mask = np.logical_not(remain_mask).astype(np.int32) remain_mask = remain_mask.astype(np.int32) remain_coords = coords * np.reshape(remain_mask, [-1, 1]) coords[:, [2, 3]] = coords[:, [3, 2]] coords[:, 4] += 90 convert_coords = coords * np.reshape(convert_mask, [-1, 1]) coords_new = remain_coords + convert_coords if shift: if coords_new[:, 4] >= 0: coords_new[:, 4] = 180 + coords_new[:, 4] return np.array(coords_new, dtype=np.float32) def backward_convert(coordinate): """ :param coordinate: format [x1, y1, x2, y2, x3, y3, x4, y4] :return: format [x_c, y_c, w, h, theta, (label)] """ boxes = [] box = np.int0(coordinate) box = box.reshape([4, 2]) rect1 = cv2.minAreaRect(box) x, y, w, h, theta = rect1[0][0], rect1[0][1], rect1[1][0], rect1[1][1], rect1[2] if theta == 0: w, h = h, w theta -= 90 boxes.append([x, y, w, h, theta]) return np.array(boxes, dtype=np.float32) os.makedirs('./roAnnotations', exist_ok=True) print('建立roAnnotations目录', 3) mem = defaultdict(list) with open('./M2021/Labels.txt','r',encoding='utf8')as fp: s = [i[:-1].split('\t') for i in fp.readlines()] 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.' else: 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])) # 用OpenCV的最小矩形转换成-90到0的角度 boxes = backward_convert([anno[j-1]['points'][0][0],anno[j-1]['points'][0][1],int(anno[j-1]['points'][1][0]),int(anno[j-1]['points'][1][1]), int(anno[j-1]['points'][2][0]),int(anno[j-1]['points'][2][1]),int(anno[j-1]['points'][3][0]),int(anno[j-1]['points'][3][1])]) # 根据长短边转成 0 到 180 new_boxes = coordinate_present_convert(boxes) # 转成弧度: new_boxes[0][-1] = new_boxes[0][-1] * math.pi/180 new_boxes = new_boxes.astype(np.float32) cx,cy,w,h,angle = new_boxes[0] mem[filename].append([label, x1, y1, x2, y2, cx, cy, w, h, angle]) with open(os.path.join('./roAnnotations', 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, cx, cy, w, h, angle in mem[filename]: f.write(f"""<object> <type>bndbox</type> <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><object> <type>robndbox</type> <name>{label}</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <robndbox> <cx>{cx}</cx> <cy>{cy}</cy> <w>{w}</w> <h>{h}</h> <angle>{angle}</angle> </robndbox> </object>\n""") f.write("</annotation>")
建立roAnnotations目录 3
M2021/IMG_20210712_101215.jpg
M2021/IMG_20210712_101222.jpg
M2021/IMG_20210712_095237.jpg
M2021/IMG_20210724_161929.jpg
M2021/16号中继站护路生活区.jpg
本文实现了PP-OCR四点标注结果到VOC和roLabelImg数据格式的批量转换,下一步,将根据转换结果,训练基于PaddleDetection的电表读数和编号数字框检测模型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。