最近在做目标检测,用yolov5。手上有一批数据集,包含图像和xml及txt格式的标签。现在将这些图片用处理工具做了一键旋转、水平翻转和垂直翻转。由于数据量还是蛮大的,像我这种懒人是不可能愿意重新标数据集的,肯定会想着去搞个一键生成的小玩意儿。毕竟俗话说得好,自己动手丰衣足食。说干就干,一段Python代码批量将文件夹下所有标签进行转换。
在xml和txt标签里,我选择了xml。两个原因,一个是xml的标签代表的含义更容易理解,另一个就是我手上有xml转txt的工具却没有txt转xml的。所以做了xml就相当于全有了,岂不美哉?
转换的思路挺简单的,首先观察一下xml里的重要信息。重点就两个地方,第一个是
<size> <width>720</width> <height>1280</height> <depth>3</depth> </size>
这里定义了图片的长和宽,这两个数据必然是用的到的。
第二个就是标注的长方形了:
<bndbox> <xmin>0</xmin> <ymin>21</ymin> <xmax>287</xmax> <ymax>565</ymax> </bndbox>
有了图片的长和宽,有了每个数据的位置。通过数学知识我们就很容易计算出新的标签位置。比如以下三种:
- 180°旋转:就是x方向和y方向都要变化,并且满足长度减去最大值就是新得到的最小值,减去最小值就是新得到的最大值
- 沿x轴翻转:沿x轴进行翻转就是x坐标不变,只有y坐标变化,变化规则与180°是相同的
- 沿y轴翻转:沿y轴进行翻转与沿x轴翻转恰好相反,y坐标不变而x轴变化,变化规则也与180°相同
按照上面的思路,我们可以很简单的用代码来描述
new_xmax = width-int(xmin))
new_xmin = width-int(xmax))
new_ymax = height-int(ymin)
new_ymin = height-int(ymax)
核心思路都有了,接下来就简单了。我接下来的操作也是十分暴力,遍历所有xml文件,读入到字符串中,正则提取出所有的xmin、ymin、xmax、ymax,然后逐个遍历,将原文本用新计算出来的结果替换掉。大致思路如下:
xmins = re.findall(r"<xmin>(.+?)</xmin>", content)
xmaxs = re.findall(r"<xmax>(.+?)</xmax>", content)
ymins = re.findall(r"<ymin>(.+?)</ymin>", content)
ymaxs = re.findall(r"<ymax>(.+?)</ymax>", content)
for i, xmin in enumerate(xmins):
content = content.replace("<xmax>"+xmaxs[i]+"</xmax>", "<xmax>"+str(width-int(xmin))+"</xmax>")
for i, xmax in enumerate(xmaxs):
content = content.replace("<xmin>"+xmins[i]+"</xmin>", "<xmin>"+str(width-int(xmax))+"</xmin>")
for i, ymin in enumerate(ymins):
content = content.replace("<ymax>"+ymaxs[i]+"</ymax>", "<ymax>"+str(height-int(ymin))+"</ymax>")
for i, ymax in enumerate(ymaxs):
content = content.replace("<ymin>"+ymins[i]+"</ymin>", "<ymin>"+str(height-int(ymax))+"</ymin>")
写好的代码运行一遍,结果就有了。停停停,千万别用上面这段代码。为什么?因为如果是那样的话,我就不会写这么一篇了。我当时用这种简单粗暴的手段处理完以后,完全没有意识到事情的严重性,直接把结果扔到yolo里跑去了。结果yolo给了我一堆warings,警告我标签中读入了负数值,不合法。我这一下可是蒙了,负数值?只好回来检查。
根据yolo中给出警告的文件,我打开了相应的xml,一看傻眼了,好家伙,我居然有xmin比xmax还大的情况。这是什么回事?我首先仔细理了一遍思路,觉得我的计算逻辑没有问题,那问题就一定出在replace里了。我认真的debug,一步一步看结果,思考,最后发现了如下两个问题。
- 当原文中恰好出现了多个xmin或xmax等的值恰好相同时,我的replace函数就会把所有的都替换掉
- 被我replace后的结果,也有可能与后序要查找的值刚好相同,这时前面算出来的值就又被替换走了。
果真是replace惹的祸啊,为了解决这两个问题,我用了如下的解决措施:
- 每次replace,只replace匹配到的第一个值
- 每次replace的结果中需要加入一段破坏字符,保证后续不会被匹配到
这样就可以按照顺序正确的生成新标签了,核心代码描述如下:
for i, xmin in enumerate(xmins):
content = content.replace("<xmax>"+xmaxs[i]+"</xmax>", "<xmax>"+str(width-int(xmin))+"calculating</xmax>", 1)
for i, xmax in enumerate(xmaxs):
content = content.replace("<xmin>"+xmins[i]+"</xmin>", "<xmin>"+str(width-int(xmax))+"calculating</xmin>", 1)
for i, ymin in enumerate(ymins):
content = content.replace("<ymax>"+ymaxs[i]+"</ymax>", "<ymax>"+str(height-int(ymin))+"calculating</ymax>", 1)
for i, ymax in enumerate(ymaxs):
content = content.replace("<ymin>"+ymins[i]+"</ymin>", "<ymin>"+str(height-int(ymax))+"calculating</ymin>", 1)
这件事告诉了我任何时候都不能轻敌啊,干任何事情都要细心、小心、耐心。思维要缜密,按照自己的想法不加验证的走,起飞了,往往也就要跌了。
下面贴出完整代码,需要自取。
rotate180.py
import os, re
def convert(inpath, outpath):
if not os.path.exists(outpath):
os.makedirs(outpath)
for file in os.listdir(inpath):
if file.endswith('.xml'):
new_file_name = file.replace('.xml', '-whirl.xml')
with open(inpath+'/'+file) as f:
content = f.read()
file_name = ''.join(re.findall(r"<filename>(.*?)</filename>", content)[0].split('.')[:-1])
width = int(re.findall(r"<width>(.+?)</width>", content)[0])
height = int(re.findall(r"<height>(.+?)</height>", content)[0])
xmins = re.findall(r"<xmin>(.+?)</xmin>", content)
xmaxs = re.findall(r"<xmax>(.+?)</xmax>", content)
ymins = re.findall(r"<ymin>(.+?)</ymin>", content)
ymaxs = re.findall(r"<ymax>(.+?)</ymax>", content)
for i, xmin in enumerate(xmins):
content = content.replace("<xmax>"+xmaxs[i]+"</xmax>", "<xmax>"+str(width-int(xmin))+"calculating</xmax>", 1)
for i, xmax in enumerate(xmaxs):
content = content.replace("<xmin>"+xmins[i]+"</xmin>", "<xmin>"+str(width-int(xmax))+"calculating</xmin>", 1)
for i, ymin in enumerate(ymins):
content = content.replace("<ymax>"+ymaxs[i]+"</ymax>", "<ymax>"+str(height-int(ymin))+"calculating</ymax>", 1)
for i, ymax in enumerate(ymaxs):
content = content.replace("<ymin>"+ymins[i]+"</ymin>", "<ymin>"+str(height-int(ymax))+"calculating</ymin>", 1)
content = content.replace('calculating', '')
content = content.replace(file_name, file_name+'-whirl')
with open(outpath+'/'+new_file_name, 'w') as f:
f.write(content)
print(f"converted {file} -> {new_file_name}")
if __name__ == '__main__':
convert(r"your_xml_label_path", r"output_xml_path")
xfliper.py
import os, re
def convert(inpath, outpath):
if not os.path.exists(outpath):
os.makedirs(outpath)
for file in os.listdir(inpath):
if file.endswith('.xml'):
new_file_name = file.replace('.xml', '-xflip.xml')
with open(inpath+'/'+file) as f:
content = f.read()
file_name = ''.join(re.findall(r"<filename>(.*?)</filename>", content)[0].split('.')[:-1])
height = int(re.findall(r"<height>(.+?)</height>", content)[0])
ymins = re.findall(r"<ymin>(.+?)</ymin>", content)
ymaxs = re.findall(r"<ymax>(.+?)</ymax>", content)
for i, ymin in enumerate(ymins):
content = content.replace("<ymax>"+ymaxs[i]+"</ymax>", "<ymax>"+str(height-int(ymin))+"calculating</ymax>")
for i, ymax in enumerate(ymaxs):
content = content.replace("<ymin>"+ymins[i]+"</ymin>", "<ymin>"+str(height-int(ymax))+"calculating</ymin>")
content = content.replace('calculating', '')
content = content.replace(file_name, file_name+'-xflip')
with open(outpath+'/'+new_file_name, 'w') as f:
f.write(content)
print(f"converted {file} -> {new_file_name}")
if __name__ == '__main__':
convert(r"your_xml_label_path", r"output_xml_path")
yfliper.py
import os, re
def convert(inpath, outpath):
if not os.path.exists(outpath):
os.makedirs(outpath)
for file in os.listdir(inpath):
if file.endswith('.xml'):
new_file_name = file.replace('.xml', '-yflip.xml')
with open(inpath+'/'+file) as f:
content = f.read()
file_name = ''.join(re.findall(r"<filename>(.*?)</filename>", content)[0].split('.')[:-1])
width = int(re.findall(r"<width>(.+?)</width>", content)[0])
xmins = re.findall(r"<xmin>(.+?)</xmin>", content)
xmaxs = re.findall(r"<xmax>(.+?)</xmax>", content)
for i, xmin in enumerate(xmins):
content = content.replace("<xmax>"+xmaxs[i]+"</xmax>", "<xmax>"+str(width-int(xmin))+"calculating</xmax>")
for i, xmax in enumerate(xmaxs):
content = content.replace("<xmin>"+xmins[i]+"</xmin>", "<xmin>"+str(width-int(xmax))+"calculating</xmin>")
content = content.replace('calculating', '')
content = content.replace(file_name, file_name+'-yflip')
with open(outpath+'/'+new_file_name, 'w') as f:
f.write(content)
print(f"converted {file} -> {new_file_name}")
if __name__ == '__main__':
convert(r"your_xml_label_path", r"output_xml_path")