最近把之前项目整理了下,YOLO-obb+PyQt实现指针式仪表的读数识别,可完成单张和批量识别读数校正,并根据序号对照表保存图片名称、检测时间、表计、最终读数等数据,其中标签对应表计,如"叁"对应第1室103号表,模型共训练了共52种标签对应不同的表计,精度尚可,可批量读数保存到excel中,绘制单张表计和所有表计检测读数趋势,效果如下。
在YOLO-obb+PyQt实现指针式仪表读数识别(一)中主要介绍YOLO-obb模型标注训练流程,在YOLO-obb+PyQt实现指针式仪表读数识别(二)中介绍读数原理和校正原理。
一、检测效果
1、指针式仪表识别效果
2、指针式仪表检测视频
YOLO-obb+PyQt实现指针式仪表读数识别
二、数据存储
1、序号对照表
2、表计
3、数据保存-单张
4、数据保存-批量
5、数据绘图-单张
6、数据绘图-批量
三、YOLO-obb模型标注训练流程
1、整体流程
初始图片经模型1检测得到表盘带标签图片,再分别经模型2和模型3得到表盘图片和标签图片,表盘图片和标签图片经obb模型4得到表计和最终读数。模型1、模型2、模型3的训练没什么好说的,LabelImg或者makesense正常标注训练就好。主要是obb模型4的标注训练流程。
2、YOLO-obb标注流程
2.1 标注软件
使用roLabelImg,在github.com/cgvict/roLabelImg下载解压,在cmd中输入python roLabelImg.py即可进入roLabelImg软件界面。
软件快捷键如下:
1) w: 创建水平矩形目标框;
2) e: 创建旋转矩形目标框;
3) zxcv: 旋转目标框,z和x逆时针旋转,c和v顺时针旋转,zv快速旋转,xc慢速旋转。
2.2 图片标注
在这里标注开始刻度线和结束刻度线,对应Scale,0.4刻度线对应Scale2(用于读数校正,具体校正方法后面再提),指针对应Pointer,图片标注完成后保存为xml文件,需进行文件格式转换。
2.3 格式转换xml-txt
2.3.1 xml转成dota格式txt
将标注完成后的xml转成yolo可训练的txt文件,第一步转换代码,将xml转成dota格式txt:
# 文件名称 :roxml_to_dota.py
# 功能描述 :把rolabelimg标注的xml文件转换成dota能识别的xml文件,
# 再转换成dota格式的txt文件
# 把旋转框 cx,cy,w,h,angle,或者矩形框cx,cy,w,h,转换成四点坐标x1,y1,x2,y2,x3,y3,x4,y4
import os
import xml.etree.ElementTree as ET
import math
# 修改为自己的标签
cls_list = ['Scale', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10',
'11', '12', '13', '14', '15', '16', '17', '18', '19', '20',
'21', '22', '23', '24', '25', '26', '27', '28', '29', '30',
'31', '32', '33', '34', '35', '36', '37', '38', '39', '40',
'41', '42', '43', '44', '45', '46', '47', '48', '49', '50',
'51', '52', 'Scale2', 'Pointer']
def edit_xml(xml_file, dotaxml_file):
"""
修改xml文件
:param xml_file:xml文件的路径
:return:
"""
# dxml_file = open(xml_file,encoding='gbk')
# tree = ET.parse(dxml_file).getroot()
tree = ET.parse(xml_file)
objs = tree.findall('object')
for ix, obj in enumerate(objs):
x0 = ET.Element("x0") # 创建节点
y0 = ET.Element("y0")
x1 = ET.Element("x1")
y1 = ET.Element("y1")
x2 = ET.Element("x2")
y2 = ET.Element("y2")
x3 = ET.Element("x3")
y3 = ET.Element("y3")
# obj_type = obj.find('bndbox')
# type = obj_type.text
# print(xml_file)
if (obj.find('robndbox') == None):
obj_bnd = obj.find('bndbox')
obj_xmin = obj_bnd.find('xmin')
obj_ymin = obj_bnd.find('ymin')
obj_xmax = obj_bnd.find('xmax')
obj_ymax = obj_bnd.find('ymax')
# 以防有负值坐标
xmin = max(float(obj_xmin.text), 0)
ymin = max(float(obj_ymin.text), 0)
xmax = max(float(obj_xmax.text), 0)
ymax = max(float(obj_ymax.text), 0)
obj_bnd.remove(obj_xmin) # 删除节点
obj_bnd.remove(obj_ymin)
obj_bnd.remove(obj_xmax)
obj_bnd.remove(obj_ymax)
x0.text = str(xmin)
y0.text = str(ymax)
x1.text = str(xmax)
y1.text = str(ymax)
x2.text = str(xmax)
y2.text = str(ymin)
x3.text = str(xmin)
y3.text = str(ymin)
else:
obj_bnd = obj.find('robndbox')
obj_bnd.tag = 'bndbox' # 修改节点名
obj_cx = obj_bnd.find('cx')
obj_cy = obj_bnd.find('cy')
obj_w = obj_bnd.find('w')
obj_h = obj_bnd.find('h')
obj_angle = obj_bnd.find('angle')
cx = float(obj_cx.text)
cy = float(obj_cy.text)
w = float(obj_w.text)
h = float(obj_h.text)
angle = float(obj_angle.text)
obj_bnd.remove(obj_cx) # 删除节点
obj_bnd.remove(obj_cy)
obj_bnd.remove(obj_w)
obj_bnd.remove(obj_h)
obj_bnd.remove(obj_angle)
x0.text, y0.text = rotatePoint(cx, cy, cx - w / 2, cy - h / 2, -angle)
x1.text, y1.text = rotatePoint(cx, cy, cx + w / 2, cy - h / 2, -angle)
x2.text, y2.text = rotatePoint(cx, cy, cx + w / 2, cy + h / 2, -angle)
x3.text, y3.text = rotatePoint(cx, cy, cx - w / 2, cy + h / 2, -angle)
# obj.remove(obj_type) # 删除节点
obj_bnd.append(x0) # 新增节点
obj_bnd.append(y0)
obj_bnd.append(x1)
obj_bnd.append(y1)
obj_bnd.append(x2)
obj_bnd.append(y2)
obj_bnd.append(x3)
obj_bnd.append(y3)
tree.write(dotaxml_file, method='xml', encoding='utf-8') # 更新xml文件
# 转换成四点坐标
def rotatePoint(xc, yc, xp, yp, theta):
xoff = xp - xc
yoff = yp - yc
cosTheta = math.cos(theta)
sinTheta = math.sin(theta)
pResx = cosTheta * xoff + sinTheta * yoff
pResy = - sinTheta * xoff + cosTheta * yoff
return str(xc + pResx), str(yc + pResy)
def totxt(xml_path, out_path):
# 想要生成的txt文件保存的路径,这里可以自己修改
files = os.listdir(xml_path)
i = 0
for file in files:
tree = ET.parse(xml_path + os.sep + file)
root = tree.getroot()
name = file.split('.')[0]
output = out_path + '\\' + name + '.txt'
file = open(output, 'w')
i = i + 1
objs = tree.findall('object')
for obj in objs:
cls = obj.find('name').text
box = obj.find('bndbox')
x0 = round(float(box.find('x0').text), 4)
y0 = round(float(box.find('y0').text), 4)
x1 = round(float(box.find('x1').text), 4)
y1 = round(float(box.find('y1').text), 4)
x2 = round(float(box.find('x2').text), 4)
y2 = round(float(box.find('y2').text), 4)
x3 = round(float(box.find('x3').text), 4)
y3 = round(float(box.find('y3').text), 4)
if x0 < 0:
x0 = 0
if x1 < 0:
x1 = 0
if x2 < 0:
x2 = 0
if x3 < 0:
x3 = 0
if y0 < 0:
y0 = 0
if y1 < 0:
y1 = 0
if y2 < 0:
y2 = 0
if y3 < 0:
y3 = 0
for cls_index, cls_name in enumerate(cls_list):
if cls == cls_name:
file.write("{} {} {} {} {} {} {} {} {} {}\n".format(x0, y0, x1, y1, x2, y2, x3, y3, cls, cls_index))
file.close()
# print(output)
print(i)
if __name__ == '__main__':
# 输入:标注的xml文件 输出:dota文件和txt文件
# -----**** 第一步:把xml文件统一转换成旋转框的xml文件 ****-----
roxml_path = r'xml_path'
dotaxml_path = r'data_path'
out_path = r'txt_path'
filelist = os.listdir(roxml_path)
for file in filelist:
edit_xml(os.path.join(roxml_path, file), os.path.join(dotaxml_path, file))
# -----**** 第二步:把旋转框xml文件转换成txt格式 ****-----
totxt(dotaxml_path, out_path)
转换完成后数据格式如下:
2.3.2 dota格式txt转成yolo可训练txt
第二步转换代码,将dota格式txt转成yolo可训练txt:
from ultralytics.data.converter import convert_dota_to_yolo_obb
# 注意your_data_path文件夹格式
convert_dota_to_yolo_obb('your_data_path')
这里有两个需要注意的地方,ctrl+鼠标左键,修改convert_dota_to_yolo_obb里class_mapping:
# Class names to indices mapping
class_mapping = {
"Scale": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"11": 11,
"12": 12,
"13": 13,
"14": 14,
"15": 15,
"16": 16,
"17": 17,
"18": 18,
"19": 19,
"20": 20,
"21": 21,
"22": 22,
"23": 23,
"24": 24,
"25": 25,
"26": 26,
"27": 27,
"28": 28,
"29": 29,
"30": 30,
"31": 31,
"32": 32,
"33": 33,
"34": 34,
"35": 35,
"36": 36,
"37": 37,
"38": 38,
"39": 39,
"40": 40,
"41": 41,
"42": 42,
"43": 43,
"44": 44,
"45": 45,
"46": 46,
"47": 47,
"48": 48,
"49": 49,
"50": 50,
"51": 51,
"52": 52,
"Scale2": 53,
"Pointer": 54,
}
此外,your_data_path文件夹格式要满足如下要求:
images/train和images/val放置原始图片文件,labels/train_original和labels/val_original放置原始dota_txt标签文件,labels/train和labels/val为空,运行代码,结束转换后的标签会保存在labels/train和labels/val中,转换后格式如下。至此,训练数据集准备完毕。
3、YOLO-obb训练流程
3.1 数据集文件夹
新建数据集文件夹,结构如下所示,test可为空,labels即为上一步骤转换完成的txt文件。
3.2 创建dota-obb.yaml
创建yaml文件,更改数据集路径。
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: your_data_path # dataset root dir
train: images/train # train images (relative to 'path') 128 images
val: images/val # val images (relative to 'path') 128 images
test: # test images (optional)
# Classes
names:
0: Scale
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
10: 10
11: 11
12: 12
13: 13
14: 14
15: 15
16: 16
17: 17
18: 18
19: 19
20: 20
21: 21
22: 22
23: 23
24: 24
25: 25
26: 26
27: 27
28: 28
29: 29
30: 30
31: 31
32: 32
33: 33
34: 34
35: 35
36: 36
37: 37
38: 38
39: 39
40: 40
41: 41
42: 42
43: 43
44: 44
45: 45
46: 46
47: 47
48: 48
49: 49
50: 50
51: 51
52: 52
53: Scale2
54: Pointer
3.3 创建yolov8-obb.yaml
创建yolov8-obb.yaml,修改nc类别数量即可。
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8 Oriented Bounding Boxes (OBB) model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
# Parameters
nc: 55 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n'
# [depth, width, max_channels]
n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs
s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs
m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs
l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs
x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, OBB, [nc, 1]] # OBB(P3, P4, P5)
3.4 创建train文件
创建train文件,可以开始训练。
from ultralytics import YOLO
model = YOLO('yolov8_obb.yaml').load('yolov8n-obb.pt')
model.train(data='data_obb.yaml', epochs=200, batch=16, workers=0)