坚持写博客💪,分享自己的在学习、工作中的所得
- 给自己做备忘
- 对知识点记录、总结,加深理解
- 给有需要的人一些帮助,少踩一个坑,多走几步路
尽量以合适的方式排版,图文兼有
如果写的有误,或者有不理解的,均可在评论区留言
如果内容对你有帮助,欢迎点赞 👍 收藏 ⭐留言 📝。
虽然平台并不会有任何奖励,但是我会很开心,可以让我保持写博客的热情🙏🙏🙏
注:本篇代码实现基于YOLO格式的标注,其他格式的数据可以参考思路实现。
🧁标注数据的检查和可视化
标注数据是一件十分枯燥的重复性工作。在标注数据时,有时因为数据量大,类别多,会出现错标、误标的情况,这时就需要对标注数据进行检查并且修改。
以labelimg标注目标检测为例,一般都两种方式:
- 先定义好类别文件classes.txt,先将所有需要的类别写入文件,在标注时进行选择即可
- 在标注时遇到新的类别再添加,会保存到标注文件路径的classes.txt
两种方式皆可。但也有可能遇到下面的问题:
第一种方式在标注时可能会遇到某些类别的数量为0的情况;
而第二种可能会出现的问题是:多个人标注时没有统一的类别名,导致每个人标注的文件不统一。
这篇文章倾向于第一种方式,在标注之前先确定好classes.txt
假设数据集有3个类别。classes.txt文件:
class_name1
class_name2
class_name3
标注出错也有两种情况,本文会针对这两种情况做相应的处理:
- 错误类别,即在原有类别的基础上增加了新的类别
- 类别之间出错,即class_name1误标为class_name2
一旦确定好文件,在标注时出现手误,标注错类别名时,比较容易发现,因为新的类别名会添加在classes.txt文件的末尾,比较容易发现和修改,而第二种如果在过程中出现,修改会比较麻烦
🧁错误标注类别
针对上面的第一种错误,在标注完成之后,如果classes.txt的类别比之前定义的多了,那么就说明在标注时出错了,可以使用以下代码,找到错误的类别的索引所对应的文件,再通过标注工具重新标注正确的类别,最后关闭标注工具并将classes.txt后面错误的类别删除即可。
注意:顺序是先找到误标的文件 >> 修改 >> 删除类别名
先删除类别名,在标注数据中或无法打开标注文件而报错
逻辑是:遍历所有标注文件,找到类别索引大于等于类别数的即打印出文件路径,根据路径在标注工具中重新打开对应文件修改即可。索引是从0开始的。
import os
from glob import glob
num_of_classes = 3
all_files = glob(r'path\to\labels\*.txt')
for file in all_files:
if file.endswith('classes.txt'):
continue
with open(file, 'r') as f:
for line in f.readlines():
try:
if int(line.split()[0]) >= num_of_classes: # 一共有num_of_classes 类,从0开始。索引大于等于num_of_classes 的为错误标注的
print(file)
except Exception as e:
print(e)
print(file)
🧁类别间标注出错
针对上面的第二种错误,在标注完成之后,将所有标注目标从原图片中裁剪下来,保存到对应的类别名文件中,这样一个文件在就是同一类目标,打开文件,如果其中有其他类别,则说明标注出错了。同样,根据错误目标的文件名,到标注文件重新标注即可。
import os
import numpy as np
from glob import glob
from PIL import Image, ImageDraw
from tqdm import tqdm
pillow读取图片以及数据格式转换的方法
def read_img_pillow(path): # 40.5 ms
with open(path, "rb") as f:
img = Image.open(f)
return img.convert("RGB") # 20.1 ms
def xywh2xyxy(x):
# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
return y
创建每个类别的文件夹
category = ['class_name1', 'class_name2', 'class_name3']
images_dir = r'path\to\dataset\images'
labels_dir = r'path\to\dataset\labels'
save_dir = r'path\to\dataset\crop'
for cat in category:
os.makedirs(os.path.join(save_dir, cat), exist_ok=False)
逻辑:通过标注文件,找到对应的图片文件,读取标注数据将对应位置的目标保存到对应的类别的文件夹中。
labels_path = glob(os.path.join(labels_dir, '*.txt'))
for label_path in tqdm(labels_path):
image_name = os.path.basename(label_path).replace('.txt', '.jpg')
image_path = os.path.join(images_dir, image_name)
if not os.path.exists(image_path):
continue
im_pil = read_img_pillow(image_path)
width, height = im_pil.size
with open(label_path, 'r') as f:
for i, l in enumerate(f.readlines(), start=1):
line = l.split() # ['1' '0.724877' '0.309082' '0.073938' '0.086914']
xywh = np.array(list(map(float, line[1:]))) * np.array([width, height, width, height])
[xyxy] = xywh2xyxy(np.array([xywh]))
# print(xyxy)
new_name = image_name.replace('.jpg', f'-{i}.jpg')
new_path = os.path.join(save_dir, category[int(line[0])], new_name)
im_crop = im_pil.crop(xyxy)
im_crop.save(new_path)
# break
注意:new_name = image_name.replace('.jpg', f'-{i}.jpg')
中使用-i
来给多个目标命名,避免同个图片中同个类别的目标被覆盖,在后续使用该文件名去修改标注数据时,记得删除-i
才能找到对应的原图片。
每个文件夹检查修改完成后,将整个crop
文件夹删除,再次运行这部分代码,可再次检查是否已经全部修正。
如果内容对你有帮助,或者觉得写的不错
🏳️🌈欢迎点赞 👍 收藏 ⭐留言 📝
有问题,请在评论区留言