Files
ModelTrainEval/tests/test_yolo_format.py
T
Shaun ZhouandClaude Opus 5 d6adea5b05 test: 单元测试与端到端冒烟
88 个测试覆盖配置继承、注册表、GPU 守卫、YOLO 格式解析、数据集、模型形状、
标签模式、聚类原语与 SimCLR 损失,以及 train.py / eval.py / 聚类脚本的入口接线。

含两条回归测试:
- PCA 白化放大噪声方向导致簇塌陷(默认 whiten=False 的依据)
- classes 写成 data.yaml 路径时 num_classes 必须是类别数而非路径字符串长度

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:55:47 +08:00

119 lines
3.9 KiB
Python

"""YOLO 标注格式的解析与写出。"""
from pathlib import Path
import pytest
pytest.importorskip("numpy")
import numpy as np # noqa: E402
from datasets.yolo import ( # noqa: E402
denormalize,
format_label_lines,
label_path_for,
list_images,
load_classes,
parse_label_file,
write_data_yaml,
)
def test_label_path_swaps_images_for_labels():
img_dir = Path("/data/film/images/train")
assert label_path_for(img_dir / "0001.png", img_dir) == Path("/data/film/labels/train/0001.txt")
def test_label_path_keeps_subdirs_and_honours_explicit_dir():
img_dir = Path("/data/film/images/train")
assert label_path_for(img_dir / "cam1/0001.png", img_dir) == Path(
"/data/film/labels/train/cam1/0001.txt"
)
assert label_path_for(img_dir / "0001.png", img_dir, Path("/tmp/lab")) == Path("/tmp/lab/0001.txt")
def test_parse_center_format(tmp_path):
path = tmp_path / "a.txt"
path.write_text("0 0.5 0.5 0.4 0.2\n# 注释\n\n1 0.25 0.25 0.1 0.1\n", encoding="utf-8")
boxes, labels = parse_label_file(path)
assert labels.tolist() == [0, 1]
assert boxes[0].tolist() == pytest.approx([0.3, 0.4, 0.7, 0.6])
assert denormalize(boxes, 100, 200)[0].tolist() == pytest.approx([30.0, 80.0, 70.0, 120.0])
def test_missing_file_means_no_defect(tmp_path):
boxes, labels = parse_label_file(tmp_path / "nope.txt")
assert boxes.shape == (0, 4) and labels.shape == (0,)
def test_polygon_rows_fall_back_to_bounding_box(tmp_path):
path = tmp_path / "a.txt"
path.write_text("2 0.1 0.1 0.4 0.2 0.3 0.5\n", encoding="utf-8")
boxes, labels = parse_label_file(path)
assert labels.tolist() == [2]
assert boxes[0].tolist() == pytest.approx([0.1, 0.1, 0.4, 0.5])
def test_coordinates_are_clamped_and_degenerate_rows_dropped(tmp_path):
path = tmp_path / "a.txt"
path.write_text("0 0.5 0.5 2.0 2.0\n0 0.5 0.5 0.0 0.0\n", encoding="utf-8")
boxes, labels = parse_label_file(path)
assert len(labels) == 1
assert boxes[0].tolist() == pytest.approx([0.0, 0.0, 1.0, 1.0])
def test_malformed_rows_report_file_and_line(tmp_path):
path = tmp_path / "a.txt"
path.write_text("0 0.5 0.5 0.4\n", encoding="utf-8")
with pytest.raises(ValueError, match=r"a\.txt:1"):
parse_label_file(path)
path.write_text("scratch 0.5 0.5 0.4 0.2\n", encoding="utf-8")
with pytest.raises(ValueError, match="不是合法的 YOLO 标注行"):
parse_label_file(path)
def test_class_id_range_is_checked(tmp_path):
path = tmp_path / "a.txt"
path.write_text("3 0.5 0.5 0.4 0.2\n", encoding="utf-8")
with pytest.raises(ValueError, match="越界"):
parse_label_file(path, num_classes=2)
def test_format_lines_roundtrips_pixel_boxes(tmp_path):
boxes = np.array([[10.0, 20.0, 50.0, 60.0]])
lines = format_label_lines(boxes, [1], width=100, height=200)
assert lines == ["1 0.300000 0.200000 0.400000 0.200000"]
path = tmp_path / "a.txt"
path.write_text("\n".join(lines), encoding="utf-8")
parsed, labels = parse_label_file(path)
assert labels.tolist() == [1]
assert denormalize(parsed, 100, 200)[0].tolist() == pytest.approx([10.0, 20.0, 50.0, 60.0])
def test_data_yaml_roundtrip(tmp_path):
pytest.importorskip("yaml")
path = write_data_yaml(tmp_path / "data.yaml", ["scratch", "bubble"], root=tmp_path)
assert load_classes(path) == ["scratch", "bubble"]
assert load_classes(["a", "b"]) == ["a", "b"]
def test_list_images_supports_dir_and_list_file(tmp_path):
images = tmp_path / "images"
images.mkdir()
for name in ("b.png", "a.jpg", "notes.txt"):
(images / name).write_bytes(b"")
assert [p.name for p in list_images(images)] == ["a.jpg", "b.png"]
list_file = tmp_path / "train.txt"
list_file.write_text("images/b.png\nimages/a.jpg\n", encoding="utf-8")
assert [p.name for p in list_images(images, list_file)] == ["b.png", "a.jpg"]