Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import logging | |
| import re | |
| import shutil | |
| import urllib.parse | |
| import urllib.request | |
| import zipfile | |
| from pathlib import Path | |
| from typing import Iterable | |
| try: | |
| from _bootstrap import add_project_root_to_path | |
| except ModuleNotFoundError: | |
| from scripts._bootstrap import add_project_root_to_path | |
| ROOT = add_project_root_to_path() | |
| from src.config import IMAGE_DIR | |
| DEFAULT_REPO = "https://github.com/marcusklasson/GroceryStoreDataset" | |
| DEFAULT_REF = "master" | |
| DEFAULT_CACHE_DIR = ROOT / "data" / "_sources" / "grocery_store_dataset" | |
| CATEGORY_LEVELS = {"super", "coarse", "fine"} | |
| INVALID_CATEGORY_CHARS = re.compile(r'[<>:"/\\|?*]+') | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s") | |
| logger = logging.getLogger(__name__) | |
| def _normalize_repo_url(repo_url: str) -> str: | |
| value = repo_url.strip().rstrip("/") | |
| if value.endswith(".git"): | |
| value = value[:-4] | |
| if "://" not in value: | |
| value = f"https://github.com/{value.lstrip('/')}" | |
| return value | |
| def _parse_github_repo(repo_url: str) -> tuple[str, str, str]: | |
| normalized = _normalize_repo_url(repo_url) | |
| parsed = urllib.parse.urlparse(normalized) | |
| if not parsed.netloc: | |
| raise ValueError(f"Invalid repo URL: {repo_url}") | |
| parts = [part for part in parsed.path.split("/") if part] | |
| if len(parts) < 2: | |
| raise ValueError(f"Invalid repo URL: {repo_url}") | |
| owner, repo = parts[0], parts[1] | |
| base = f"{parsed.scheme or 'https'}://{parsed.netloc}/{owner}/{repo}" | |
| return owner, repo, base | |
| def _build_zip_url(repo_base: str, ref: str) -> str: | |
| if ref.startswith("refs/"): | |
| return f"{repo_base}/archive/{ref}.zip" | |
| return f"{repo_base}/archive/refs/heads/{ref}.zip" | |
| def _download_zip(url: str, dest: Path, force: bool) -> None: | |
| if dest.exists() and not force: | |
| logger.info("Using cached archive: %s", dest) | |
| return | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| logger.info("Downloading: %s", url) | |
| request = urllib.request.Request(url, headers={"User-Agent": "image-retrieval-importer"}) | |
| with urllib.request.urlopen(request) as response, open(dest, "wb") as handle: | |
| shutil.copyfileobj(response, handle) | |
| def _extract_zip(zip_path: Path, dest_dir: Path, force: bool) -> Path: | |
| dest_dir.mkdir(parents=True, exist_ok=True) | |
| with zipfile.ZipFile(zip_path) as archive: | |
| top_levels = { | |
| Path(name).parts[0] | |
| for name in archive.namelist() | |
| if name and not name.endswith("/") | |
| } | |
| if len(top_levels) != 1: | |
| raise RuntimeError("Unexpected archive structure; could not locate repo root.") | |
| repo_root = dest_dir / next(iter(top_levels)) | |
| if repo_root.exists() and not force: | |
| logger.info("Using extracted repo: %s", repo_root) | |
| return repo_root | |
| if repo_root.exists(): | |
| shutil.rmtree(repo_root) | |
| logger.info("Extracting archive to: %s", dest_dir) | |
| with zipfile.ZipFile(zip_path) as archive: | |
| archive.extractall(dest_dir) | |
| return repo_root | |
| def _iter_split_entries(split_file: Path) -> Iterable[str]: | |
| with split_file.open("r", encoding="utf-8") as handle: | |
| reader = csv.reader(handle) | |
| for row in reader: | |
| if not row: | |
| continue | |
| path_value = row[0].strip() | |
| if path_value: | |
| yield path_value | |
| def _category_from_path(relative_path: str, level: str) -> str: | |
| parts = Path(relative_path).parts | |
| if level == "super": | |
| index = 1 | |
| elif level == "coarse": | |
| index = 2 | |
| else: | |
| index = 3 | |
| if len(parts) > index: | |
| return parts[index] | |
| if len(parts) >= 2: | |
| return parts[-2] | |
| return "Uncategorized" | |
| def _safe_category(name: str) -> str: | |
| cleaned = INVALID_CATEGORY_CHARS.sub("-", name.strip()) | |
| return cleaned or "Uncategorized" | |
| def _unique_destination(dest_dir: Path, filename: str, split: str) -> Path: | |
| candidate = dest_dir / filename | |
| if not candidate.exists(): | |
| return candidate | |
| stem = Path(filename).stem | |
| suffix = Path(filename).suffix | |
| candidate = dest_dir / f"{stem}_{split}{suffix}" | |
| if not candidate.exists(): | |
| return candidate | |
| counter = 2 | |
| while True: | |
| candidate = dest_dir / f"{stem}_{split}_{counter}{suffix}" | |
| if not candidate.exists(): | |
| return candidate | |
| counter += 1 | |
| def _resolve_path(value: Path) -> Path: | |
| return value if value.is_absolute() else ROOT / value | |
| def _import_images( | |
| dataset_dir: Path, | |
| target_dir: Path, | |
| splits: list[str], | |
| category_level: str, | |
| dry_run: bool, | |
| limit: int | None, | |
| ) -> None: | |
| total_entries = 0 | |
| copied = 0 | |
| missing = 0 | |
| skipped = 0 | |
| seen: set[str] = set() | |
| for split in splits: | |
| split_file = dataset_dir / f"{split}.txt" | |
| if not split_file.exists(): | |
| logger.warning("Split file not found: %s", split_file) | |
| continue | |
| for relative_path in _iter_split_entries(split_file): | |
| total_entries += 1 | |
| relative_norm = relative_path.replace("\\", "/") | |
| if relative_norm in seen: | |
| skipped += 1 | |
| continue | |
| seen.add(relative_norm) | |
| src = dataset_dir / relative_norm | |
| if not src.exists(): | |
| missing += 1 | |
| continue | |
| category = _safe_category(_category_from_path(relative_norm, category_level)) | |
| dest_dir = target_dir / category | |
| if not dry_run: | |
| dest_dir.mkdir(parents=True, exist_ok=True) | |
| dest_path = _unique_destination(dest_dir, Path(relative_norm).name, split) | |
| if not dry_run: | |
| shutil.copy2(src, dest_path) | |
| copied += 1 | |
| if limit and copied >= limit: | |
| logger.info("Reached limit of %d images", limit) | |
| logger.info("Processed entries: %d", total_entries) | |
| logger.info("Copied: %d, missing: %d, skipped: %d", copied, missing, skipped) | |
| return | |
| logger.info("Processed entries: %d", total_entries) | |
| logger.info("Copied: %d, missing: %d, skipped: %d", copied, missing, skipped) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Download GroceryStoreDataset from GitHub and import images into data/images." | |
| ) | |
| parser.add_argument("--repo", default=DEFAULT_REPO, help="GitHub repo URL or owner/repo.") | |
| parser.add_argument("--ref", default=DEFAULT_REF, help="Git ref (branch, tag, or commit).") | |
| parser.add_argument( | |
| "--dataset-dir", | |
| type=Path, | |
| help="Use an existing dataset folder (the one containing train.txt).", | |
| ) | |
| parser.add_argument( | |
| "--cache-dir", | |
| type=Path, | |
| default=DEFAULT_CACHE_DIR, | |
| help="Where to download and extract the repo.", | |
| ) | |
| parser.add_argument( | |
| "--target-dir", | |
| type=Path, | |
| default=IMAGE_DIR, | |
| help="Target images directory (default: data/images).", | |
| ) | |
| parser.add_argument( | |
| "--splits", | |
| nargs="+", | |
| default=["train", "val", "test"], | |
| help="Which splits to import (train/val/test).", | |
| ) | |
| parser.add_argument( | |
| "--category-level", | |
| choices=sorted(CATEGORY_LEVELS), | |
| default="coarse", | |
| help="Category granularity derived from the dataset paths.", | |
| ) | |
| parser.add_argument("--limit", type=int, help="Import at most N images.") | |
| parser.add_argument("--dry-run", action="store_true", help="Scan without copying files.") | |
| parser.add_argument( | |
| "--force", | |
| action="store_true", | |
| help="Re-download and re-extract the dataset archive.", | |
| ) | |
| parser.add_argument( | |
| "--cleanup", | |
| action="store_true", | |
| help="Remove the downloaded archive and extracted repo after import.", | |
| ) | |
| args = parser.parse_args() | |
| splits = [split for split in args.splits if split in {"train", "val", "test"}] | |
| if not splits: | |
| raise SystemExit("No valid splits provided; use train, val, or test.") | |
| target_dir = _resolve_path(args.target_dir) | |
| if args.dataset_dir: | |
| dataset_dir = _resolve_path(args.dataset_dir) | |
| if dataset_dir.is_dir() and not (dataset_dir / "train.txt").exists(): | |
| candidate = dataset_dir / "dataset" | |
| if candidate.exists(): | |
| dataset_dir = candidate | |
| if not dataset_dir.exists(): | |
| raise SystemExit(f"Dataset directory not found: {dataset_dir}") | |
| logger.info("Using existing dataset directory: %s", dataset_dir) | |
| else: | |
| cache_dir = _resolve_path(args.cache_dir) | |
| owner, repo, repo_base = _parse_github_repo(args.repo) | |
| zip_url = _build_zip_url(repo_base, args.ref) | |
| ref_slug = re.sub(r"[^A-Za-z0-9._-]+", "_", args.ref) | |
| zip_path = cache_dir / f"{repo}-{ref_slug}.zip" | |
| _download_zip(zip_url, zip_path, args.force) | |
| repo_root = _extract_zip(zip_path, cache_dir, args.force) | |
| dataset_dir = repo_root / "dataset" | |
| if not dataset_dir.exists(): | |
| raise SystemExit(f"Dataset folder not found: {dataset_dir}") | |
| logger.info("Dataset directory: %s", dataset_dir) | |
| logger.info("Target directory: %s", target_dir) | |
| logger.info("Category level: %s", args.category_level) | |
| logger.info("Splits: %s", ", ".join(splits)) | |
| _import_images( | |
| dataset_dir=dataset_dir, | |
| target_dir=target_dir, | |
| splits=splits, | |
| category_level=args.category_level, | |
| dry_run=args.dry_run, | |
| limit=args.limit, | |
| ) | |
| if args.cleanup and not args.dataset_dir: | |
| cache_dir = _resolve_path(args.cache_dir) | |
| if cache_dir.exists(): | |
| shutil.rmtree(cache_dir) | |
| logger.info("Removed cache directory: %s", cache_dir) | |
| if __name__ == "__main__": | |
| main() | |