fix(detect): 钳掉 Label Studio 导出坐标的浮点溢出

真实导出里出现 height=1.0000000000000002:Label Studio 按百分比存框,导出时 /100
留下浮点误差。ultralytics 读标签时对 >1 的坐标直接报错,所以这点溢出必须钳掉,
否则装配出的数据集根本训不起来。

只钳 1e-6 量级的溢出,真正越界的坐标仍然报错。顺带把坐标统一写成 6 位小数。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-17 16:44:42 +08:00
co-authored by Claude Opus 5
parent 998e6601d4
commit 59d4b8a9c4
2 changed files with 76 additions and 12 deletions
+42 -1
View File
@@ -76,7 +76,10 @@ def test_builds_a_split_dataset(export, tmp_path):
images, labels = outputs(out, "images"), outputs(out, "labels")
assert set(images) == set(LABELS) == set(labels)
for stem, path in labels.items():
assert path.read_text(encoding="utf-8") == LABELS[stem]
# 内容等价,但坐标统一成 6 位小数
written = [line.split() for line in path.read_text(encoding="utf-8").splitlines()]
expected = [line.split() for line in LABELS[stem].splitlines()]
assert [[v[0], *(f"{float(x):.6f}" for x in v[1:])] for v in expected] == written
# 图与标签必须落在同一个 split,否则验证集会混进训练图
assert path.parent.name == images[stem].parent.name
@@ -227,3 +230,41 @@ def test_refuses_to_clobber_and_dry_run_writes_nothing(export, tmp_path):
assert run(export, out) == 0
assert run(export, out) != 0 # 已存在且非空
assert run(export, out, "--overwrite") == 0
def _repack(archive, tmp_path, replacements):
"""复制一份导出,替换其中几个标签文件。"""
broken = tmp_path / "repacked.zip"
with zipfile.ZipFile(archive) as source, zipfile.ZipFile(broken, "w") as target:
for item in source.namelist():
data = replacements.get(item, source.read(item))
target.writestr(item, data.encode("utf-8") if isinstance(data, str) else data)
return broken
def test_float_overshoot_is_clamped(export, tmp_path, capsys):
"""Label Studio 按百分比存框,/100 后会出现 1.0000000000000002。
回归:不钳位的话 ultralytics 读标签时会直接报"坐标未归一化"而拒绝训练。
"""
archive, images = export
overshoot = "1 0.49999999999999994 0.5000000000000001 0.9999999999999999 1.0000000000000002\n"
repacked = _repack(archive, tmp_path, {"labels/grease_0001.txt": overshoot})
out = tmp_path / "out"
assert run((repacked, images), out) == 0
assert "已钳到 [0,1]" in capsys.readouterr().out
written = outputs(out, "labels")["grease_0001"].read_text(encoding="utf-8").split()
assert written == ["1", "0.500000", "0.500000", "1.000000", "1.000000"]
assert all(0.0 <= float(v) <= 1.0 for v in written[1:])
# 四个值里只有 1.0000000000000002 真的越界,其余虽有浮点误差但仍在 [0,1] 内
assert json.loads((out / "summary.json").read_text(encoding="utf-8"))["clamped_coordinates"] == 1
def test_real_out_of_range_is_still_rejected(export, tmp_path, capsys):
"""只钳浮点误差量级;真正越界的坐标仍要报错。"""
archive, images = export
repacked = _repack(archive, tmp_path, {"labels/grease_0001.txt": "1 0.5 1.02 0.1 0.1\n"})
assert run((repacked, images), tmp_path / "out") != 0
assert "不在 [0,1]" in capsys.readouterr().out