Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refine the doc and code of annotation images #3497

Merged
merged 8 commits into from
Sep 12, 2023
101 changes: 55 additions & 46 deletions docs/data/transform/transform_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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的交互界面。

<div align="center">
<img src="../image/image-1.png" width = "600" />
<p>图1 LableMe交互界面的示意图</p>
<p>LableMe交互界面</p>
</div>

点击`OpenDir`打开`<path/to/labelme>/examples/semantic_segmentation/data_annotated`,其中`<path/to/labelme>`为克隆下来的`labelme`的路径,打开后显示的是语义分割的真值标注。

点击左上角`File`:
* 勾选`Save Automatically`,设置软件自动保存标注json文件,避免需要手动保存
* 取消勾选`Save With Image Data`,设置标注json文件中不保存data数据

<div align="center">
<img src="../image/image-2.png" width = "600" />
<p>图2 已标注图片的示意图</p>
<img src="https://github.com/PaddlePaddle/PaddleSeg/assets/52520497/935090d4-7b4f-4afc-b878-5e2b6c8dd2a8" width = "600" />
<p>LableMe设置</p>
</div>


### 开始标注图片
### 2.2 预览已标注图片(可选)

(1) 点击`OpenDir`打开待标注图片所在目录,点击`Create Polygons`,沿着目标的边缘画多边形,完成后输入目标的类别。在标注过程中,如果某个点画错了,可以按撤销快捷键可撤销该点。Mac下的撤销快捷键为`command+Z`。
执行如下命令,clone下载LabelMe的代码。
```
git clone https://github.com/wkentaro/labelme.git
```

在LabelMe交互界面上点击`OpenDir`,选择`<path/to/labelme>/examples/semantic_segmentation/data_annotated`目录(`<path/to/labelme>`为clone下载的`labelme`的路径),打开后可以显示的是语义分割的真值标注。

<div align="center">
<img src="../image/image-3.png" width = "600" />
<p>图3 标注单个目标的示意图</p>
<img src="../image/image-2.png" width = "600" />
<p>已标注图片的示意图</p>
</div>


​(2) 右击选择`Edit Polygons`可以整体移动多边形的位置,也可以移动某个点的位置;右击选择`Edit Label`可以修改每个目标的类别。请根据自己的需要执行这一步骤,若不需要修改,可跳过。
### 2.3 标注图片

将所有待标注图片保存在一个目录下,点击`OpenDir`打开待标注图片所在目录。

点击`Create Polygons`,沿着前景目标的边缘画闭合的多边形,然后输入或者选择目标的类别。

<div align="center">
<img src="../image/image-4-2.png" width = "600" />
<p>图4 修改标注的示意图</p>
<img src="../image/image-3.png" width = "600" />
<p>标注单个目标的示意图</p>
</div>

通常情况下,大家只需要标注前景目标并设置标注类别,其他像素默认作为背景。如果大家需要手动标注背景区域,**类别必须设置为`_background_`**,否则格式转换会有问题。

(3) 图片中所有目标的标注都完成后,点击`Save`保存json文件,**请将json文件和图片放在同一个文件夹里**,点击`Next Image`标注下一张图片
比如针对有空洞的目标,在标注完目标外轮廓后,再沿空洞边缘画多边形,并将空洞指定为特定类别,如果空洞是背景则指定为`_background_`,示例如下

LableMe产出的真值文件可参考我们给出的[文件夹](https://github.com/PaddlePaddle/PaddleSeg/blob/release/v0.8.0/docs/annotation/labelme_demo)。
<div align="center">
<img src="../image/image-10.jpg" width = "600" />
<p>带空洞目标的标注示意图</p>
</div>

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

<div align="center">
<img src="../image/image-5.png" width = "600" />
<p>图5 LableMe产出的真值文件的示意图</p>
<img src="../image/image-4-2.png" width = "600" />
<p>修改标注的示意图</p>
</div>


**Note:**

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

检查标注json文件和图片**存放在同一个文件夹**,而且是一一对应关系,如下图所示。

<div align="center">
<img src="../image/image-10.jpg" width = "600" />
<p>图6 带空洞目标的标注示意图</p>
<img src="https://github.com/PaddlePaddle/PaddleSeg/assets/52520497/03407e35-f5bf-4312-aecd-0929dff1a984" width = "400" />
<p>LableMe产出的标注文件的示意图</p>
</div>


## 3. 数据格式转换

使用PaddleSeg提供的数据转换脚本,将LabelMe标注工具产出的数据格式转换为PaddleSeg所需的数据格式
使用PaddleSeg提供的数据转换脚本,将LabelMe标注工具产出的数据格式转换为PaddleSeg和PaddleX所需的数据格式

运行以下代码进行转换,其中`<PATH/TO/LABEL_JSON_FILE>`为图片以及LabelMe产出的json文件所在文件夹的目录,同时也是转换后的标注集所在文件夹的目录
运行以下代码进行转换,第一个`input_dir`参数是原始图像和json标注文件的保存目录,第二个`output_dir`参数是转换后数据集的保存目录

```
python tools/data/labelme2seg.py <PATH/TO/LABEL_JSON_FILE>
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依次递增
```


<div align="center">
<img src="../image/image-6.png" width = "600" />
<p>图7 格式转换后的数据集目录的结构示意图</p>
</div>
110 changes: 65 additions & 45 deletions tools/data/labelme2seg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down