Spaces:
Sleeping
Sleeping
File size: 6,287 Bytes
78738de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | """Tải dataset pix2pockets (F2 Bước 2, BRIEF 05/08/2026) — idempotent.
Nguồn CHÍNH THỨC: repo của paper pix2pockets (arXiv 2504.12045, SCIA 2025)
https://github.com/viktorseba/pix2pockets — tác giả ship sẵn bản export
Roboflow `8-Ball-Pool-3.zip` (~41 MB) ngay trong root repo, nên KHÔNG cần
Roboflow API key (project gốc: universe.roboflow.com/bachelorthesis/
8-ball-pool-l530o, **License: CC BY 4.0** — ghi trong README.dataset.txt
và data.yaml của bản export).
Zip v3 dồn CẢ 247 ảnh vào train/ (val/test trong data.yaml gốc trỏ thư mục
không tồn tại) → script dựng thêm split 80/20 TẤT ĐỊNH (sort tên + shuffle
seed 20260805) ở `yolo/`, copy ảnh chứ không symlink (Windows), bản gốc
`8-Ball-Pool-3/` giữ nguyên không sửa.
Chạy lại thoải mái: mỗi bước tự skip nếu sản phẩm đã có. Xoá thư mục
`datasets/pix2pockets/` nếu muốn ép làm mới toàn bộ.
python scripts/cv/fetch_dataset.py # tải + giải + split + tóm tắt
python scripts/cv/fetch_dataset.py --summary # chỉ in tóm tắt
"""
from __future__ import annotations
import argparse
import random
import shutil
import sys
import urllib.request
import zipfile
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] # poolcoach-rl/
ZIP_URL = "https://github.com/viktorseba/pix2pockets/raw/main/8-Ball-Pool-3.zip"
DATASET_DIR = ROOT / "datasets" / "pix2pockets"
ZIP_PATH = DATASET_DIR / "8-Ball-Pool-3.zip"
RAW_DIR = DATASET_DIR / "8-Ball-Pool-3" # bản gốc từ zip — KHÔNG sửa
YOLO_DIR = DATASET_DIR / "yolo" # split 80/20 dựng lại cho ultralytics
SPLIT_SEED = 20260805
VAL_FRACTION = 0.2
CLASS_NAMES = ["Black", "Cue", "Dot", "Solid", "Striped"]
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp"}
def _download() -> None:
if ZIP_PATH.exists() and zipfile.is_zipfile(ZIP_PATH):
print(f"[skip] zip already present and valid: {ZIP_PATH}")
return
DATASET_DIR.mkdir(parents=True, exist_ok=True)
print(f"[down] {ZIP_URL}")
tmp = ZIP_PATH.with_suffix(".zip.part")
urllib.request.urlretrieve(ZIP_URL, tmp) # noqa: S310 — URL cố định https github
if not zipfile.is_zipfile(tmp):
tmp.unlink(missing_ok=True)
sys.exit("[ERROR] Downloaded file is not a zip - check URL/network and retry.")
tmp.replace(ZIP_PATH)
print(f"[ok] {ZIP_PATH} ({ZIP_PATH.stat().st_size / 1e6:.1f} MB)")
def _extract() -> None:
if (RAW_DIR / "data.yaml").exists():
print(f"[skip] already extracted: {RAW_DIR}")
return
print(f"[unzip] -> {DATASET_DIR}")
with zipfile.ZipFile(ZIP_PATH) as zf:
zf.extractall(DATASET_DIR)
def _make_split() -> None:
"""Split 80/20 tất định từ 247 ảnh train gốc — copy, không đụng bản gốc."""
if (YOLO_DIR / "data.yaml").exists():
print(f"[skip] split already built: {YOLO_DIR}")
return
src_img = RAW_DIR / "train" / "images"
src_lbl = RAW_DIR / "train" / "labels"
stems = sorted(p.stem for p in src_img.iterdir() if p.suffix.lower() in IMG_EXTS)
if not stems:
sys.exit(f"[ERROR] No images found under {src_img}")
rng = random.Random(SPLIT_SEED)
rng.shuffle(stems)
n_val = round(len(stems) * VAL_FRACTION)
splits = {"valid": set(stems[:n_val]), "train": set(stems[n_val:])}
print(f"[split] seed={SPLIT_SEED}: train={len(splits['train'])} valid={n_val}")
for split, chosen in splits.items():
(YOLO_DIR / split / "images").mkdir(parents=True, exist_ok=True)
(YOLO_DIR / split / "labels").mkdir(parents=True, exist_ok=True)
for img in src_img.iterdir():
if img.suffix.lower() not in IMG_EXTS:
continue
split = "valid" if img.stem in splits["valid"] else "train"
shutil.copy2(img, YOLO_DIR / split / "images" / img.name)
lbl = src_lbl / (img.stem + ".txt")
if lbl.exists():
shutil.copy2(lbl, YOLO_DIR / split / "labels" / lbl.name)
names_yaml = "\n".join(f" - {n}" for n in CLASS_NAMES)
(YOLO_DIR / "data.yaml").write_text(
"# Split 80/20 tat dinh tu pix2pockets 8-Ball-Pool-3 (CC BY 4.0)\n"
f"# sinh boi scripts/cv/fetch_dataset.py, seed={SPLIT_SEED}\n"
f"path: {YOLO_DIR.as_posix()}\n"
"train: train/images\n"
"val: valid/images\n"
f"nc: {len(CLASS_NAMES)}\n"
f"names:\n{names_yaml}\n",
encoding="utf-8",
)
print(f"[ok] {YOLO_DIR / 'data.yaml'}")
def _count_split(base: Path) -> None:
for split in ("train", "valid", "test"):
img_dir = base / split / "images"
lbl_dir = base / split / "labels"
if not img_dir.is_dir():
continue
imgs = [p for p in img_dir.iterdir() if p.suffix.lower() in IMG_EXTS]
n_boxes = 0
cls_counter: Counter[int] = Counter()
if lbl_dir.is_dir():
for lbl in lbl_dir.glob("*.txt"):
for line in lbl.read_text().splitlines():
parts = line.split()
if parts:
n_boxes += 1
cls_counter[int(parts[0])] += 1
per_class = {CLASS_NAMES[k]: v for k, v in sorted(cls_counter.items())}
print(f" {split:>5}: {len(imgs):4d} imgs, {n_boxes:5d} box, per-class {per_class}")
def _summary() -> None:
print("\n===== DATASET SUMMARY =====")
readme = RAW_DIR / "README.dataset.txt"
if readme.exists():
print(f"--- {readme.name} ---")
print(readme.read_text(encoding="utf-8", errors="replace").strip())
else:
print("[!] README.dataset.txt NOT found - license unconfirmed from zip.")
print("\n--- raw export (as shipped) ---")
_count_split(RAW_DIR)
print(f"--- rebuilt split (seed={SPLIT_SEED}) ---")
_count_split(YOLO_DIR)
def main() -> None:
ap = argparse.ArgumentParser(description="Fetch pix2pockets dataset (idempotent)")
ap.add_argument("--summary", action="store_true", help="only print summary, no download")
args = ap.parse_args()
if not args.summary:
_download()
_extract()
_make_split()
_summary()
if __name__ == "__main__":
main()
|