Files

253 lines
9.8 KiB
Python

from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
from typing import Any
import numpy as np
from PIL import Image, ImageDraw
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from opentoonz_line_tools.autoclose import AutoCloseSettings, autoclose_gaps, closure_overlay
from opentoonz_line_tools.cleanup import BlueCleanupSettings, cleanup_blue_lines
from opentoonz_line_tools.regions import RegionSettings, label_fill_regions, labels_to_preview
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Evaluate OpenToonz Line Tools on research image sets.")
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--dataset-root", type=Path, help="Dataset root containing images/train and images/val.")
source.add_argument("--image-root", type=Path, help="Directory containing images directly or recursively.")
parser.add_argument("--output-dir", required=True, type=Path, help="Directory for metrics and contact sheet outputs.")
parser.add_argument("--image-glob", default="panel*.*", help="Glob used with --image-root. Default: panel*.*")
parser.add_argument("--region-min-area", default=256, type=int)
parser.add_argument("--max-regions", default=128, type=int)
parser.add_argument("--max-endpoints", default=600, type=int)
parser.add_argument("--thumb-width", default=220, type=int)
return parser.parse_args()
def dataset_image_paths(dataset_root: Path) -> list[Path]:
image_root = dataset_root / "images"
return [
path
for path in sorted(image_root.glob("*/*.*"))
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
]
def root_image_paths(image_root: Path, image_glob: str) -> list[Path]:
return [
path
for path in sorted(image_root.rglob(image_glob))
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
]
def label_count(dataset_root: Path, image_path: Path) -> int:
rel = image_path.relative_to(dataset_root)
label_path = dataset_root / "labels" / rel.parent.name / f"{image_path.stem}.txt"
if not label_path.exists():
return 0
return sum(1 for line in label_path.read_text(encoding="utf-8").splitlines() if line.strip())
def fit(image: Image.Image, width: int) -> Image.Image:
height = max(1, round(image.height * width / image.width))
return image.resize((width, height), Image.Resampling.LANCZOS)
def make_contact_row(
*,
index: int,
rel: Path,
label_count_value: int,
line_ratio: float,
skipped: str,
region_count: int,
images: list[Image.Image],
thumb_width: int,
) -> Image.Image:
thumbs = [fit(image, thumb_width) for image in images]
label_h = 42
row_h = max(thumb.height for thumb in thumbs) + label_h
strip = Image.new("RGB", (thumb_width * len(thumbs), row_h), "white")
draw = ImageDraw.Draw(strip)
title = f"{index:02d} {rel.name} labels={label_count_value} line={line_ratio:.3f} regions={region_count}"
draw.text((4, 4), title, fill=(0, 0, 0))
if skipped:
draw.text((4, 20), skipped[:90], fill=(180, 0, 0))
for column, thumb in enumerate(thumbs):
strip.paste(thumb, (column * thumb_width, label_h))
return strip
def write_contact_sheet(rows: list[Image.Image], path: Path) -> None:
width = max(row.width for row in rows)
height = sum(row.height for row in rows)
contact = Image.new("RGB", (width, height), "white")
y = 0
for row in rows:
contact.paste(row, (0, y))
y += row.height
contact.save(path, quality=90)
def write_summary(summary: dict[str, Any], path: Path) -> None:
rows = summary["rows"]
max_regions = summary["settings"]["regions"]["max_regions"]
lines = [
f"# {summary['title']}",
"",
f"Dataset: `{summary['dataset']}`",
"",
"## Summary",
"",
f"- Images: {summary['image_count']}",
f"- Avg blue line ratio: {summary['averages']['line_ratio']}",
f"- Avg cleanup sec: {summary['averages']['cleanup_sec']}",
f"- Avg autoclose sec: {summary['averages']['autoclose_sec']}",
f"- Autoclose skipped: {summary['counts']['autoclose_skipped']}/{summary['image_count']}",
f"- Avg region count: {summary['averages']['region_count']}",
f"- Region capped at {max_regions}: {summary['counts']['region_capped']}/{summary['image_count']}",
"",
"## Rows",
"",
"| file | split | labels | line ratio | skip | regions |",
"|---|---:|---:|---:|---|---:|",
]
for row in rows:
lines.append(
f"| {row['file']} | {row['split']} | {row['label_count']} | {row['line_ratio']} | "
f"{row['autoclose_skipped']} | {row['region_count']} |"
)
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> None:
args = parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
if args.dataset_root:
source_root = args.dataset_root
paths = dataset_image_paths(source_root)
title = "MC/MS Research Image Evaluation"
source_kind = "dataset"
else:
source_root = args.image_root
paths = root_image_paths(source_root, args.image_glob)
title = "Panel Crop Image Evaluation"
source_kind = "image_root"
if not paths:
raise SystemExit(f"No images found under {source_root} with the selected input mode.")
cleanup_settings = BlueCleanupSettings()
autoclose_settings = AutoCloseSettings(max_endpoints=args.max_endpoints)
region_settings = RegionSettings(
min_area=args.region_min_area,
max_regions=args.max_regions,
ignore_border_regions=True,
)
rows: list[dict[str, Any]] = []
contact_rows: list[Image.Image] = []
for index, path in enumerate(paths, 1):
rel = path.relative_to(source_root)
source_pil = Image.open(path).convert("RGB")
source = np.asarray(source_pil)
labels = label_count(source_root, path) if args.dataset_root else 0
start = time.perf_counter()
clean = cleanup_blue_lines(source, cleanup_settings)
cleanup_sec = time.perf_counter() - start
start = time.perf_counter()
closed = autoclose_gaps(clean["line_mask"], autoclose_settings)
autoclose_sec = time.perf_counter() - start
start = time.perf_counter()
regions = label_fill_regions(closed["closed_mask"], region_settings)
region_sec = time.perf_counter() - start
line_ratio = float(clean["line_mask"].mean())
row = {
"index": index,
"split": rel.parts[1] if args.dataset_root and len(rel.parts) > 1 else rel.parent.as_posix() or ".",
"file": rel.name,
"relative_path": str(rel),
"width": int(source.shape[1]),
"height": int(source.shape[0]),
"label_count": labels,
"line_ratio": round(line_ratio, 6),
"cleanup_sec": round(cleanup_sec, 3),
"autoclose_sec": round(autoclose_sec, 3),
"segments": len(closed["segments"]),
"autoclose_skipped": closed["skipped"],
"region_sec": round(region_sec, 3),
"region_count": len(regions["regions"]),
"region_capped": len(regions["regions"]) >= region_settings.max_regions,
}
rows.append(row)
contact_rows.append(
make_contact_row(
index=index,
rel=rel,
label_count_value=labels,
line_ratio=line_ratio,
skipped=closed["skipped"],
region_count=len(regions["regions"]),
images=[
source_pil,
Image.fromarray(clean["line_rgb"]),
Image.fromarray(closure_overlay(clean["line_mask"], closed["closed_mask"])),
Image.fromarray(labels_to_preview(regions["label_map"], closed["closed_mask"])),
],
thumb_width=args.thumb_width,
)
)
print(
f"{index:02d}/{len(paths)} {rel} "
f"line={line_ratio:.4f} skip={closed['skipped'] or '-'} regions={len(regions['regions'])}"
)
summary = {
"title": title,
"dataset": str(source_root),
"source_kind": source_kind,
"image_glob": args.image_glob if args.image_root else None,
"image_count": len(rows),
"settings": {
"cleanup": cleanup_settings.__dict__,
"autoclose": autoclose_settings.__dict__,
"regions": region_settings.__dict__,
},
"averages": {
"line_ratio": round(sum(row["line_ratio"] for row in rows) / max(1, len(rows)), 6),
"cleanup_sec": round(sum(row["cleanup_sec"] for row in rows) / max(1, len(rows)), 3),
"autoclose_sec": round(sum(row["autoclose_sec"] for row in rows) / max(1, len(rows)), 3),
"region_sec": round(sum(row["region_sec"] for row in rows) / max(1, len(rows)), 3),
"region_count": round(sum(row["region_count"] for row in rows) / max(1, len(rows)), 2),
},
"counts": {
"autoclose_skipped": sum(1 for row in rows if row["autoclose_skipped"]),
"region_capped": sum(1 for row in rows if row["region_capped"]),
"low_blue_signal_lt_0_03": sum(1 for row in rows if row["line_ratio"] < 0.03),
},
"rows": rows,
}
(args.output_dir / "metrics.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
write_summary(summary, args.output_dir / "summary.md")
write_contact_sheet(contact_rows, args.output_dir / "contact_sheet.jpg")
print(f"wrote {args.output_dir}")
if __name__ == "__main__":
main()