Skip to content

Commit

Permalink
refine the doc and code of annotation images (#3497)
Browse files Browse the repository at this point in the history
* Update QR code

* refine the doc and code of annotation images
  • Loading branch information
juncaipeng authored Sep 12, 2023
1 parent 14f5abd commit 1b9574e
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 91 deletions.
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

0 comments on commit 1b9574e

Please sign in to comment.