Label Studio 图像标注坐标单位详解:LS 百分比(%)与像素(px)的换算原理与实战
【免费下载链接】label-studioLabel Studio is a multi-type data labeling and annotation tool with standardized output format项目地址: https://gitcode.com/GitHub_Trending/la/label-studio
Label Studio 的图像类标注(矩形框、多边形、关键点、椭圆等)在注解结果中,x、y、width、height等几何字段统一以相对图像宽高的百分比存储,而非像素绝对值。本文以官方文档 docs/source/includes/image_units.md 为核心,完整讲解这一坐标体系的含义、双向换算公式、注解结果(result)的完整结构,并结合前端编辑器源码(web/libs/editor)验证其底层实现,帮助你正确处理导出数据、训练集构建与模型推理结果回写。
读完本文,你将能够:理解 Label Studio 图像标注结果中每个字段的真实含义;在 Python 中自由完成「百分比 ↔ 像素」双向换算;读懂任何图像类标注的 JSON 结果(含旋转与多图场景);并在训练自己的检测模型时准确还原像素级标注框。
为什么图像标注坐标要用百分比而不是像素
在 docs/source/includes/image_units.md 中明确规定:图像标注结果中x、y、width、height的单位是占图像整体尺寸的百分比(percentages of overall image dimension)。
这并非随意设计,而是由 result_format 所定义的通用注解格式决定的。Label Studio 的每条标注结果(region)在annotation.result下存储为列表项,value字段承载标注动作的几何或语义信息。对于图像类标注,采用归一化百分比坐标有以下实际好处:
- 与原始图像分辨率解耦:同一份标注结果可以应用于不同分辨率下的同一图像,预览、缩放、适配容器时无需重新换算;
- 兼容多图与画布缩放:编辑器在画布上渲染时,统一将内部归一化坐标换算为画布坐标(见下文源码分析),导出时则记录原始图像尺寸作为换算基准;
- 统一各标签类型的存储口径:矩形框、多边形、椭圆、关键点等所有图像类 region 都遵循同一套百分比规则。
核心换算公式:LS 百分比 ↔ 像素
官方文档给出了两组标准换算公式。设original_width、original_height为原始图像的像素宽高,x, y, width, height为标注结果中的百分比数值:
百分比 → 像素(LS → Pixel):
pixel_x = x / 100.0 * original_width pixel_y = y / 100.0 * original_height pixel_width = width / 100.0 * original_width pixel_height = height / 100.0 * original_height像素 → 百分比(Pixel → LS):
x = pixel_x / original_width * 100.0 y = pixel_y / original_height * 100.0 width = pixel_width / original_width * 100.0 height = pixel_height / original_height * 100.0需要注意:x、y表示区域左上角相对于图像左上角的百分比偏移;width、height表示区域宽高占图像宽高的百分比。因此理论上它们都应在0~100区间内(编辑器的拖拽范围也受此约束),但通过 API 写入时引擎并不会强制限制,越界值需要自行校验。
完整实战示例:从注解结果到像素框
以下完整代码来自官方文档 image_units.md,包含一个标准的图像矩形框注解 Task 结构,以及两个方向的转换函数与调用验证,可直接复制运行:
task = { "annotations": [{ "result": [ { "...": "...", "original_width": 600, "original_height": 403, "image_rotation": 0, "value": { "x": 5.33, "y": 23.57, "width": 29.16, "height": 31.26, "rotation": 0, "rectanglelabels": [ "Airplane" ] } } ] }] } # convert from LS percent units to pixels def convert_from_ls(result): if 'original_width' not in result or 'original_height' not in result: return None value = result['value'] w, h = result['original_width'], result['original_height'] if all([key in value for key in ['x', 'y', 'width', 'height']]): return w * value['x'] / 100.0, \ h * value['y'] / 100.0, \ w * value['width'] / 100.0, \ h * value['height'] / 100.0 # convert from pixels to LS percent units def convert_to_ls(x, y, width, height, original_width, original_height): return x / original_width * 100.0, y / original_height * 100.0, \ width / original_width * 100.0, height / original_height * 100 # convert from LS output = convert_from_ls(task['annotations'][0]['result'][0]) if output is None: raise Exception('Wrong convert') pixel_x, pixel_y, pixel_width, pixel_height = output print(pixel_x, pixel_y, pixel_width, pixel_height) # convert back to LS x, y, width, height = convert_to_ls(pixel_x, pixel_y, pixel_width, pixel_height, 600, 403) print(x, y, width, height)针对上述示例数据手工验算:pixel_x = 5.33 / 100 * 600 = 31.98,pixel_y = 23.57 / 100 * 403 ≈ 94.99,pixel_width = 29.16 / 100 * 600 = 174.96,pixel_height = 31.26 / 100 * 403 ≈ 125.98。反向转换后应能恢复出原始的百分比数值(浮点精度导致的微小尾差属正常现象)。
读懂完整的结果字段:original_width / original_height / image_rotation / value
结合前端源码 Image.js 的createSerializedResult实现,可以看到每个图像类 region 的序列化结构由「图像维度元数据 + value」两部分组成:
const imageDimension = { original_width: currentImageEntity.naturalWidth, // 原始图像像素宽 original_height: currentImageEntity.naturalHeight, // 原始图像像素高 image_rotation: currentImageEntity.rotation, // 图像整体旋转角度(度) };各字段含义如下:
| 字段 | 类型 | 含义 |
|---|---|---|
original_width | number | 原始图像的像素宽度,是百分比换算回像素的基准 |
original_height | number | 原始图像的像素高度,同理为换算基准 |
image_rotation | number | 图像在画布上的整体旋转角度(度),例如 90/180/270,参与坐标系变换 |
value.x | number | 区域左上角 X(占原图宽度的百分比,0–100) |
value.y | number | 区域左上角 Y(占原图高度的百分比,0–100) |
value.width | number | 区域宽度(占原图宽度的百分比) |
value.height | number | 区域高度(占原图高度的百分比) |
value.rotation | number | 区域自身的旋转角度(度),不随image_rotation改变 |
value.rectanglelabels | array | 该区域命中的标签列表,如["Airplane"] |
上述字段的语义注释(含0-100的取值范围说明)在 RectRegion.jsx 的RectRegionResultJSDoc 中也有完整定义,矩形框、椭圆、多边形、关键点等区域类型均继承该坐标体系。
源码级验证:前端如何序列化与换算坐标
为了确证百分比坐标的底层机制,可以从前端编辑器的两个关键实现点验证:
序列化出口:
Image.createSerializedResult(Image.js)在每次区域创建/修改时,将当前图像的naturalWidth、naturalHeight与旋转角写入original_width、original_height、image_rotation,再把内部归一化坐标value一并落盘。相关单元测试(Image.test.js)直接断言了该方法的输出结构,如{ original_width: 100, original_height: 80, image_rotation: 90, value }。画布坐标换算:
RectRegion(RectRegion.jsx)通过internalToCanvasX/Y与canvasToInternalX/Y在「归一化内部坐标」与「画布像素坐标」之间往返换算,canvasX/canvasY/canvasWidth/canvasHeight均由此得出;而serialize()(同文件 L383-L393)只输出归一化的x, y, width, height, rotation给后端存储。
因此,无论用户在画布上如何缩放、旋转,持久化到数据库的始终是百分比坐标 + 原始图像尺寸,像素计算由读取方按需完成——这正是本文开头公式的工程基础。
使用中的边界情况与注意事项
结合源码与格式定义,实际使用中需留意以下几点:
- 图像未加载完成时的序列化:
createSerializedResult中有一条保护逻辑——当图像尚未加载完成且区域携带_rawResult时,会直接克隆原始结果(Image.js),避免以错误的尺寸元数据覆盖已有标注。若你在 API 回写时省略original_width/original_height,编辑器可能无法正确换算显示。 - 多图场景:使用多图像对象标签时,每个 region 还会带有
item_index字段以区分属于哪张子图(Image.js),换算像素时必须使用对应子图的原始尺寸。 - 旋转区域:
value.rotation是区域自身旋转角,计算包围盒像素范围时需额外做旋转校正;image_rotation是整图旋转角,二者不要混淆。 - 浮点精度:反向换算后百分比可能存在极小尾差,比较时建议使用容差而非严格相等。
延伸阅读
- 注解结果通用结构(region、id、from_name/to_name、perRegion 等):result_format
- 图像对象标签配置(多图、旋转、缩放等属性):image 标签文档
- 矩形框标签配置:rectangle 标签文档
- 前端矩形区域实现与序列化源码:RectRegion.jsx
- 图像元数据注入与序列化源码:Image.js
- 测试套件中的真实图像标注样例(含 bbox ground truth):image_urls_with_bboxes_gt.json
掌握「百分比为存储单位、像素为计算单位」这一原则后,无论你是导出标注训练目标检测模型,还是把模型预测结果回写为 Label Studio 注解,都能在两种坐标系间准确无误地往返切换。
【免费下载链接】label-studioLabel Studio is a multi-type data labeling and annotation tool with standardized output format项目地址: https://gitcode.com/GitHub_Trending/la/label-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考