feat: initial HSAP platform
Huaxu Sentinel Active Safety Platform with embedded algorithm code, Docker Compose setup, and vendored dataset scaffolds for clone-and-run. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
268
datasets/dms/scripts/convert_to_yolo.py
Normal file
268
datasets/dms/scripts/convert_to_yolo.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""将非标准数据转为 Ultralytics/YOLO 可用格式。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from stratified_split import collect_yolo_samples, stratified_assign, stratified_assign_classify
|
||||
|
||||
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".JPG", ".JPEG", ".PNG"}
|
||||
|
||||
DAM_NAMES = [
|
||||
"face", "eye_open", "eye_partially_open", "eye_close", "mouth_open",
|
||||
"mouth_partially_open", "mouth_close", "side_face", "nod_face", "glasses",
|
||||
"sunglasses", "smoke", "phone", "driver", "rise_face",
|
||||
]
|
||||
DAM_NAME_TO_ID = {n: i for i, n in enumerate(DAM_NAMES)}
|
||||
|
||||
|
||||
def voc_to_yolo_box(xmin: float, ymin: float, xmax: float, ymax: float, w: int, h: int) -> tuple[float, float, float, float]:
|
||||
xc = (xmin + xmax) / 2.0 / w
|
||||
yc = (ymin + ymax) / 2.0 / h
|
||||
bw = (xmax - xmin) / w
|
||||
bh = (ymax - ymin) / h
|
||||
return xc, yc, bw, bh
|
||||
|
||||
|
||||
def _classes_from_lines(lines: list[str]) -> set[int]:
|
||||
out: set[int] = set()
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
out.add(int(line.split()[0]))
|
||||
return out
|
||||
|
||||
|
||||
def convert_dam_voc(
|
||||
src_root: Path,
|
||||
dst_root: Path,
|
||||
val_ratio: float = 0.1,
|
||||
seed: int = 42,
|
||||
link_images: bool = True,
|
||||
) -> dict:
|
||||
"""VOC(xml) -> YOLO,一次写入 train/val,避免二次 move。"""
|
||||
src_images = src_root / "images"
|
||||
src_labels = src_root / "labels"
|
||||
if not src_images.is_dir() or not src_labels.is_dir():
|
||||
raise FileNotFoundError(f"expect {src_images} and {src_labels}")
|
||||
|
||||
records: list[tuple[str, Path, list[str]]] = []
|
||||
unknown: dict[str, int] = {}
|
||||
n_skip, n_empty = 0, 0
|
||||
xml_files = sorted(src_labels.glob("*.xml"))
|
||||
total = len(xml_files)
|
||||
print(f" 解析 xml: {total} 个")
|
||||
|
||||
for i, xml_path in enumerate(xml_files, 1):
|
||||
if i % 500 == 0 or i == total:
|
||||
print(f" xml {i}/{total}", flush=True)
|
||||
stem = xml_path.stem
|
||||
img_src = None
|
||||
for ext in IMG_EXTS:
|
||||
p = src_images / f"{stem}{ext}"
|
||||
if p.is_file():
|
||||
img_src = p
|
||||
break
|
||||
if img_src is None:
|
||||
n_skip += 1
|
||||
continue
|
||||
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
size = root.find("size")
|
||||
w = int(size.findtext("width", "0"))
|
||||
h = int(size.findtext("height", "0"))
|
||||
if w <= 0 or h <= 0:
|
||||
n_skip += 1
|
||||
continue
|
||||
|
||||
lines: list[str] = []
|
||||
for obj in root.findall("object"):
|
||||
name = (obj.findtext("name") or "").strip()
|
||||
if name not in DAM_NAME_TO_ID:
|
||||
unknown[name] = unknown.get(name, 0) + 1
|
||||
continue
|
||||
box = obj.find("bndbox")
|
||||
xmin = float(box.findtext("xmin", "0"))
|
||||
ymin = float(box.findtext("ymin", "0"))
|
||||
xmax = float(box.findtext("xmax", "0"))
|
||||
ymax = float(box.findtext("ymax", "0"))
|
||||
xc, yc, bw, bh = voc_to_yolo_box(xmin, ymin, xmax, ymax, w, h)
|
||||
lines.append(f"{DAM_NAME_TO_ID[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}")
|
||||
|
||||
if not lines:
|
||||
n_empty += 1
|
||||
records.append((stem, img_src, lines))
|
||||
|
||||
samples = [(stem, _classes_from_lines(lines)) for stem, _, lines in records]
|
||||
assignment = stratified_assign(samples, val_ratio=val_ratio, seed=seed, min_val_per_class=1)
|
||||
print(f" 写入 YOLO: {len(records)} 对", flush=True)
|
||||
|
||||
img_abs_cache: dict[Path, str] = {}
|
||||
for i, (stem, img_src, lines) in enumerate(records, 1):
|
||||
if i % 500 == 0 or i == len(records):
|
||||
print(f" write {i}/{len(records)}", flush=True)
|
||||
split = assignment.get(stem, "train")
|
||||
lab_dst = dst_root / "labels" / split / f"{stem}.txt"
|
||||
img_dst = dst_root / "images" / split / img_src.name
|
||||
lab_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
img_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
lab_dst.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
||||
if img_dst.exists() or img_dst.is_symlink():
|
||||
img_dst.unlink()
|
||||
if link_images:
|
||||
if img_src not in img_abs_cache:
|
||||
img_abs_cache[img_src] = str(img_src.resolve())
|
||||
img_dst.symlink_to(img_abs_cache[img_src])
|
||||
else:
|
||||
shutil.copy2(img_src, img_dst)
|
||||
|
||||
n_val = sum(1 for v in assignment.values() if v == "val")
|
||||
yaml_path = dst_root.parent.parent / "configs" / "dam_0417.yaml"
|
||||
yaml_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
names_yaml = ", ".join(f'"{n}"' for n in DAM_NAMES)
|
||||
yaml_path.write_text(
|
||||
f"""# DAM 源数据 dam_src_0417 转换 (VOC -> YOLO)
|
||||
path: {dst_root.resolve()}
|
||||
train: images/train
|
||||
val: images/val
|
||||
|
||||
nc: {len(DAM_NAMES)}
|
||||
names: [{names_yaml}]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return {
|
||||
"total": len(records),
|
||||
"train": len(records) - n_val,
|
||||
"val": n_val,
|
||||
"empty_labels": n_empty,
|
||||
"skipped_no_image": n_skip,
|
||||
"unknown_names": unknown,
|
||||
"yaml": str(yaml_path),
|
||||
"dst": str(dst_root),
|
||||
}
|
||||
|
||||
|
||||
def convert_classify_layout(root: Path, val_ratio: float = 0.1, seed: int = 42) -> dict:
|
||||
"""从 train 按类划出 val(硬链优先,同盘更快)。"""
|
||||
train_dir = root / "train"
|
||||
if not train_dir.is_dir():
|
||||
raise FileNotFoundError(f"missing train/: {train_dir}")
|
||||
|
||||
val_dir = root / "val"
|
||||
if val_dir.is_dir() and any(val_dir.iterdir()):
|
||||
print(f" val/ 已存在,跳过: {root}")
|
||||
return {"skipped": "val exists"}
|
||||
|
||||
class_dirs = sorted(d for d in train_dir.iterdir() if d.is_dir())
|
||||
print(f" 类别数: {len(class_dirs)}", flush=True)
|
||||
assignment = stratified_assign_classify(class_dirs, val_ratio, seed, min_val_per_class=1)
|
||||
val_items = [(p, sp) for p, sp in assignment.items() if sp == "val"]
|
||||
print(f" 划出 val: {len(val_items)} 张", flush=True)
|
||||
|
||||
val_dir.mkdir(parents=True, exist_ok=True)
|
||||
moved, linked = 0, 0
|
||||
for i, (src_path, _) in enumerate(val_items, 1):
|
||||
if i % 2000 == 0 or i == len(val_items):
|
||||
print(f" val {i}/{len(val_items)}", flush=True)
|
||||
dst = val_dir / src_path.parent.name / src_path.name
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dst.exists():
|
||||
continue
|
||||
try:
|
||||
os.link(src_path, dst)
|
||||
src_path.unlink()
|
||||
linked += 1
|
||||
except OSError:
|
||||
shutil.move(str(src_path), str(dst))
|
||||
moved += 1
|
||||
|
||||
yaml_path = root.parent.parent / "configs" / "isa_class_0116.yaml"
|
||||
yaml_path.write_text(
|
||||
f"""# ISA 交通标志分类 (Ultralytics classify)
|
||||
path: {root.resolve()}
|
||||
train: train
|
||||
val: val
|
||||
test: test
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return {"val_total": len(val_items), "hardlink": linked, "move": moved, "yaml": str(yaml_path)}
|
||||
|
||||
|
||||
def verify_yolo_detect(root: Path) -> str:
|
||||
for sp in ("train", "val"):
|
||||
img_d = root / "images" / sp
|
||||
lab_d = root / "labels" / sp
|
||||
if not img_d.is_dir() or not lab_d.is_dir():
|
||||
return f"missing images|labels/{sp}"
|
||||
if not any(img_d.iterdir()):
|
||||
return f"empty images/{sp}"
|
||||
if not any(lab_d.glob("*.txt")):
|
||||
return f"empty labels/{sp}"
|
||||
return "ok"
|
||||
|
||||
|
||||
def verify_yolo_pose(root: Path) -> str:
|
||||
msg = verify_yolo_detect(root)
|
||||
if msg != "ok":
|
||||
return msg
|
||||
sample = next((root / "labels" / "train").glob("*.txt"), None)
|
||||
if sample and len(sample.read_text().split()) < 6:
|
||||
return "pose label fields < 6"
|
||||
return "ok"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--gyp", type=Path, default=Path(__file__).resolve().parents[1] / "gyp")
|
||||
p.add_argument("--val-ratio", type=float, default=0.1)
|
||||
p.add_argument("--seed", type=int, default=42)
|
||||
p.add_argument("--copy-images", action="store_true")
|
||||
p.add_argument("--only", choices=("dam", "classify", "verify", "all"), default="all")
|
||||
args = p.parse_args()
|
||||
gyp = args.gyp.resolve()
|
||||
|
||||
if args.only in ("dam", "all"):
|
||||
print("=" * 60)
|
||||
print("1) dam_src_0417 VOC -> YOLO => gyp/dam_0417/")
|
||||
src = gyp / "dam_src_0417" / "src_data_0417_pick"
|
||||
dst = gyp / "dam_0417"
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
r = convert_dam_voc(src, dst, args.val_ratio, args.seed, link_images=not args.copy_images)
|
||||
print(r)
|
||||
|
||||
if args.only in ("classify", "all"):
|
||||
print("\n" + "=" * 60)
|
||||
print("2) isa_class_0116 分类 -> train/val/test")
|
||||
r2 = convert_classify_layout(gyp / "isa_class_0116", args.val_ratio, args.seed)
|
||||
print(r2)
|
||||
|
||||
if args.only in ("verify", "all"):
|
||||
print("\n" + "=" * 60)
|
||||
print("3) 校验")
|
||||
for name in ["ddaw_1124", "addw_0523", "isa_detect", "dam_0516", "dam_0417"]:
|
||||
root = gyp / name
|
||||
if root.is_dir():
|
||||
print(f" {name}: {verify_yolo_detect(root)}")
|
||||
print(f" yoloface-0726: {verify_yolo_pose(gyp / 'yoloface-0726')}")
|
||||
ic = gyp / "isa_class_0116"
|
||||
if (ic / "val").is_dir():
|
||||
print(f" isa_class_0116: train/val/test ok")
|
||||
|
||||
print("\n完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
538
datasets/dms/scripts/ingest_incremental.py
Normal file
538
datasets/dms/scripts/ingest_incremental.py
Normal file
@@ -0,0 +1,538 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
接入新数据:合并进 dataset_bundle/<task>/,默认按类分层重划 train/val。
|
||||
|
||||
叠放新批次(推荐):
|
||||
.../dam/sources/20260520_line2/ # images+labels 或 images/train+labels/train
|
||||
python ml.py build dms dam --all-sources
|
||||
|
||||
inbox 方式仍可用:
|
||||
python ml.py add dms dam --src /path/to/batch
|
||||
python ml.py build dms dam --batch <name>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
DATASET_ROOT = SCRIPT_DIR.parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from pack_registry import task_data_root as pack_task_data_root # noqa: E402
|
||||
from convert_to_yolo import convert_dam_voc # noqa: E402
|
||||
from stratified_split import ( # noqa: E402
|
||||
apply_yolo_split,
|
||||
collect_yolo_samples,
|
||||
print_yolo_stats,
|
||||
resplit_classify_root,
|
||||
stratified_assign,
|
||||
)
|
||||
|
||||
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".JPG", ".JPEG", ".PNG"}
|
||||
|
||||
|
||||
def load_registry(root: Path) -> dict:
|
||||
return yaml.safe_load((root / "datasets.registry.yaml").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def split_kwargs(reg: dict, args: argparse.Namespace) -> dict:
|
||||
s = reg.get("split") or {}
|
||||
return {
|
||||
"val_ratio": args.val_ratio if args.val_ratio is not None else float(s.get("val_ratio", 0.1)),
|
||||
"seed": args.seed if args.seed is not None else int(s.get("seed", 42)),
|
||||
"min_val_per_class": int(s.get("min_val_per_class", 1)),
|
||||
"min_train_per_class": int(s.get("min_train_per_class", 1)),
|
||||
"rare_class_train_floor": int(s.get("rare_class_train_floor", 5)),
|
||||
}
|
||||
|
||||
|
||||
def sources_dir(data_root: Path, reg: dict) -> Path:
|
||||
sub = (reg.get("ingest") or {}).get("sources_subdir", "sources")
|
||||
return data_root / sub
|
||||
|
||||
|
||||
def ingested_dir(data_root: Path, reg: dict) -> Path:
|
||||
rel = (reg.get("ingest") or {}).get("ingested_subdir", "sources/_ingested")
|
||||
return (data_root / rel).resolve()
|
||||
|
||||
|
||||
def list_pending_sources(data_root: Path, reg: dict) -> list[Path]:
|
||||
src_root = sources_dir(data_root, reg)
|
||||
if not src_root.is_dir():
|
||||
return []
|
||||
ing = ingested_dir(data_root, reg)
|
||||
skip = {ing.name, "_ingested", "_merged"}
|
||||
return sorted(
|
||||
p
|
||||
for p in src_root.iterdir()
|
||||
if p.is_dir() and p.name not in skip and not p.name.startswith(".")
|
||||
)
|
||||
|
||||
|
||||
def archive_source_batch(src: Path, data_root: Path, reg: dict, dry_run: bool) -> str | None:
|
||||
"""若 src 在 sources/ 下,合并后移到 sources/_ingested/。"""
|
||||
src_root = sources_dir(data_root, reg).resolve()
|
||||
try:
|
||||
src.resolve().relative_to(src_root)
|
||||
except ValueError:
|
||||
return None
|
||||
dst_base = ingested_dir(data_root, reg)
|
||||
dst = dst_base / src.name
|
||||
if dry_run:
|
||||
return str(dst)
|
||||
dst_base.mkdir(parents=True, exist_ok=True)
|
||||
if dst.exists():
|
||||
dst = dst_base / f"{src.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
shutil.move(str(src), str(dst))
|
||||
return str(dst)
|
||||
|
||||
|
||||
def run_stratified_resplit(
|
||||
data_root: Path,
|
||||
tcfg: dict,
|
||||
sk: dict,
|
||||
dry_run: bool = False,
|
||||
) -> dict | None:
|
||||
if dry_run:
|
||||
return None
|
||||
if tcfg["type"] in ("detect", "pose"):
|
||||
samples = collect_yolo_samples(data_root, ("train", "val"))
|
||||
assign = stratified_assign(samples, **sk)
|
||||
apply_yolo_split(data_root, assign, pool_splits=("train", "val"), dry_run=False)
|
||||
print_yolo_stats(data_root, "after stratified resplit (per-class)")
|
||||
return {
|
||||
"train": sum(1 for v in assign.values() if v == "train"),
|
||||
"val": sum(1 for v in assign.values() if v == "val"),
|
||||
}
|
||||
if tcfg["type"] == "classify":
|
||||
return resplit_classify_root(data_root, dry_run=False, **sk)
|
||||
return None
|
||||
|
||||
|
||||
def find_image(images_dirs: list[Path], stem: str) -> Path | None:
|
||||
for d in images_dirs:
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for ext in IMG_EXTS:
|
||||
p = d / f"{stem}{ext}"
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def resolve_yolo_layout(src: Path) -> tuple[list[Path], list[Path], bool]:
|
||||
if (src / "images" / "train").is_dir():
|
||||
return [src / "images" / "train"], [src / "labels" / "train"], False
|
||||
if (src / "images").is_dir() and (src / "labels").is_dir():
|
||||
voc = any((src / "labels").glob("*.xml"))
|
||||
return [src / "images"], [src / "labels"], voc
|
||||
raise SystemExit(
|
||||
f"无法识别 YOLO 目录: {src}\n"
|
||||
"需要 images/train+labels/train 或 images+labels"
|
||||
)
|
||||
|
||||
|
||||
def resolve_classify_layout(src: Path) -> Path:
|
||||
if (src / "train").is_dir() and any((src / "train").iterdir()):
|
||||
return src / "train"
|
||||
if any(d.is_dir() for d in src.iterdir()):
|
||||
return src
|
||||
raise SystemExit(f"无法识别分类目录: {src}\n需要 train/类名/*.jpg 或 类名/*.jpg")
|
||||
|
||||
|
||||
def file_md5(p: Path) -> str:
|
||||
h = hashlib.md5()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def existing_yolo_index(gyp_root: Path) -> tuple[set[str], dict[str, str]]:
|
||||
"""stem -> md5 of label file(用于去重)。"""
|
||||
stems: set[str] = set()
|
||||
md5s: dict[str, str] = {}
|
||||
for sp in ("train", "val"):
|
||||
lab_d = gyp_root / "labels" / sp
|
||||
if not lab_d.is_dir():
|
||||
continue
|
||||
for lab in lab_d.glob("*.txt"):
|
||||
stems.add(lab.stem)
|
||||
md5s[lab.stem] = file_md5(lab)
|
||||
return stems, md5s
|
||||
|
||||
|
||||
def validate_detect_label(text: str, nc: int) -> str | None:
|
||||
for i, line in enumerate(text.splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 5:
|
||||
return f"line {i}: fields={len(parts)} < 5"
|
||||
cid = int(parts[0])
|
||||
if cid < 0 or cid >= nc:
|
||||
return f"line {i}: class {cid} not in [0,{nc - 1}]"
|
||||
return None
|
||||
|
||||
|
||||
def validate_pose_label(text: str, kpt_shape: list[int]) -> str | None:
|
||||
nk, nd = kpt_shape
|
||||
min_f = 5 + nk * nd
|
||||
for i, line in enumerate(text.splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
n = len(line.split())
|
||||
if n < min_f:
|
||||
return f"line {i}: pose fields={n} < {min_f} (kpt_shape={kpt_shape})"
|
||||
return None
|
||||
|
||||
|
||||
def validate_label(path: Path, tcfg: dict) -> str | None:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
typ = tcfg["type"]
|
||||
if typ == "detect":
|
||||
return validate_detect_label(text, int(tcfg["nc"]))
|
||||
if typ == "pose":
|
||||
return validate_pose_label(text, tcfg.get("kpt_shape", [37, 3]))
|
||||
return None
|
||||
|
||||
|
||||
def copy_pair(lab: Path, img: Path, dst_lab: Path, dst_img: Path, copy: bool) -> None:
|
||||
dst_lab.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst_img.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(lab, dst_lab)
|
||||
if copy:
|
||||
shutil.copy2(img, dst_img)
|
||||
else:
|
||||
if dst_img.exists() or dst_img.is_symlink():
|
||||
dst_img.unlink()
|
||||
dst_img.symlink_to(img.resolve())
|
||||
|
||||
|
||||
def ingest_yolo(
|
||||
task: str,
|
||||
tcfg: dict,
|
||||
data_root: Path,
|
||||
src: Path,
|
||||
sk: dict,
|
||||
to_split: str = "train",
|
||||
resplit: bool = False,
|
||||
dry_run: bool = False,
|
||||
copy: bool = False,
|
||||
dedup: str = "stem",
|
||||
) -> dict:
|
||||
img_dirs, lab_dirs, is_voc = resolve_yolo_layout(src)
|
||||
staging_parent = None
|
||||
|
||||
if is_voc:
|
||||
if tcfg["type"] != "detect":
|
||||
raise SystemExit("VOC xml 仅支持 detect 任务(dam / dam_0417)")
|
||||
staging = data_root.parent / "_staging_voc" / task
|
||||
staging_parent = staging.parent
|
||||
if staging.exists() and not dry_run:
|
||||
shutil.rmtree(staging)
|
||||
if not dry_run:
|
||||
convert_dam_voc(src, staging, val_ratio=0.0, seed=sk["seed"], link_images=not copy)
|
||||
img_dirs = [staging / "images" / "train"]
|
||||
lab_dirs = [staging / "labels" / "train"]
|
||||
|
||||
known_stems, known_md5 = existing_yolo_index(data_root)
|
||||
dst_img = data_root / "images" / to_split
|
||||
dst_lab = data_root / "labels" / to_split
|
||||
|
||||
added, skipped_dup, skipped_bad, skipped_no_img = 0, 0, 0, 0
|
||||
bad_samples: list[str] = []
|
||||
|
||||
for lab in sorted(lab_dirs[0].glob("*.txt")):
|
||||
err = validate_label(lab, tcfg)
|
||||
if err:
|
||||
skipped_bad += 1
|
||||
if len(bad_samples) < 5:
|
||||
bad_samples.append(f"{lab.name}: {err}")
|
||||
continue
|
||||
|
||||
stem = lab.stem
|
||||
lab_md5 = file_md5(lab) if dedup == "md5" else None
|
||||
if stem in known_stems:
|
||||
if dedup == "md5" and known_md5.get(stem) != lab_md5:
|
||||
pass # 同名不同内容,仍跳过并计 dup;可改为告警
|
||||
skipped_dup += 1
|
||||
continue
|
||||
|
||||
img = find_image(img_dirs, stem)
|
||||
if img is None:
|
||||
skipped_no_img += 1
|
||||
continue
|
||||
|
||||
if not dry_run:
|
||||
copy_pair(lab, img, dst_lab / f"{stem}.txt", dst_img / img.name, copy)
|
||||
added += 1
|
||||
known_stems.add(stem)
|
||||
if lab_md5:
|
||||
known_md5[stem] = lab_md5
|
||||
|
||||
resplit_info = None
|
||||
if resplit and not dry_run and tcfg["type"] in ("detect", "pose"):
|
||||
resplit_info = run_stratified_resplit(data_root, tcfg, sk, dry_run=False)
|
||||
|
||||
if staging_parent and staging_parent.exists() and not dry_run:
|
||||
shutil.rmtree(staging_parent, ignore_errors=True)
|
||||
|
||||
return {
|
||||
"task": task,
|
||||
"type": tcfg["type"],
|
||||
"added": added,
|
||||
"skipped_dup": skipped_dup,
|
||||
"skipped_bad_label": skipped_bad,
|
||||
"skipped_no_img": skipped_no_img,
|
||||
"bad_samples": bad_samples,
|
||||
"to_split": to_split,
|
||||
"resplit": resplit_info,
|
||||
"dry_run": dry_run,
|
||||
}
|
||||
|
||||
|
||||
def existing_classify_names(gyp_root: Path, split: str) -> set[str]:
|
||||
d = gyp_root / split
|
||||
if not d.is_dir():
|
||||
return set()
|
||||
return {x.name for x in d.iterdir() if x.is_dir()}
|
||||
|
||||
|
||||
def ingest_classify(
|
||||
task: str,
|
||||
tcfg: dict,
|
||||
data_root: Path,
|
||||
src: Path,
|
||||
sk: dict,
|
||||
to_split: str = "train",
|
||||
resplit: bool = False,
|
||||
dry_run: bool = False,
|
||||
copy: bool = True,
|
||||
) -> dict:
|
||||
src_root = resolve_classify_layout(src)
|
||||
dst_root = data_root / to_split
|
||||
added, skipped_dup, new_classes = 0, 0, []
|
||||
|
||||
for cls_dir in sorted(d for d in src_root.iterdir() if d.is_dir()):
|
||||
dst_cls = dst_root / cls_dir.name
|
||||
if not dst_cls.exists() and not dry_run:
|
||||
new_classes.append(cls_dir.name)
|
||||
for img in cls_dir.iterdir():
|
||||
if not img.is_file() or img.suffix not in IMG_EXTS:
|
||||
continue
|
||||
dst = dst_cls / img.name
|
||||
if dst.exists():
|
||||
skipped_dup += 1
|
||||
continue
|
||||
if dry_run:
|
||||
added += 1
|
||||
continue
|
||||
dst_cls.mkdir(parents=True, exist_ok=True)
|
||||
if copy:
|
||||
shutil.copy2(img, dst)
|
||||
else:
|
||||
dst.symlink_to(img.resolve())
|
||||
added += 1
|
||||
|
||||
resplit_info = None
|
||||
if resplit and not dry_run:
|
||||
resplit_info = run_stratified_resplit(data_root, tcfg, sk, dry_run=False)
|
||||
|
||||
return {
|
||||
"task": task,
|
||||
"type": "classify",
|
||||
"added": added,
|
||||
"skipped_dup": skipped_dup,
|
||||
"new_classes": new_classes[:20],
|
||||
"new_class_count": len(new_classes),
|
||||
"to_split": to_split,
|
||||
"resplit": resplit_info,
|
||||
"dry_run": dry_run,
|
||||
}
|
||||
|
||||
|
||||
def append_log(root: Path, record: dict) -> None:
|
||||
log = root / "manifests" / "ingest_log.jsonl"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
record["ts"] = datetime.now(timezone.utc).isoformat()
|
||||
with log.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def run_refresh(root: Path) -> None:
|
||||
subprocess.run(
|
||||
[sys.executable, str(SCRIPT_DIR / "refresh_yaml.py"), "--root", str(root)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def ingest_one(
|
||||
root: Path,
|
||||
reg: dict,
|
||||
task: str,
|
||||
src: Path,
|
||||
args: argparse.Namespace,
|
||||
) -> dict:
|
||||
tcfg = reg["tasks"][task]
|
||||
pack = getattr(args, "pack", None) or "dms_v1"
|
||||
data_root = pack_task_data_root(root, pack, tcfg["task_dir"])
|
||||
sk = split_kwargs(reg, args)
|
||||
print(f"\n=== pack={pack} task={task} type={tcfg['type']} src={src} ===")
|
||||
print(
|
||||
f" split: val_ratio={sk['val_ratio']} min_train={sk['min_train_per_class']} "
|
||||
f"rare_floor={sk['rare_class_train_floor']} resplit={args.resplit}"
|
||||
)
|
||||
|
||||
if tcfg["type"] == "classify":
|
||||
result = ingest_classify(
|
||||
task, tcfg, data_root, src,
|
||||
sk=sk,
|
||||
to_split=args.to, resplit=args.resplit,
|
||||
dry_run=args.dry_run, copy=args.copy,
|
||||
)
|
||||
else:
|
||||
result = ingest_yolo(
|
||||
task, tcfg, data_root, src,
|
||||
sk=sk,
|
||||
to_split=args.to, resplit=args.resplit,
|
||||
dry_run=args.dry_run, copy=args.copy, dedup=args.dedup,
|
||||
)
|
||||
|
||||
if not args.dry_run:
|
||||
archived = archive_source_batch(src, data_root, reg, dry_run=False)
|
||||
if archived:
|
||||
result["archived_to"] = archived
|
||||
print(f" archived source -> {archived}")
|
||||
|
||||
print(result)
|
||||
return result
|
||||
|
||||
|
||||
def ingest_extra_train(root: Path, reg: dict, task: str, args: argparse.Namespace) -> list[dict]:
|
||||
tcfg = reg["tasks"][task]
|
||||
results = []
|
||||
for ep in tcfg.get("extra_train") or []:
|
||||
src = Path(ep)
|
||||
if not src.is_absolute():
|
||||
src = (root / ep).resolve()
|
||||
if not src.is_dir():
|
||||
print(f" skip extra_train (missing): {src}")
|
||||
continue
|
||||
results.append(ingest_one(root, reg, task, src, args))
|
||||
return results
|
||||
|
||||
|
||||
def ingest_all_sources(root: Path, reg: dict, task: str, args: argparse.Namespace) -> None:
|
||||
tcfg = reg["tasks"][task]
|
||||
pack = getattr(args, "pack", None) or "dms_v1"
|
||||
data_root = pack_task_data_root(root, pack, tcfg["task_dir"])
|
||||
batches = list_pending_sources(data_root, reg)
|
||||
if not batches:
|
||||
print(f" sources 为空: {sources_dir(data_root, reg)}")
|
||||
return
|
||||
print(f"\n>>> sources {task}: {len(batches)} batch(es)")
|
||||
for batch in batches:
|
||||
ingest_one(root, reg, task, batch, args)
|
||||
if not args.dry_run:
|
||||
append_log(root, {"src": str(batch), "task": task, "pack": pack, "via": "sources"})
|
||||
|
||||
|
||||
def ingest_all_inbox(root: Path, reg: dict, args: argparse.Namespace) -> None:
|
||||
for task, tcfg in reg["tasks"].items():
|
||||
inbox = root / tcfg.get("inbox", f"inbox/{task}")
|
||||
if not inbox.is_dir():
|
||||
continue
|
||||
batches = sorted(d for d in inbox.iterdir() if d.is_dir())
|
||||
if not batches:
|
||||
continue
|
||||
print(f"\n>>> inbox {task}: {len(batches)} batch(es)")
|
||||
for batch in batches:
|
||||
ingest_one(root, reg, task, batch, args)
|
||||
if not args.dry_run:
|
||||
append_log(root, {"src": str(batch), "task": task, "pack": pack, "via": "inbox"})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="DMS 全任务增量接入")
|
||||
p.add_argument("--task", help="registry 任务名;与 --all-inbox 二选一")
|
||||
p.add_argument("--src", type=Path, help="新数据目录")
|
||||
p.add_argument("--all-inbox", action="store_true", help="处理所有 inbox/<task>/* 批次")
|
||||
p.add_argument("--all-sources", action="store_true", help="处理任务 data/sources/* 下所有待合并批次")
|
||||
p.add_argument("--sync-extra", action="store_true", help="合并 registry.extra_train 中所有路径")
|
||||
p.add_argument("--to", default="train", choices=("train", "val"))
|
||||
p.add_argument("--no-resplit", action="store_true", help="跳过重划分(默认按 registry.split.resplit_after_ingest)")
|
||||
p.add_argument("--val-ratio", type=float, default=None)
|
||||
p.add_argument("--seed", type=int, default=None)
|
||||
p.add_argument("--copy", action="store_true")
|
||||
p.add_argument("--dedup", choices=("stem", "md5"), default="stem")
|
||||
p.add_argument("--dry-run", action="store_true")
|
||||
p.add_argument("--refresh", action="store_true", help="完成后运行 refresh_yaml.py")
|
||||
p.add_argument("--pack", default="dms_v1", help="写入的数据包名(见 data_packs.yaml)")
|
||||
p.add_argument("--root", type=Path, default=DATASET_ROOT)
|
||||
args = p.parse_args()
|
||||
|
||||
root = args.root.resolve()
|
||||
reg = load_registry(root)
|
||||
split_cfg = reg.get("split") or {}
|
||||
if args.val_ratio is None:
|
||||
args.val_ratio = float(split_cfg.get("val_ratio", 0.1))
|
||||
if args.seed is None:
|
||||
args.seed = int(split_cfg.get("seed", 42))
|
||||
if args.no_resplit:
|
||||
args.resplit = False
|
||||
else:
|
||||
args.resplit = bool(split_cfg.get("resplit_after_ingest", True))
|
||||
|
||||
if args.all_sources:
|
||||
if not args.task:
|
||||
raise SystemExit("--all-sources 需要 --task")
|
||||
ingest_all_sources(root, reg, args.task, args)
|
||||
if args.refresh and not args.dry_run:
|
||||
run_refresh(root)
|
||||
return
|
||||
|
||||
if args.all_inbox:
|
||||
ingest_all_inbox(root, reg, args)
|
||||
if args.refresh and not args.dry_run:
|
||||
run_refresh(root)
|
||||
return
|
||||
|
||||
if args.sync_extra:
|
||||
for task in reg["tasks"]:
|
||||
ingest_extra_train(root, reg, task, args)
|
||||
if args.refresh and not args.dry_run:
|
||||
run_refresh(root)
|
||||
return
|
||||
|
||||
if not args.task or not args.src:
|
||||
raise SystemExit("需要 --task + --src,或 --all-inbox / --all-sources,或 --sync-extra")
|
||||
|
||||
if args.task not in reg["tasks"]:
|
||||
raise SystemExit(f"未知 task: {args.task},可选: {list(reg['tasks'])}")
|
||||
|
||||
src = args.src.resolve()
|
||||
if not src.is_dir():
|
||||
raise SystemExit(f"源目录不存在: {src}")
|
||||
|
||||
result = ingest_one(root, reg, args.task, src, args)
|
||||
if not args.dry_run:
|
||||
append_log(root, {"src": str(src), "pack": pack, **result})
|
||||
if args.refresh:
|
||||
run_refresh(root)
|
||||
else:
|
||||
print("提示: 可运行 python scripts/refresh_yaml.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
138
datasets/dms/scripts/isa_preprocess.py
Executable file
138
datasets/dms/scripts/isa_preprocess.py
Executable file
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
import random
|
||||
import glob
|
||||
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from PIL import Image
|
||||
import cv2
|
||||
import csv
|
||||
from itertools import islice
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[0] # YOLOv5 root directory
|
||||
print("-------------------ROOT=",ROOT)
|
||||
|
||||
# test_rate=0.05
|
||||
|
||||
# src_data_path = "./isa_class/src_data"
|
||||
|
||||
def process_img(src_data_path, test_rate):
|
||||
|
||||
|
||||
dst_data_path = src_data_path.replace('src_data','dst_data')
|
||||
# if not os.path.exists(dst_data_path):
|
||||
# print("create dst_data = ", dst_data_path)
|
||||
# os.mkdir(dst_data_path)
|
||||
|
||||
list = glob.glob(os.path.join(src_data_path,"*/*.jpg"))
|
||||
print("len list = ",len(list))
|
||||
test_cout = int(test_rate*len(list))
|
||||
print("test_cout = ",test_cout)
|
||||
test_list=random.sample(list, test_cout)
|
||||
print("test_list len = ",len(test_list))
|
||||
|
||||
# print("test_list = ",test_list)
|
||||
|
||||
dst_train_path=os.path.join(dst_data_path,'train')
|
||||
dst_test_path=os.path.join(dst_data_path,'test')
|
||||
print("dst_train_path={} , dst_test_path={}".format(dst_train_path,dst_test_path))
|
||||
if not os.path.exists(dst_train_path):
|
||||
os.makedirs(dst_train_path)
|
||||
if not os.path.exists(dst_test_path):
|
||||
os.makedirs(dst_test_path)
|
||||
|
||||
class_name_list = []
|
||||
for x in glob.glob(os.path.join(src_data_path, '*')):
|
||||
class_name_list.append(os.path.split(x)[-1])
|
||||
print("class_name_list = ",class_name_list)
|
||||
|
||||
data_set_type = ['train','test']
|
||||
for class_name in class_name_list:
|
||||
for set in data_set_type:
|
||||
dst_class_dir = os.path.join(dst_data_path,set,class_name)
|
||||
if not os.path.exists(dst_class_dir):
|
||||
os.makedirs(dst_class_dir)
|
||||
print("create dst_class_dir = ",dst_class_dir)
|
||||
|
||||
for class_name in class_name_list:
|
||||
sub_src_path = os.path.join(src_data_path, class_name)
|
||||
img_list = glob.glob(os.path.join(sub_src_path,'*.jpg'))
|
||||
total_len = len(img_list)
|
||||
test_cout = int(test_rate*len(img_list))
|
||||
if test_cout < 1:
|
||||
test_cout = 1
|
||||
print("test_cout = ",test_cout)
|
||||
print("class_name = ",class_name)
|
||||
test_list=random.sample(img_list, test_cout)
|
||||
print("test_list len = ",len(test_list))
|
||||
for src_img in img_list:
|
||||
img_name = os.path.split(src_img)[-1]
|
||||
# for date_set in data_set_type:
|
||||
dst_train_img_path=os.path.join(dst_train_path, class_name, img_name)
|
||||
dst_test_img_path=os.path.join(dst_test_path, class_name, img_name)
|
||||
# if src_img in test_list:
|
||||
|
||||
# else:
|
||||
# dst_img_path=os.path.join(dst_train_path, class_name, img_name)
|
||||
|
||||
print("src_img={},dst_train_img_path={},dst__test_img_path={}".format(src_img, dst_train_img_path, dst_test_img_path))
|
||||
if src_img in test_list:
|
||||
shutil.copy(src_img, dst_test_img_path)
|
||||
if total_len < 2:
|
||||
shutil.copy(src_img, dst_train_img_path)
|
||||
else:
|
||||
shutil.copy(src_img, dst_train_img_path)
|
||||
|
||||
|
||||
def parse_opt():
|
||||
parser=argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--src-data',type=str,default=ROOT / "src_data",help='src dir')
|
||||
parser.add_argument('--test-rate',type=float,default=0.1,help='test_rate')
|
||||
# parser.add_argument('--src-test',type=str,default=ROOT / "src_test",help='src dir')
|
||||
parser.add_argument('--dst-data',type=str,default=ROOT / "dst_data",help='dst dir')
|
||||
# parser.add_argument('--prefix',type=str,default="dst",help='prefix name ')
|
||||
opt=parser.parse_args()
|
||||
|
||||
print("src_data=%s" % (opt.src_data))
|
||||
print("test_rate=%s" % (opt.test_rate))
|
||||
# print("src_test=%s" % (opt.src_test))
|
||||
print("dst_data=%s" % (opt.dst_data))
|
||||
# print("prefix=%s" % (opt.prefix))
|
||||
|
||||
return opt
|
||||
|
||||
# python isa_preprocess.py --src-data ./isa_class_tsrd_gtsrb_eureg_cctsdb_tt100k/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_tsrd_gtsrb_eureg_cctsdb_tt100k_speed/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1020/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1023/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1025/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1026/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1102/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1120/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1212/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1221/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1222/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1224/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_1229/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_0103/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_0104/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_0108/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_0112/src_data
|
||||
# python isa_preprocess.py --src-data ./isa_class_0116/src_data
|
||||
def main(opt):
|
||||
|
||||
if opt.src_data and opt.dst_data:
|
||||
# train_ppm_to_jpg(opt.src_train, opt.dst_data)
|
||||
process_img(opt.src_data, opt.test_rate)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
opt = parse_opt()
|
||||
main(opt)
|
||||
51
datasets/dms/scripts/pack_registry.py
Normal file
51
datasets/dms/scripts/pack_registry.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""DMS 多包:data_packs.yaml + ML/workflow.registry.yaml active_packs。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def load_packs_registry(root: Path) -> dict:
|
||||
p = root / "data_packs.yaml"
|
||||
if not p.is_file():
|
||||
raise SystemExit(f"缺少 {p}")
|
||||
return yaml.safe_load(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def ml_workflow_path(dataset_root: Path) -> Path:
|
||||
# workspace/DMS/DATASET -> DATA/HSAP/workflow.registry.yaml
|
||||
return dataset_root.resolve().parent.parent.parent / "ML" / "workflow.registry.yaml"
|
||||
|
||||
|
||||
def load_active_pack_names(dataset_root: Path, cli_packs: list[str] | None = None) -> list[str]:
|
||||
if cli_packs:
|
||||
return cli_packs
|
||||
wf_path = ml_workflow_path(dataset_root)
|
||||
if wf_path.is_file():
|
||||
wf = yaml.safe_load(wf_path.read_text(encoding="utf-8"))
|
||||
active = wf.get("projects", {}).get("dms", {}).get("active_packs")
|
||||
if active:
|
||||
return list(active)
|
||||
reg = load_packs_registry(dataset_root)
|
||||
return [reg["packs"][0]["name"]] if reg.get("packs") else []
|
||||
|
||||
|
||||
def resolve_pack_dir(root: Path, pack_name: str) -> Path:
|
||||
reg = load_packs_registry(root)
|
||||
name = reg.get("aliases", {}).get(pack_name, pack_name)
|
||||
for item in reg.get("packs", []):
|
||||
if item.get("name") == name:
|
||||
return (root / item["path"]).resolve()
|
||||
candidate = root / name
|
||||
if candidate.is_dir():
|
||||
return candidate.resolve()
|
||||
raise SystemExit(f"未知数据包: {pack_name},已登记: {[p['name'] for p in reg.get('packs', [])]}")
|
||||
|
||||
|
||||
def task_data_root(dataset_root: Path, pack_name: str, task_dir: str) -> Path:
|
||||
return resolve_pack_dir(dataset_root, pack_name) / task_dir
|
||||
|
||||
|
||||
def list_registered_packs(root: Path) -> list[dict]:
|
||||
return load_packs_registry(root).get("packs", [])
|
||||
131
datasets/dms/scripts/refresh_yaml.py
Normal file
131
datasets/dms/scripts/refresh_yaml.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""按 workflow active_packs 生成 manifests/yaml_active/*.yaml(可多包合并 train/val)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from pack_registry import ( # noqa: E402
|
||||
load_active_pack_names,
|
||||
resolve_pack_dir,
|
||||
)
|
||||
|
||||
|
||||
def fmt_names(names) -> str:
|
||||
if isinstance(names, dict):
|
||||
lines = ["names:"]
|
||||
for k, v in sorted(names.items(), key=lambda x: int(x[0])):
|
||||
lines.append(f" {k}: {v}")
|
||||
return "\n".join(lines)
|
||||
inner = ", ".join(f'"{n}"' for n in names)
|
||||
return f"names: [{inner}]"
|
||||
|
||||
|
||||
def yaml_list(key: str, paths: list[str]) -> str:
|
||||
if len(paths) == 1:
|
||||
return f"{key}: {paths[0]}"
|
||||
lines = [f"{key}:"] + [f" - {p}" for p in paths]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def pack_task_root(root: Path, pack_name: str, task_dir: str) -> Path:
|
||||
return resolve_pack_dir(root, pack_name) / task_dir
|
||||
|
||||
|
||||
def build_detect_pose_yaml(
|
||||
task: str,
|
||||
tcfg: dict,
|
||||
root: Path,
|
||||
pack_names: list[str],
|
||||
typ: str,
|
||||
) -> str:
|
||||
task_dir = tcfg["task_dir"]
|
||||
bases = []
|
||||
train_paths = []
|
||||
val_paths = []
|
||||
for pack in pack_names:
|
||||
base = pack_task_root(root, pack, task_dir)
|
||||
if not base.is_dir():
|
||||
print(f" skip pack {pack}: missing {base}")
|
||||
continue
|
||||
bases.append(base)
|
||||
train_paths.append(str((base / "images" / "train").resolve()))
|
||||
val_paths.append(str((base / "images" / "val").resolve()))
|
||||
|
||||
if not bases:
|
||||
raise SystemExit(f"{task}: 无可用数据包目录")
|
||||
|
||||
lines = [
|
||||
f"# {task} — packs: {', '.join(pack_names)}",
|
||||
f"path: {bases[0]}",
|
||||
yaml_list("train", train_paths),
|
||||
yaml_list("val", val_paths),
|
||||
"",
|
||||
]
|
||||
if typ == "pose":
|
||||
lines.insert(4, f"kpt_shape: {tcfg.get('kpt_shape', [37, 3])}")
|
||||
else:
|
||||
lines.extend([f"nc: {tcfg['nc']}", fmt_names(tcfg["names"]), ""])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_classify_yaml(task: str, tcfg: dict, root: Path, pack_names: list[str]) -> str:
|
||||
task_dir = tcfg["task_dir"]
|
||||
if len(pack_names) > 1:
|
||||
print(f" warn {task}: classify 暂用首个包 {pack_names[0]}(多包请先合并目录)")
|
||||
base = pack_task_root(root, pack_names[0], task_dir)
|
||||
return f"""# {task} — pack: {pack_names[0]}
|
||||
path: {base.resolve()}
|
||||
train: train
|
||||
val: val
|
||||
test: test
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--root", type=Path, default=SCRIPT_DIR.parent)
|
||||
p.add_argument("--packs", help="逗号分隔,覆盖 workflow active_packs")
|
||||
p.add_argument("--task", help="只生成某一任务")
|
||||
args = p.parse_args()
|
||||
root = args.root.resolve()
|
||||
reg = yaml.safe_load((root / "datasets.registry.yaml").read_text(encoding="utf-8"))
|
||||
cli = [x.strip() for x in args.packs.split(",")] if args.packs else None
|
||||
pack_names = load_active_pack_names(root, cli)
|
||||
if not pack_names:
|
||||
raise SystemExit("active_packs 为空,请编辑 ML/workflow.registry.yaml 或 --packs")
|
||||
|
||||
out_dir = root / "manifests" / "yaml_active"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"active_packs: {pack_names}")
|
||||
|
||||
tasks = reg["tasks"]
|
||||
if args.task:
|
||||
if args.task not in tasks:
|
||||
raise SystemExit(f"未知 task: {args.task}")
|
||||
tasks = {args.task: tasks[args.task]}
|
||||
|
||||
for task, tcfg in tasks.items():
|
||||
typ = tcfg["type"]
|
||||
if typ in ("detect", "pose"):
|
||||
content = build_detect_pose_yaml(task, tcfg, root, pack_names, typ)
|
||||
elif typ == "classify":
|
||||
content = build_classify_yaml(task, tcfg, root, pack_names)
|
||||
else:
|
||||
print(f" skip {task}: type {typ}")
|
||||
continue
|
||||
out = out_dir / f"{task}.yaml"
|
||||
out.write_text(content, encoding="utf-8")
|
||||
print(f" wrote {out.relative_to(root)}")
|
||||
|
||||
print("完成。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
504
datasets/dms/scripts/stratified_split.py
Normal file
504
datasets/dms/scripts/stratified_split.py
Normal file
@@ -0,0 +1,504 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
按类别分层划分数据集,避免仅按总量随机切分导致 train/val 类别比例失衡。
|
||||
|
||||
YOLO 检测:先按「图像所含类别中最稀有类」决定归属,再对各类别依次划分 val。
|
||||
分类(文件夹按类):每个类别目录内独立划分 train/val(或 train/test)。
|
||||
|
||||
用法示例:
|
||||
# 预览 DDAW 重划分效果(合并现有 train+val 后重分)
|
||||
python stratified_split.py yolo --root ../gyp/ddaw_1124 --val-ratio 0.1 --dry-run
|
||||
|
||||
# 执行划分(会移动 images/labels 下文件)
|
||||
python stratified_split.py yolo --root ../gyp/ddaw_1124 --val-ratio 0.1 --seed 42
|
||||
|
||||
# 分类数据:从 train 按类划出 val
|
||||
python stratified_split.py classify --root ../gyp/isa_class_0116 --val-ratio 0.1 --dry-run
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import shutil
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".JPG", ".JPEG", ".PNG"}
|
||||
|
||||
|
||||
def _read_yolo_classes(label_path: Path) -> set[int]:
|
||||
if not label_path.is_file():
|
||||
return set()
|
||||
classes: set[int] = set()
|
||||
for line in label_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
classes.add(int(line.split()[0]))
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return classes
|
||||
|
||||
|
||||
def _find_image(images_dir: Path, stem: str) -> Path | None:
|
||||
for ext in IMG_EXTS:
|
||||
p = images_dir / f"{stem}{ext}"
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def collect_yolo_samples(root: Path, splits: tuple[str, ...]) -> list[tuple[str, set[int]]]:
|
||||
samples: list[tuple[str, set[int]]] = []
|
||||
seen: set[str] = set()
|
||||
for split in splits:
|
||||
labels_dir = root / "labels" / split
|
||||
if not labels_dir.is_dir():
|
||||
continue
|
||||
for label_path in labels_dir.glob("*.txt"):
|
||||
stem = label_path.stem
|
||||
if stem in seen:
|
||||
continue
|
||||
seen.add(stem)
|
||||
classes = _read_yolo_classes(label_path)
|
||||
samples.append((stem, classes))
|
||||
return samples
|
||||
|
||||
|
||||
def val_count_for_class(
|
||||
n: int,
|
||||
val_ratio: float,
|
||||
min_val_per_class: int,
|
||||
min_train_per_class: int,
|
||||
rare_class_train_floor: int,
|
||||
) -> int:
|
||||
"""该类未分配样本数为 n 时,划入 val 的数量(其余进 train)。"""
|
||||
if n <= 0:
|
||||
return 0
|
||||
if n <= rare_class_train_floor:
|
||||
if n <= min_train_per_class:
|
||||
return 0
|
||||
return min(min_val_per_class, n - min_train_per_class)
|
||||
n_val = int(round(n * val_ratio))
|
||||
if min_val_per_class > 0:
|
||||
n_val = max(min_val_per_class, n_val)
|
||||
if min_train_per_class > 0 and n > min_train_per_class:
|
||||
n_val = min(n_val, n - min_train_per_class)
|
||||
return max(0, min(n_val, n))
|
||||
|
||||
|
||||
def stratified_assign(
|
||||
samples: list[tuple[str, set[int]]],
|
||||
val_ratio: float,
|
||||
seed: int,
|
||||
min_val_per_class: int = 1,
|
||||
min_train_per_class: int = 1,
|
||||
rare_class_train_floor: int = 5,
|
||||
) -> dict[str, str]:
|
||||
"""按类别分层:从稀有类到常见类,为含该类的未分配图像划分 train/val。"""
|
||||
rng = random.Random(seed)
|
||||
class_to_stems: dict[int, list[str]] = defaultdict(list)
|
||||
stem_to_classes: dict[str, set[int]] = {}
|
||||
|
||||
for stem, classes in samples:
|
||||
stem_to_classes[stem] = classes
|
||||
for c in classes:
|
||||
class_to_stems[c].append(stem)
|
||||
|
||||
no_label = [s for s, c in samples if not c]
|
||||
assignment: dict[str, str] = {}
|
||||
|
||||
classes_sorted = sorted(class_to_stems.keys(), key=lambda c: len(set(class_to_stems[c])))
|
||||
|
||||
for c in classes_sorted:
|
||||
stems = list(dict.fromkeys(class_to_stems[c]))
|
||||
unassigned = [s for s in stems if s not in assignment]
|
||||
if not unassigned:
|
||||
continue
|
||||
rng.shuffle(unassigned)
|
||||
n = len(unassigned)
|
||||
n_val = val_count_for_class(
|
||||
n, val_ratio, min_val_per_class, min_train_per_class, rare_class_train_floor,
|
||||
)
|
||||
for s in unassigned[:n_val]:
|
||||
assignment[s] = "val"
|
||||
for s in unassigned[n_val:]:
|
||||
assignment[s] = "train"
|
||||
|
||||
for stem in no_label:
|
||||
assignment.setdefault(stem, "train")
|
||||
|
||||
for stem, _ in samples:
|
||||
assignment.setdefault(stem, "train")
|
||||
|
||||
return assignment
|
||||
|
||||
|
||||
def yolo_class_stats(root: Path, split: str) -> tuple[int, Counter, Counter]:
|
||||
labels_dir = root / "labels" / split
|
||||
if not labels_dir.is_dir():
|
||||
return 0, Counter(), Counter()
|
||||
inst = Counter()
|
||||
imgs = Counter()
|
||||
n_img = 0
|
||||
for label_path in labels_dir.glob("*.txt"):
|
||||
n_img += 1
|
||||
cls_in_img: set[int] = set()
|
||||
for line in label_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
c = int(line.split()[0])
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
inst[c] += 1
|
||||
cls_in_img.add(c)
|
||||
for c in cls_in_img:
|
||||
imgs[c] += 1
|
||||
return n_img, inst, imgs
|
||||
|
||||
|
||||
def print_yolo_stats(root: Path, title: str) -> None:
|
||||
print(f"\n=== {title} ===")
|
||||
for split in ("train", "val"):
|
||||
n_img, inst, imgs = yolo_class_stats(root, split)
|
||||
if n_img == 0:
|
||||
continue
|
||||
print(f" [{split}] {n_img} images")
|
||||
all_cls = sorted(set(inst) | set(imgs))
|
||||
for c in all_cls:
|
||||
ratio = imgs[c] / n_img * 100 if n_img else 0
|
||||
print(
|
||||
f" cls {c}: instances={inst[c]}, images={imgs[c]} "
|
||||
f"({imgs[c]}/{n_img}={ratio:.1f}% of split images)"
|
||||
)
|
||||
|
||||
|
||||
def apply_yolo_split(
|
||||
root: Path,
|
||||
assignment: dict[str, str],
|
||||
pool_splits: tuple[str, ...] = ("train", "val"),
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""根据 assignment 将图像与标签移动到 images/{train,val}、labels/{train,val}。"""
|
||||
for split in ("train", "val"):
|
||||
(root / "images" / split).mkdir(parents=True, exist_ok=True)
|
||||
(root / "labels" / split).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# stem -> (image_path, label_path)
|
||||
located: dict[str, tuple[Path | None, Path | None]] = {}
|
||||
for split in pool_splits:
|
||||
labels_dir = root / "labels" / split
|
||||
images_dir = root / "images" / split
|
||||
if not labels_dir.is_dir():
|
||||
continue
|
||||
for label_path in labels_dir.glob("*.txt"):
|
||||
stem = label_path.stem
|
||||
if stem in located:
|
||||
continue
|
||||
img = _find_image(images_dir, stem) if images_dir.is_dir() else None
|
||||
located[stem] = (img, label_path)
|
||||
|
||||
moves: list[tuple[Path, Path]] = []
|
||||
for stem, target_split in assignment.items():
|
||||
img_src, lab_src = located.get(stem, (None, None))
|
||||
if lab_src is None:
|
||||
continue
|
||||
lab_dst = root / "labels" / target_split / lab_src.name
|
||||
if lab_src.resolve() != lab_dst.resolve():
|
||||
moves.append((lab_src, lab_dst))
|
||||
if img_src is not None:
|
||||
img_dst = root / "images" / target_split / img_src.name
|
||||
if img_src.resolve() != img_dst.resolve():
|
||||
moves.append((img_src, img_dst))
|
||||
|
||||
print(f" planned moves: {len(moves)}")
|
||||
if dry_run:
|
||||
return
|
||||
for src, dst in moves:
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dst.exists():
|
||||
dst.unlink()
|
||||
shutil.move(str(src), str(dst))
|
||||
|
||||
|
||||
def cmd_yolo(args: argparse.Namespace) -> None:
|
||||
root = Path(args.root).resolve()
|
||||
if not (root / "images").is_dir():
|
||||
raise SystemExit(f"not a YOLO dataset root (missing images/): {root}")
|
||||
|
||||
pool_splits = tuple(s.strip() for s in args.pool_splits.split(","))
|
||||
samples = collect_yolo_samples(root, pool_splits)
|
||||
print(f"pool: {root} samples={len(samples)} val_ratio={args.val_ratio} seed={args.seed}")
|
||||
|
||||
print_yolo_stats(root, "before")
|
||||
assignment = stratified_assign(
|
||||
samples,
|
||||
val_ratio=args.val_ratio,
|
||||
seed=args.seed,
|
||||
min_val_per_class=args.min_val_per_class,
|
||||
min_train_per_class=args.min_train_per_class,
|
||||
rare_class_train_floor=args.rare_class_train_floor,
|
||||
)
|
||||
n_val = sum(1 for v in assignment.values() if v == "val")
|
||||
print(f"\nplanned: train={len(assignment) - n_val} val={n_val}")
|
||||
|
||||
# 模拟统计(不写盘)
|
||||
if args.dry_run:
|
||||
tmp_counts: dict[str, Counter] = {"train": Counter(), "val": Counter()}
|
||||
tmp_imgs: dict[str, Counter] = {"train": Counter(), "val": Counter()}
|
||||
for stem, split in assignment.items():
|
||||
for split_name in pool_splits:
|
||||
lab = root / "labels" / split_name / f"{stem}.txt"
|
||||
if lab.is_file():
|
||||
classes = _read_yolo_classes(lab)
|
||||
break
|
||||
else:
|
||||
classes = set()
|
||||
for c in classes:
|
||||
tmp_imgs[split][c] += 1
|
||||
for split_name in pool_splits:
|
||||
lab = root / "labels" / split_name / f"{stem}.txt"
|
||||
if not lab.is_file():
|
||||
continue
|
||||
for line in lab.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
if line.strip():
|
||||
try:
|
||||
tmp_counts[split][int(line.split()[0])] += 1
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
break
|
||||
print("\n=== after (simulated) ===")
|
||||
for sp in ("train", "val"):
|
||||
n = sum(1 for v in assignment.values() if v == sp)
|
||||
print(f" [{sp}] {n} images")
|
||||
for c in sorted(set(tmp_counts[sp]) | set(tmp_imgs[sp])):
|
||||
print(f" cls {c}: instances={tmp_counts[sp][c]}, images={tmp_imgs[sp][c]}")
|
||||
print("\n=== per-class val ratio (images with class / all images with class) ===")
|
||||
print(f" {'cls':>4} {'before':>8} {'after':>8} {'target':>8}")
|
||||
before_val: Counter[int] = Counter()
|
||||
before_tot: Counter[int] = Counter()
|
||||
for split in pool_splits:
|
||||
_, _, imgs = yolo_class_stats(root, split)
|
||||
if split == "val":
|
||||
before_val.update(imgs)
|
||||
before_tot.update(imgs)
|
||||
after_tot = Counter()
|
||||
after_val = Counter()
|
||||
for stem, split in assignment.items():
|
||||
for split_name in pool_splits:
|
||||
lab = root / "labels" / split_name / f"{stem}.txt"
|
||||
if lab.is_file():
|
||||
classes = _read_yolo_classes(lab)
|
||||
break
|
||||
else:
|
||||
classes = set()
|
||||
for c in classes:
|
||||
after_tot[c] += 1
|
||||
if split == "val":
|
||||
after_val[c] += 1
|
||||
for c in sorted(set(before_tot) | set(after_tot)):
|
||||
b = before_val[c] / before_tot[c] * 100 if before_tot[c] else 0
|
||||
a = after_val[c] / after_tot[c] * 100 if after_tot[c] else 0
|
||||
print(f" {c:4d} {b:7.1f}% {a:7.1f}% {args.val_ratio * 100:7.1f}%")
|
||||
return
|
||||
|
||||
apply_yolo_split(root, assignment, pool_splits=pool_splits, dry_run=False)
|
||||
print_yolo_stats(root, "after")
|
||||
|
||||
|
||||
def stratified_assign_classify(
|
||||
class_dirs: list[Path],
|
||||
val_ratio: float,
|
||||
seed: int,
|
||||
min_val_per_class: int,
|
||||
min_train_per_class: int,
|
||||
rare_class_train_floor: int,
|
||||
) -> dict[Path, str]:
|
||||
"""每个类别目录内独立划分。"""
|
||||
rng = random.Random(seed)
|
||||
assignment: dict[Path, str] = {}
|
||||
for class_dir in sorted(class_dirs):
|
||||
files = [p for p in class_dir.iterdir() if p.is_file() and p.suffix in IMG_EXTS]
|
||||
rng.shuffle(files)
|
||||
n = len(files)
|
||||
if n == 0:
|
||||
continue
|
||||
n_val = val_count_for_class(
|
||||
n, val_ratio, min_val_per_class, min_train_per_class, rare_class_train_floor,
|
||||
)
|
||||
for p in files[:n_val]:
|
||||
assignment[p] = "val"
|
||||
for p in files[n_val:]:
|
||||
assignment[p] = "train"
|
||||
return assignment
|
||||
|
||||
|
||||
def resplit_classify_root(
|
||||
root: Path,
|
||||
val_ratio: float = 0.1,
|
||||
seed: int = 42,
|
||||
min_val_per_class: int = 1,
|
||||
min_train_per_class: int = 1,
|
||||
rare_class_train_floor: int = 5,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, int]:
|
||||
"""合并 train+val 按类重分 val,保留 test 不动。"""
|
||||
pooled: dict[str, list[Path]] = defaultdict(list)
|
||||
for split in ("train", "val"):
|
||||
sp = root / split
|
||||
if not sp.is_dir():
|
||||
continue
|
||||
for cls_dir in sp.iterdir():
|
||||
if not cls_dir.is_dir():
|
||||
continue
|
||||
for f in cls_dir.iterdir():
|
||||
if f.is_file() and f.suffix in IMG_EXTS:
|
||||
pooled[cls_dir.name].append(f)
|
||||
|
||||
staging = root / "_resplit_staging"
|
||||
if staging.exists() and not dry_run:
|
||||
shutil.rmtree(staging)
|
||||
|
||||
staged_dirs: list[Path] = []
|
||||
for cls, files in sorted(pooled.items()):
|
||||
seen: dict[str, Path] = {}
|
||||
for f in files:
|
||||
seen[f.name] = f
|
||||
if not seen:
|
||||
continue
|
||||
cls_staging = staging / cls
|
||||
if not dry_run:
|
||||
cls_staging.mkdir(parents=True, exist_ok=True)
|
||||
for name, f in seen.items():
|
||||
dst = cls_staging / name
|
||||
if dry_run:
|
||||
staged_dirs.append(cls_staging)
|
||||
continue
|
||||
if f.resolve() != dst.resolve():
|
||||
shutil.move(str(f), str(dst))
|
||||
if not dry_run:
|
||||
staged_dirs.append(cls_staging)
|
||||
|
||||
if dry_run:
|
||||
n_tr = n_va = 0
|
||||
for cls, files in pooled.items():
|
||||
n = len({f.name for f in files})
|
||||
n_val = val_count_for_class(
|
||||
n, val_ratio, min_val_per_class, min_train_per_class, rare_class_train_floor,
|
||||
)
|
||||
n_va += n_val
|
||||
n_tr += n - n_val
|
||||
return {"train": n_tr, "val": n_va, "dry_run": True}
|
||||
|
||||
assignment = stratified_assign_classify(
|
||||
staged_dirs, val_ratio, seed, min_val_per_class, min_train_per_class, rare_class_train_floor,
|
||||
)
|
||||
(root / "train").mkdir(exist_ok=True)
|
||||
(root / "val").mkdir(exist_ok=True)
|
||||
n_val = 0
|
||||
for src_path, sp in assignment.items():
|
||||
dst = root / sp / src_path.parent.name / src_path.name
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dst.exists():
|
||||
dst.unlink()
|
||||
shutil.move(str(src_path), str(dst))
|
||||
if sp == "val":
|
||||
n_val += 1
|
||||
if staging.exists():
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
n_train = sum(len(list((root / "train" / c).iterdir())) for c in pooled if (root / "train" / c).is_dir())
|
||||
return {"train": n_train, "val": n_val}
|
||||
|
||||
|
||||
def cmd_classify(args: argparse.Namespace) -> None:
|
||||
root = Path(args.root).resolve()
|
||||
src_split = args.src_split
|
||||
src_dir = root / src_split
|
||||
if not src_dir.is_dir():
|
||||
raise SystemExit(f"missing source split dir: {src_dir}")
|
||||
|
||||
class_dirs = [d for d in src_dir.iterdir() if d.is_dir()]
|
||||
files_all = [p for d in class_dirs for p in d.iterdir() if p.is_file() and p.suffix in IMG_EXTS]
|
||||
print(f"classify: {root} classes={len(class_dirs)} images={len(files_all)}")
|
||||
|
||||
assignment = stratified_assign_classify(
|
||||
class_dirs,
|
||||
args.val_ratio,
|
||||
args.seed,
|
||||
args.min_val_per_class,
|
||||
args.min_train_per_class,
|
||||
args.rare_class_train_floor,
|
||||
)
|
||||
n_val = sum(1 for v in assignment.values() if v == "val")
|
||||
print(f"planned: train={len(assignment) - n_val} val={n_val}")
|
||||
|
||||
if args.dry_run:
|
||||
per_cls: dict[str, Counter] = {"train": Counter(), "val": Counter()}
|
||||
for path, sp in assignment.items():
|
||||
per_cls[sp][path.parent.name] += 1
|
||||
print("\n=== per-class counts (simulated) ===")
|
||||
for cls_name in sorted({p.parent.name for p in assignment}):
|
||||
tr = per_cls["train"][cls_name]
|
||||
va = per_cls["val"][cls_name]
|
||||
tot = tr + va
|
||||
pct = va / tot * 100 if tot else 0
|
||||
print(f" {cls_name}: train={tr} val={va} (val%={pct:.1f})")
|
||||
return
|
||||
|
||||
for target in ("train", "val"):
|
||||
(root / target).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
moves = 0
|
||||
for src_path, target_split in assignment.items():
|
||||
cls_name = src_path.parent.name
|
||||
dst_dir = root / target_split / cls_name
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
dst = dst_dir / src_path.name
|
||||
if src_path.resolve() == dst.resolve():
|
||||
continue
|
||||
if dst.exists():
|
||||
dst.unlink()
|
||||
shutil.move(str(src_path), str(dst))
|
||||
moves += 1
|
||||
print(f"done, moved {moves} files into train/val")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(description="按类别分层划分 DMS 数据集")
|
||||
sub = p.add_subparsers(dest="mode", required=True)
|
||||
|
||||
py = sub.add_parser("yolo", help="YOLO 检测:images/labels 的 train+val")
|
||||
py.add_argument("--root", required=True, help="数据集根目录,含 images/ labels/")
|
||||
py.add_argument("--val-ratio", type=float, default=0.1)
|
||||
py.add_argument("--seed", type=int, default=42)
|
||||
py.add_argument("--pool-splits", default="train,val", help="合并哪些 split 后重分")
|
||||
py.add_argument("--min-val-per-class", type=int, default=1)
|
||||
py.add_argument("--min-train-per-class", type=int, default=1)
|
||||
py.add_argument("--rare-class-train-floor", type=int, default=5)
|
||||
py.add_argument("--dry-run", action="store_true")
|
||||
py.set_defaults(func=cmd_yolo)
|
||||
|
||||
pc = sub.add_parser("classify", help="分类:每类文件夹内独立划分")
|
||||
pc.add_argument("--root", required=True)
|
||||
pc.add_argument("--src-split", default="train", help="从哪个目录按类采样(如 train)")
|
||||
pc.add_argument("--val-ratio", type=float, default=0.1)
|
||||
pc.add_argument("--seed", type=int, default=42)
|
||||
pc.add_argument("--min-val-per-class", type=int, default=1)
|
||||
pc.add_argument("--min-train-per-class", type=int, default=1)
|
||||
pc.add_argument("--rare-class-train-floor", type=int, default=5)
|
||||
pc.add_argument("--dry-run", action="store_true")
|
||||
pc.set_defaults(func=cmd_classify)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
96
datasets/dms/scripts/train.sh
Executable file
96
datasets/dms/scripts/train.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# train.sh <task> [full|continue] — 读 datasets.registry.yaml
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DATASET_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
YOLO26_ROOT="${YOLO26_ROOT:-$(cd "$DATASET_ROOT/../Code/yolo26_rknn_ultralytics-main" 2>/dev/null && pwd || echo "")}"
|
||||
|
||||
# 优先使用 dms_yolo26 环境
|
||||
if [[ -z "${CONDA_DEFAULT_ENV:-}" || "${CONDA_DEFAULT_ENV}" != "dms_yolo26" ]]; then
|
||||
if [[ -f "${HOME}/miniconda3/etc/profile.d/conda.sh" ]]; then
|
||||
source "${HOME}/miniconda3/etc/profile.d/conda.sh"
|
||||
conda activate dms_yolo26 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
TASK="${1:?用法: $0 <task> [full|continue]}"
|
||||
TRAIN_MODE="${2:-full}"
|
||||
|
||||
REG="$DATASET_ROOT/datasets.registry.yaml"
|
||||
YAML="$DATASET_ROOT/manifests/yaml_active/${TASK}.yaml"
|
||||
VERSIONS="$DATASET_ROOT/manifests/train_versions.yaml"
|
||||
|
||||
if [[ ! -f "$YAML" ]]; then
|
||||
echo "找不到 yaml: $YAML"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -r TYPE MODE MODEL EPOCHS LR0 IMGSZ RUN_SUFFIX <<< "$(python3 - <<PY
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
reg = yaml.safe_load(Path("$REG").read_text())
|
||||
tcfg = reg["tasks"]["$TASK"]
|
||||
typ = tcfg["type"]
|
||||
train_mode = "$TRAIN_MODE" if "$TRAIN_MODE" in ("full", "continue") else reg.get("train", {}).get("mode", "full")
|
||||
t = reg.get("train", {}).get(typ, reg.get("train_defaults", {}).get(typ, {}))
|
||||
if train_mode == "continue":
|
||||
model = t.get("warm_start") or "null"
|
||||
epochs = t.get("epochs_continue", t.get("epochs_increment", 50))
|
||||
lr0 = t.get("lr0_continue", t.get("lr0", 0.001))
|
||||
suffix = "continue"
|
||||
else:
|
||||
model = t.get("model", "yolo26n.pt")
|
||||
epochs = t.get("epochs", 100)
|
||||
lr0 = t.get("lr0", 0.01)
|
||||
suffix = "full"
|
||||
imgsz = t.get("imgsz", 224 if typ == "classify" else 640)
|
||||
mode = {"detect": "detect", "pose": "pose", "classify": "classify"}.get(typ, "detect")
|
||||
print(typ, mode, model, epochs, lr0, imgsz, suffix)
|
||||
PY
|
||||
)"
|
||||
|
||||
# continue 模式:warm_start 为空则读 train_versions.yaml
|
||||
if [[ "$TRAIN_MODE" == "continue" && ( "$MODEL" == "null" || "$MODEL" == "None" || -z "$MODEL" ) ]]; then
|
||||
MODEL=$(python3 - <<PY 2>/dev/null || true
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
p = Path("$VERSIONS")
|
||||
if p.is_file():
|
||||
v = yaml.safe_load(p.read_text()) or {}
|
||||
c = v.get("$TASK", {}).get("current")
|
||||
if c: print(c)
|
||||
PY
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$TRAIN_MODE" == "continue" && ( -z "$MODEL" || "$MODEL" == "null" ) ]]; then
|
||||
echo "continue 模式需要 registry.train.<type>.warm_start 或 manifests/train_versions.yaml 中的 current"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_NAME="${TASK}_${RUN_SUFFIX}_$(date +%Y%m%d)"
|
||||
|
||||
echo "task=$TASK type=$TYPE yolo_mode=$MODE train_mode=$TRAIN_MODE"
|
||||
echo "data=$YAML"
|
||||
echo "model=$MODEL epochs=$EPOCHS lr0=$LR0 imgsz=$IMGSZ name=$RUN_NAME"
|
||||
|
||||
if [[ -z "$YOLO26_ROOT" || ! -d "$YOLO26_ROOT" ]]; then
|
||||
echo "请设置 YOLO26_ROOT 或安装到 ../Code/yolo26_rknn_ultralytics-main"
|
||||
echo " cd \$YOLO26_ROOT"
|
||||
echo " yolo $MODE train data=$YAML model=$MODEL epochs=$EPOCHS lr0=$LR0 imgsz=$IMGSZ project=runs/${MODE} name=$RUN_NAME"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$YOLO26_ROOT"
|
||||
yolo "$MODE" train \
|
||||
data="$YAML" \
|
||||
model="$MODEL" \
|
||||
epochs="$EPOCHS" \
|
||||
lr0="$LR0" \
|
||||
imgsz="$IMGSZ" \
|
||||
project="runs/${MODE}" \
|
||||
name="$RUN_NAME"
|
||||
|
||||
BEST="runs/${MODE}/${RUN_NAME}/weights/best.pt"
|
||||
echo "完成: $BEST"
|
||||
echo "请更新 manifests/train_versions.yaml 中 $TASK.current = $BEST"
|
||||
Reference in New Issue
Block a user