diff --git a/docs/data/transform/transform_cn.md b/docs/data/transform/transform_cn.md index e5649ca5b0..ba8c7d8e5a 100644 --- a/docs/data/transform/transform_cn.md +++ b/docs/data/transform/transform_cn.md @@ -2,102 +2,111 @@ # LabelMe分割数据标注 -无论是语义分割,全景分割,还是实例分割,我们都需要充足的训练数据。 +本文档简要介绍使用LabelMe软件进行分割数据标注,并将标注数据转换为PaddleSeg和PaddleX支持的格式。 -本文档简要介绍使用LabelMe软件进行分割数据标注,并将标注数据转换为PaddleSeg支持的格式。 ## 1. 安装LabelMe -LabelMe支持在Windows/macOS/Linux三个系统上使用,且三个系统下的标注格式是一样。 +LabelMe支持在Windows/macOS/Linux三个系统上安装。 -LabelMe的安装流程请参见[官方安装指南](https://github.com/wkentaro/labelme)。 +在Python3环境下,执行如下命令,可以快速安装LabelMe。 +``` +pip install labelme +``` + +LabelMe详细的安装和使用流程,可以参照[官方指南](https://github.com/wkentaro/labelme)。 ## 2. 使用LabelMe -### 预览已标注图片 -打开终端输入`labelme`会出现LableMe的交互界面,可以先预览`LabelMe`给出的已标注好的图片。 +### 2.1 启动LabelMe +在电脑终端输入`labelme`,稍等会出现LableMe的交互界面。
-

图1 LableMe交互界面的示意图

+

LableMe交互界面

-点击`OpenDir`打开`/examples/semantic_segmentation/data_annotated`,其中``为克隆下来的`labelme`的路径,打开后显示的是语义分割的真值标注。 - +点击左上角`File`: +* 勾选`Save Automatically`,设置软件自动保存标注json文件,避免需要手动保存 +* 取消勾选`Save With Image Data`,设置标注json文件中不保存data数据
- -

图2 已标注图片的示意图

+ +

LableMe设置

-### 开始标注图片 +### 2.2 预览已标注图片(可选) -(1) 点击`OpenDir`打开待标注图片所在目录,点击`Create Polygons`,沿着目标的边缘画多边形,完成后输入目标的类别。在标注过程中,如果某个点画错了,可以按撤销快捷键可撤销该点。Mac下的撤销快捷键为`command+Z`。 +执行如下命令,clone下载LabelMe的代码。 +``` +git clone https://github.com/wkentaro/labelme.git +``` + +在LabelMe交互界面上点击`OpenDir`,选择`/examples/semantic_segmentation/data_annotated`目录(``为clone下载的`labelme`的路径),打开后可以显示的是语义分割的真值标注。
- -

图3 标注单个目标的示意图

+ +

已标注图片的示意图

-​(2) 右击选择`Edit Polygons`可以整体移动多边形的位置,也可以移动某个点的位置;右击选择`Edit Label`可以修改每个目标的类别。请根据自己的需要执行这一步骤,若不需要修改,可跳过。 +### 2.3 标注图片 +将所有待标注图片保存在一个目录下,点击`OpenDir`打开待标注图片所在目录。 + +点击`Create Polygons`,沿着前景目标的边缘画闭合的多边形,然后输入或者选择目标的类别。
- -

图4 修改标注的示意图

+ +

标注单个目标的示意图

+通常情况下,大家只需要标注前景目标并设置标注类别,其他像素默认作为背景。如果大家需要手动标注背景区域,**类别必须设置为`_background_`**,否则格式转换会有问题。 -(3) 图片中所有目标的标注都完成后,点击`Save`保存json文件,**请将json文件和图片放在同一个文件夹里**,点击`Next Image`标注下一张图片。 +比如针对有空洞的目标,在标注完目标外轮廓后,再沿空洞边缘画多边形,并将空洞指定为特定类别,如果空洞是背景则指定为`_background_`,示例如下。 -LableMe产出的真值文件可参考我们给出的[文件夹](https://github.com/PaddlePaddle/PaddleSeg/blob/release/v0.8.0/docs/annotation/labelme_demo)。 +
+ +

带空洞目标的标注示意图

+
+如果在标注过程中某个点画错了,可以鼠标右键选择撤销该点;点击`Edit Polygons`可以移动多边形的位置,也可以移动某个点的位置;右击点击类别label,可以选择`Edit Label`修改类别名称。
- -

图5 LableMe产出的真值文件的示意图

+ +

修改标注的示意图

-**Note:** - -对于中间有空洞的目标的标注方法:在标注完目标轮廓后,再沿空洞区域边缘画多边形,并将其指定为其他类别,如果是背景则指定为`_background_`。如下: +图片中所有目标的标注都完成后,直接选择下一张图片进行标注。(由于勾选`Save Automatically`,不再需要手动点击`Save`保存json文件) +检查标注json文件和图片**存放在同一个文件夹**,而且是一一对应关系,如下图所示。
- -

图6 带空洞目标的标注示意图

+ +

LableMe产出的标注文件的示意图

## 3. 数据格式转换 -使用PaddleSeg提供的数据转换脚本,将LabelMe标注工具产出的数据格式转换为PaddleSeg所需的数据格式。 +使用PaddleSeg提供的数据转换脚本,将LabelMe标注工具产出的数据格式转换为PaddleSeg和PaddleX所需的数据格式。 -运行以下代码进行转换,其中``为图片以及LabelMe产出的json文件所在文件夹的目录,同时也是转换后的标注集所在文件夹的目录。 +运行以下代码进行转换,第一个`input_dir`参数是原始图像和json标注文件的保存目录,第二个`output_dir`参数是转换后数据集的保存目录。 ``` -python tools/data/labelme2seg.py +python tools/data/labelme2seg.py input_dir output_dir ``` -经过数据格式转换后的数据集目录结构如下: +格式转换后的数据集目录结构如下: ``` -my_dataset # 根目录 -|-- annotations # 标注图像的目录 -| |-- xxx.png # 标注图像 +dataset_dir # 根目录 +|-- images # 原始图像的目录 +| |-- xxx.png(png or other) # 原始图像 | |... -|-- class_names.txt # 数据集的类别名称 -|-- xxx.jpg(png or other) # 原图 -|-- ... -|-- xxx.json # 标注json文件 -|-- ... - +|-- annotations # 标注图像的目录 +| |-- xxx.png # 标注图像 +| |... +|-- class_names.txt # 数据集的类别名称,背景_background_的类别id是0,其他类别id依次递增 ``` - - -
- -

图7 格式转换后的数据集目录的结构示意图

-
diff --git a/tools/data/labelme2seg.py b/tools/data/labelme2seg.py index 20bf7d55f5..554bb80e1d 100644 --- a/tools/data/labelme2seg.py +++ b/tools/data/labelme2seg.py @@ -21,102 +21,122 @@ import json import os import os.path as osp +import shutil + import numpy as np import PIL.Image import PIL.ImageDraw import cv2 -from gray2pseudo_color import get_color_map_list - def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('input_dir', help='input annotated directory') + parser.add_argument('output_dir', help='output annotated directory') return parser.parse_args() +def get_color_map_list(num_classes): + num_classes += 1 + color_map = num_classes * [0, 0, 0] + for i in range(0, num_classes): + j = 0 + lab = i + while lab: + color_map[i * 3] |= (((lab >> 0) & 1) << (7 - j)) + color_map[i * 3 + 1] |= (((lab >> 1) & 1) << (7 - j)) + color_map[i * 3 + 2] |= (((lab >> 2) & 1) << (7 - j)) + j += 1 + lab >>= 3 + color_map = color_map[3:] + return color_map + + +def shape2mask(img_size, points): + label_mask = PIL.Image.fromarray(np.zeros(img_size[:2], dtype=np.uint8)) + image_draw = PIL.ImageDraw.Draw(label_mask) + points_list = [tuple(point) for point in points] + assert len(points_list) > 2, 'Polygon must have points more than 2' + image_draw.polygon(xy=points_list, outline=1, fill=1) + return np.array(label_mask, dtype=bool) + + +def shape2label(img_size, shapes, class_name_mapping): + label = np.zeros(img_size[:2], dtype=np.int32) + for shape in shapes: + points = shape['points'] + class_name = shape['label'] + shape_type = shape.get('shape_type', None) + class_id = class_name_mapping[class_name] + label_mask = shape2mask(img_size[:2], points) + label[label_mask] = class_id + return label + + def main(args): - output_dir = osp.join(args.input_dir, 'annotations') + # prepare + output_dir = args.output_dir + output_img_dir = osp.join(args.output_dir, 'images') + output_annot_dir = osp.join(args.output_dir, 'annotations') if not osp.exists(output_dir): os.makedirs(output_dir) - print('Creating annotations directory:', output_dir) - - # get the all class names for the given dataset + print('Creating directory:', output_dir) + if not osp.exists(output_img_dir): + os.makedirs(output_img_dir) + print('Creating directory:', output_img_dir) + if not osp.exists(output_annot_dir): + os.makedirs(output_annot_dir) + print('Creating directory:', output_annot_dir) + + # collect and save class names class_names = ['_background_'] for label_file in glob.glob(osp.join(args.input_dir, '*.json')): with open(label_file) as f: data = json.load(f) for shape in data['shapes']: - label = shape['label'] - cls_name = label - if not cls_name in class_names: + cls_name = shape['label'] + if cls_name not in class_names: class_names.append(cls_name) class_name_to_id = {} for i, class_name in enumerate(class_names): class_id = i # starts with 0 class_name_to_id[class_name] = class_id - if class_id == 0: - assert class_name == '_background_' - class_names = tuple(class_names) print('class_names:', class_names) - out_class_names_file = osp.join(args.input_dir, 'class_names.txt') + out_class_names_file = osp.join(output_dir, 'class_names.txt') with open(out_class_names_file, 'w') as f: f.writelines('\n'.join(class_names)) print('Saved class_names:', out_class_names_file) + # create annotated images and copy origin images color_map = get_color_map_list(256) - for label_file in glob.glob(osp.join(args.input_dir, '*.json')): print('Generating dataset from:', label_file) + filename = osp.splitext(osp.basename(label_file))[0] + annotated_img_path = osp.join(output_annot_dir, filename + '.png') with open(label_file) as f: - base = osp.splitext(osp.basename(label_file))[0] - out_png_file = osp.join(output_dir, base + '.png') - data = json.load(f) + img_path = osp.join(osp.dirname(label_file), data['imagePath']) + shutil.copy(img_path, output_img_dir) - img_file = osp.join(osp.dirname(label_file), data['imagePath']) - img = np.asarray(cv2.imread(img_file)) - + img = np.asarray(cv2.imread(img_path)) lbl = shape2label( img_size=img.shape, shapes=data['shapes'], class_name_mapping=class_name_to_id, ) - if osp.splitext(out_png_file)[1] != '.png': - out_png_file += '.png' # Assume label ranges [0, 255] for uint8, if lbl.min() >= 0 and lbl.max() <= 255: lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode='P') lbl_pil.putpalette(color_map) - lbl_pil.save(out_png_file) + lbl_pil.save(annotated_img_path) else: raise ValueError( '[%s] Cannot save the pixel-wise class label as PNG. ' - 'Please consider using the .npy format.' % out_png_file) - - -def shape2mask(img_size, points): - label_mask = PIL.Image.fromarray(np.zeros(img_size[:2], dtype=np.uint8)) - image_draw = PIL.ImageDraw.Draw(label_mask) - points_list = [tuple(point) for point in points] - assert len(points_list) > 2, 'Polygon must have points more than 2' - image_draw.polygon(xy=points_list, outline=1, fill=1) - return np.array(label_mask, dtype=bool) - - -def shape2label(img_size, shapes, class_name_mapping): - label = np.zeros(img_size[:2], dtype=np.int32) - for shape in shapes: - points = shape['points'] - class_name = shape['label'] - shape_type = shape.get('shape_type', None) - class_id = class_name_mapping[class_name] - label_mask = shape2mask(img_size[:2], points) - label[label_mask] = class_id - return label + 'Please consider using the .npy format.' % + annotated_img_path) if __name__ == '__main__':