Spaces:
Running
Running
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from PIL import Image, ImageDraw | |
| import psycopg | |
| from psycopg import sql | |
| def normalize_pg_url(url: str) -> str: | |
| if url.startswith("postgresql+psycopg://"): | |
| return "postgresql://" + url.split("postgresql+psycopg://", 1)[1] | |
| return url | |
| def save_grid(label: str, image_paths: list[Path], out_path: Path) -> None: | |
| if not image_paths: | |
| return | |
| thumbs: list[Image.Image] = [] | |
| for path in image_paths: | |
| thumbs.append(Image.open(path).convert("RGB").resize((128, 128), Image.Resampling.BILINEAR)) | |
| cols = 4 | |
| rows = (len(thumbs) + cols - 1) // cols | |
| canvas = Image.new("RGB", (cols * 128, rows * 128 + 30), (255, 255, 255)) | |
| draw = ImageDraw.Draw(canvas) | |
| draw.text((8, 6), f"{label} pgvector top-{len(image_paths)}", fill=(0, 0, 0)) | |
| for idx, thumb in enumerate(thumbs): | |
| r = idx // cols | |
| c = idx % cols | |
| canvas.paste(thumb, (c * 128, r * 128 + 30)) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| canvas.save(out_path) | |
| def parse_labels(raw: str) -> list[str]: | |
| return [item.strip() for item in raw.split(",") if item.strip()] | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--db-url", required=True, help="PostgreSQL URL") | |
| parser.add_argument("--data-root", default="data/quickdraw/images") | |
| parser.add_argument("--embedding-table", default="quickdraw_embeddings") | |
| parser.add_argument("--prototype-table", default="quickdraw_prototypes") | |
| parser.add_argument("--embedding-source", default="quickdraw_train_v1") | |
| parser.add_argument("--prototype-source", default="quickdraw_avg_v1") | |
| parser.add_argument("--labels", required=True, help="Comma-separated labels") | |
| parser.add_argument("--top-k", type=int, default=12) | |
| parser.add_argument("--out-dir", default="data/quickdraw/pgvector_inspect") | |
| args = parser.parse_args() | |
| labels = parse_labels(args.labels) | |
| if not labels: | |
| raise RuntimeError("labels must not be empty") | |
| data_root = Path(args.data_root) | |
| out_dir = Path(args.out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| db_url = normalize_pg_url(args.db_url) | |
| summary: dict[str, dict[str, object]] = {} | |
| query = sql.SQL( | |
| """ | |
| SELECT e.image_path, 1 - (e.embedding <=> p.embedding) AS similarity | |
| FROM {embedding_table} e | |
| JOIN {prototype_table} p | |
| ON p.label = %s | |
| AND p.source = %s | |
| WHERE e.label = %s | |
| AND e.source = %s | |
| ORDER BY e.embedding <=> p.embedding | |
| LIMIT %s | |
| """ | |
| ).format( | |
| embedding_table=sql.Identifier(args.embedding_table), | |
| prototype_table=sql.Identifier(args.prototype_table), | |
| ) | |
| with psycopg.connect(db_url) as conn: | |
| with conn.cursor() as cur: | |
| for label in labels: | |
| cur.execute( | |
| query, | |
| ( | |
| label, | |
| args.prototype_source, | |
| label, | |
| args.embedding_source, | |
| args.top_k, | |
| ), | |
| ) | |
| rows = cur.fetchall() | |
| if not rows: | |
| print(f"[warn] {label}: no rows found") | |
| summary[label] = {"top_k": 0, "items": [], "output": None} | |
| continue | |
| items: list[dict[str, object]] = [] | |
| found_paths: list[Path] = [] | |
| missing_count = 0 | |
| for image_path, similarity in rows: | |
| rel = Path(str(image_path)) | |
| abs_path = data_root / rel | |
| exists = abs_path.exists() | |
| if exists: | |
| found_paths.append(abs_path) | |
| else: | |
| missing_count += 1 | |
| items.append( | |
| { | |
| "image_path": str(rel).replace("\\", "/"), | |
| "absolute_path": str(abs_path), | |
| "similarity": float(similarity), | |
| "exists": exists, | |
| } | |
| ) | |
| out_png = out_dir / f"{label}_top{len(found_paths)}.png" | |
| if found_paths: | |
| save_grid(label, found_paths, out_png) | |
| print(f"[done] {label}: wrote {out_png} (missing={missing_count})") | |
| else: | |
| print(f"[warn] {label}: all top-k files missing under {data_root}") | |
| summary[label] = { | |
| "top_k_requested": args.top_k, | |
| "top_k_found_files": len(found_paths), | |
| "missing_files": missing_count, | |
| "output": str(out_png) if found_paths else None, | |
| "items": items, | |
| } | |
| summary_path = out_dir / "summary.json" | |
| summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8") | |
| print(f"[done] summary: {summary_path}") | |
| if __name__ == "__main__": | |
| main() | |