<think>我们有一个labelme的JSON文件,它包含图像标注信息,通常包括多个形状(多边形、矩形等)以及每个形状对应的标签。我们的目标是将这些标注转换为单通道的PNG掩码图像。
在单通道PNG掩码中,每个像素的值表示该像素所属的类别(通常用整数表示,例如0表示背景,1表示类别1,2表示类别2,等等)。
步骤:
1. 读取JSON文件,解析出图像的高度和宽度。
2. 创建一个与图像大小相同的全零数组(表示背景)。
3. 遍历JSON中的每一个形状(shape),每个形状都有一个标签(label)和一组点(points)。
4. 根据标签,我们确定该形状对应的类别索引(需要一个从标签名到类别索引的映射,例如通过一个类别列表)。
5. 使用这些点绘制多边形(或其他形状)填充,将数组中对应的位置设置为类别索引。
6. 将数组保存为单通道的PNG图像。
注意:labelme的JSON文件可能包含多个不同类别的形状,甚至可能有重叠。我们需要确保在绘制时,后绘制的形状可能会覆盖先绘制的(取决于需求,但通常掩码要求每个像素只属于一个类别,所以需要处理覆盖顺序,或者使用其他规则,如按标注顺序,后面的覆盖前面的)。
但是,在语义分割中,我们通常希望每个像素只属于一个类别。如果标注有重叠,我们可以按照标注的顺序,后面的覆盖前面的,或者我们可以使用一个规则(例如,只保留第一个出现的类别)。这里我们按照常见的做法:按顺序绘制,后面的形状会覆盖前面的。
另外,labelme支持多种形状(多边形、矩形、圆形、线段等),但通常用于语义分割的是多边形和矩形。这里我们主要处理多边形(polygon)和矩形(rectangle)。矩形可以看作是多边形的一种特殊形式(四个点)。
实现:
我们可以使用PIL(Python Imaging Library)或者OpenCV来绘制多边形并填充。这里我们使用PIL的ImageDraw。
首先,需要安装必要的库(如果未安装):
pip install labelme numpy Pillow
注意:labelme库并不是必须的,因为我们可以直接解析JSON,但这里我们不需要labelme库,直接使用json库读取即可。
步骤详细说明:
1. 读取JSON文件,使用json.load。
2. 从JSON中获取图像高度和宽度(imageHeight和imageWidth)。
3. 创建一个新的PIL图像,模式为'I'(32位有符号整数)或者'L'(8位无符号整数),具体取决于类别数量。如果类别数量小于256,我们可以使用8位,否则需要更大的位数。这里假设类别数量较少,使用8位(0-255)。
4. 使用ImageDraw来绘制多边形。
但是,注意:单通道图像中,0通常表示背景,而类别索引从1开始。所以我们需要一个从标签名到索引的映射(例如,{'class1':1, 'class2':2, ...})。这个映射可以由用户提供,或者从JSON文件中提取所有标签并分配索引(注意顺序,我们可以按照标签在JSON中出现的顺序,但要注意重复标签)。
然而,为了避免混淆,最好由用户提供一个类别列表,这样索引顺序是固定的。如果没有提供,我们可以从JSON中提取所有不重复的标签(按出现顺序),然后背景为0,第一个标签为1,以此类推。
但是,在转换过程中,我们通常需要预先定义好的类别映射。因此,我们可以要求用户传入一个类别名称到索引的字典,或者我们自动生成(但自动生成可能因顺序问题导致不一致)。
这里,我们假设用户会提供一个标签名称到索引的字典(label_to_index),其中不包括背景(背景默认为0)。如果某个标签不在字典中,我们可以选择忽略或报错。
具体代码步骤:
1. 读取JSON文件。
2. 创建空白画布(全0,大小与原图相同)。
3. 遍历每个形状(shape):
获取形状的标签(label)
获取点(points)列表
将点转换为元组列表(因为PIL的多边形绘制需要)
在画布上绘制多边形,用对应的类别索引填充。
4. 将数组保存为PNG。
注意:绘制多边形时,我们使用numpy数组操作,然后转换为PIL图像,或者直接在PIL图像上绘制。这里我们使用PIL的ImageDraw。
但是,PIL的ImageDraw在绘制多边形时,要求多边形的点是一个元组序列,例如[(x0,y0), (x1,y1), ...]。
另外,labelme的JSON文件中,点的坐标是浮点数,我们需要转换为整数(四舍五入)。
处理矩形:labelme的矩形在JSON中是以两个点的形式(左上角和右下角)存储的。我们可以将矩形转换为四个点的多边形。
但是,在labelme的JSON中,形状的"shape_type"字段可以是"polygon"、"rectangle"等。对于矩形,我们可以这样转换:
points = [[x1, y1], [x2, y2]]
然后转换为多边形:[[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
然而,实际上labelme在导出矩形时,points就是两个点,我们需要自己扩展成四个点。
因此,我们需要根据shape_type来处理:
如果是"rectangle",则扩展成四个点;
如果是"polygon",则直接使用点列表。
另外,还有圆形(circle)?labelme支持圆形,但通常语义分割中较少使用。如果遇到,我们可以用多边形近似(例如用正多边形),但这里我们暂时不支持圆形,或者忽略。
考虑到通用性,我们只处理多边形和矩形。
代码示例:
假设我们有一个JSON文件路径和一个输出PNG路径,以及一个标签到索引的字典(label_map)。
如果没有提供label_map,我们可以自动生成(但这样可能每次顺序不同,所以不推荐)。因此,我们要求必须提供label_map。
步骤:
1. 读取JSON文件。
2. 获取图像大小(注意:有些JSON文件可能没有imageHeight和imageWidth,而是通过imagePath指向的图片获取大小。但labelme的JSON通常包含这两个字段)。
3. 创建一个全零的数组(numpy数组),大小为(height, width),数据类型为uint8。
4. 遍历每个形状:
label = shape['label']
shape_type = shape.get('shape_type', 'polygon') # 默认为多边形
points = shape['points']
if label not in label_map:
# 可以选择跳过或报错,这里我们跳过
continue
index = label_map[label]
# 处理矩形
if shape_type == 'rectangle' and len(points) == 2:
# 将矩形转换为四个点
x1, y1 = points[0]
x2, y2 = points[1]
points = [[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
shape_type = 'polygon' # 现在当做多边形处理
if shape_type == 'polygon':
# 将点转换为整数(四舍五入)
int_points = [(int(round(x)), int(round(y))) for x, y in points]
# 创建一个临时图像,用于绘制这个多边形
# 注意:我们不能直接在原数组上绘制,因为PIL的ImageDraw操作的是图像,而不是数组。
# 因此,我们可以创建一个和原图一样大的空白图像,绘制多边形,然后将绘制的结果(非零位置)赋值到原数组。
# 这样,每个形状我们单独绘制,然后合并到主掩码上。
# 但是这样效率较低,因为每个形状都要创建一个新图像。我们可以考虑使用另一个方法:在同一个ImageDraw上绘制,但是这样会覆盖之前的形状(因为我们是按顺序绘制,后面的覆盖前面,符合要求)。
# 因此,我们可以创建一个PIL图像(与掩码同样大小),然后绘制,最后将绘制的结果(像素值等于当前索引)更新到主掩码数组。
# 但是,我们也可以在主掩码图像上直接绘制。因为我们已经在使用PIL,我们可以:
# 1. 将当前的掩码数组转换为PIL图像(模式为'L')
# 2. 创建ImageDraw对象
# 3. 绘制多边形(用当前索引值填充)
# 4. 将PIL图像转回数组
# 但是这样每次都要转换,效率不高。
# 另一种方法:一开始就创建PIL图像,然后遍历每个形状,在PIL图像上绘制。这样我们只需要绘制一次。
# 所以,我们改变策略:
# 初始化一个PIL图像(全0),模式为'L'
# 遍历每个形状,用ImageDraw绘制多边形(填充值为类别索引)
# 最后保存这个图像。
# 但是,这样我们就需要一开始就创建PIL图像,然后绘制。这样更高效。
因此,我们修改步骤:
a. 创建PIL图像:img = Image.new('L', (width, height), 0) # 0是背景
b. 创建draw对象:draw = ImageDraw.Draw(img)
c. 遍历每个形状:
如果是多边形,则绘制多边形(用当前类别索引填充)
如果是矩形,则绘制矩形(但ImageDraw有绘制矩形的方法,但这里我们已经将矩形转换为多边形,所以统一用多边形绘制)
d. 保存img为PNG。
但是,这样绘制的话,后面的形状会覆盖前面的形状,因为每次绘制都会覆盖之前的像素。这正是我们想要的(按标注顺序,后面的覆盖前面的)。
因此,我们不需要使用数组,直接使用PIL绘制即可。
但是,注意:绘制多边形时,如果多边形有重叠,那么重叠部分会被后面的覆盖。这符合我们的要求。
代码实现:
注意:PIL的ImageDraw.polygon方法要求点的顺序是[(x0,y0), (x1,y1), ...],并且是整数。
另外,矩形转换后也是多边形,所以统一用多边形绘制。
对于其他形状(如圆形、线段),我们暂时不支持,可以跳过并给出警告。
具体代码:
假设输入:json_file(JSON文件路径),output_png(输出PNG路径),label_map(标签到索引的字典)
步骤:
1. 读取JSON文件。
2. 获取图像宽度和高度(如果JSON中没有,则可能需要从图像文件读取,但labelme的JSON一般有,所以直接读取)。
3. 创建PIL图像(模式'L',背景0)。
4. 创建ImageDraw对象。
5. 遍历每个形状(shapes):
label = shape['label']
shape_type = shape['shape_type']
points = shape['points']
if label not in label_map:
print(f"Warning: label '{label}' not found in label_map, skipped.")
continue
idx = label_map[label]
if shape_type == 'rectangle' and len(points) == 2:
# 转换为四个点
x1, y1 = points[0]
x2, y2 = points[1]
points = [[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
# 然后当做多边形处理
shape_type = 'polygon'
if shape_type == 'polygon':
# 转换为整数点
int_points = [(int(round(x)), int(round(y))) for x, y in points]
# 绘制多边形
draw.polygon(int_points, fill=idx)
else:
print(f"Warning: shape_type '{shape_type}' is not supported, skipped.")
6. 保存图像:img.save(output_png)
注意:PIL的坐标是(x,y),而numpy数组是(height, width),但这里我们创建图像时使用了(width, height),所以坐标点需要是(x,y)。我们使用JSON中的点,注意JSON中的点坐标是浮点数,四舍五入取整。
但是,这样绘制会覆盖重叠区域,符合我们的要求。
但是,有一个问题:如果同一个形状内部有空洞(即多个多边形组成一个对象),labelme通常用一个包含多个多边形的形状来表示(但labelme的一个形状其实是一个连续的多边形,如果有空洞,实际上是一个多边形包含内环)。但labelme的JSON格式中,每个形状只有一个多边形(即一个点列表),所以如果有空洞,需要多个形状?或者在一个形状中用多个点列表?实际上,labelme的JSON格式中,每个形状的"points"是一个点列表,表示一个多边形。如果有空洞,通常用多个形状(多个shape)来表示,每个形状都是独立的多边形,然后通过不同的标签?或者相同的标签?所以,我们按照标签来绘制,相同标签的多个多边形会分别绘制,但绘制顺序可能会影响(因为后面的会覆盖前面的,但相同标签覆盖也没关系,因为填充值相同)。但是,如果有空洞,通常需要将空洞区域标记为背景,那么我们需要在绘制时考虑。然而,在语义分割中,我们通常不区分子区域,而是将整个区域标记为同一类别。所以,如果出现空洞,那么空洞区域会被后面绘制的其他形状覆盖,或者如果空洞区域没有被其他形状覆盖,那么它仍然是背景(0),这符合要求。
因此,我们不需要特别处理空洞。
但是,实例分割和语义分割不同。我们这里做的是语义分割(单通道,每个像素一个类别),所以不考虑实例。
所以,代码可以这样写。
但是,如果用户没有提供label_map,我们可以自动生成吗?我们可以遍历所有形状,收集所有标签,然后分配索引(从1开始),背景为0。但是这样可能会因为每次运行顺序不同而导致索引变化。所以,我们最好要求用户提供。
我们可以提供一个可选参数,如果用户不提供,则自动生成,但需要按标签名字母排序,以保证顺序一致?或者按出现顺序?按出现顺序可能会不一致。所以,我们要求用户提供label_map。
下面是一个完整的函数示例:
注意:我们假设JSON文件中有imageHeight和imageWidth。如果没有,则需要从图像文件读取,但这里我们不做处理,直接报错。
代码:</think>### 将LabelMe的JSON文件转换为单通道PNG掩码
以下是使用Python实现的完整代码,通过解析LabelMe的JSON标注文件生成单通道PNG掩码图像。核心步骤包括解析多边形坐标、创建空白掩码画布、填充多边形区域,并处理重叠标注。
```python
import json
import numpy as np
from PIL import Image, ImageDraw
def labelme_json_to_mask(json_path, output_path):
"""
将LabelMe的JSON标注文件转换为单通道PNG掩码
:param json_path: LabelMe JSON文件路径
:param output_path: 输出PNG掩码路径
"""
# 加载JSON数据
with open(json_path, 'r') as f:
data = json.load(f)
# 获取图像尺寸
width = data['imageWidth']
height = data['imageHeight']
# 创建空白掩码(单通道,初始为0=背景)
mask = np.zeros((height, width), dtype=np.uint8)
pil_mask = Image.fromarray(mask)
draw = ImageDraw.Draw(pil_mask)
# 遍历所有标注形状
for shape in data['shapes']:
label = shape['label']
points = shape['points']
# 将多边形坐标转换为整数元组
polygon = [(int(x), int(y)) for point in points for x, y in [point]]
# 根据标签分配像素值(示例:'cat'->1, 'dog'->2)
if label == 'cat':
fill_value = 1
elif label == 'dog':
fill_value = 2
else: # 其他类别
fill_value = 3
# 绘制填充多边形
draw.polygon(polygon, fill=fill_value)
# 保存单通道PNG掩码
pil_mask.save(output_path)
print(f"掩码已保存至: {output_path}")
# 使用示例
labelme_json_to_mask('annotation.json', 'mask.png')
```
### 关键说明:
1. **标签值映射**
- 背景像素值固定为 `0`
- 示例中:`'cat'→1`, `'dog'→2`,其他类别→`3`
- 需根据实际类别修改映射关系(如构建字典 `label_mapping = {'cat':1, 'dog':2}`)
2. **重叠处理**
- 后绘制的形状会覆盖先前区域(按JSON中顺序)
- 确保关键类别在JSON中靠后声明
3. **支持类型**
- 原生支持多边形(`polygon`)
- 矩形自动转换为多边形(LabelMe矩形即两点定义的多边形)
4. **注意事项**
- 要求JSON包含 `imageWidth`/`imageHeight` 字段
- 输出为8位单通道PNG(0-255类别值)