Download reference_lpips.py from Atriclove/sd-extract-new: direct link, hf CLI and curl.
- Browser
- Download file 8.78 kB
-
https://huggingface.co/Atriclove/sd-extract-new/resolve/main/reference_lpips.py
- Command line
-
hf download hf://Atriclove/sd-extract-new/reference_lpips.py
-
curl -L -o reference_lpips.py https://huggingface.co/Atriclove/sd-extract-new/resolve/main/reference_lpips.py
8.78 kB
| """ | |
| Compute LPIPS similarity between generated images and each prompt's reference image. | |
| 输入是 pia_extract.py 的输出目录: | |
| output_xxx/ | |
| generated/<prompt_id>/seed_*.png | |
| results/ | |
| 脚本会读取 prompts.json 中的 image 字段,在 results 下写出: | |
| reference_lpips_summary.jsonl | |
| reference_lpips_scores.jsonl | |
| """ | |
| import argparse | |
| import json | |
| import shutil | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from tqdm import tqdm | |
| def load_lpips_model(net: str, device: torch.device): | |
| try: | |
| import lpips | |
| except ImportError as exc: | |
| raise SystemExit("缺少 lpips 依赖,请先安装:pip install lpips") from exc | |
| model = lpips.LPIPS(net=net) | |
| model.eval().to(device) | |
| return model | |
| def load_prompts(path: Path, max_prompts: int | None = None) -> list[dict]: | |
| with open(path, "r", encoding="utf-8") as f: | |
| prompts = json.load(f) | |
| if max_prompts is not None: | |
| prompts = prompts[:max_prompts] | |
| return prompts | |
| def load_metadata(path: Path) -> dict[str, dict]: | |
| records = {} | |
| if not path.exists(): | |
| return records | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| if not line.strip(): | |
| continue | |
| record = json.loads(line) | |
| records[Path(record["path"]).name] = record | |
| return records | |
| def image_to_tensor(path: Path, image_size: int, device: torch.device) -> torch.Tensor: | |
| image = Image.open(path).convert("RGB").resize((image_size, image_size), Image.BICUBIC) | |
| array = np.asarray(image).astype(np.float32) / 127.5 - 1.0 | |
| tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0) | |
| return tensor.to(device=device) | |
| def compute_lpips_scores( | |
| model, | |
| reference_path: Path, | |
| image_paths: list[Path], | |
| image_size: int, | |
| batch_size: int, | |
| device: torch.device, | |
| ) -> list[tuple[Path, float]]: | |
| reference = image_to_tensor(reference_path, image_size, device) | |
| scores = [] | |
| for start in range(0, len(image_paths), batch_size): | |
| batch_paths = image_paths[start:start + batch_size] | |
| batch = torch.cat( | |
| [image_to_tensor(path, image_size, device) for path in batch_paths], | |
| dim=0, | |
| ) | |
| reference_batch = reference.expand(batch.shape[0], -1, -1, -1) | |
| distances = model(reference_batch, batch).flatten().detach().float().cpu().tolist() | |
| scores.extend(zip(batch_paths, distances)) | |
| return scores | |
| def best_by_base_seed(score_records: list[dict], top_k: int) -> list[dict]: | |
| best = {} | |
| for record in score_records: | |
| base_seed = record.get("base_seed") | |
| if base_seed is None: | |
| continue | |
| previous = best.get(base_seed) | |
| if previous is None or record["lpips"] < previous["lpips"]: | |
| best[base_seed] = record | |
| return sorted(best.values(), key=lambda item: item["lpips"])[:top_k] | |
| def write_jsonl(path: Path, records: list[dict]) -> None: | |
| with open(path, "w", encoding="utf-8") as f: | |
| for record in records: | |
| f.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| def lpips_tag(value: float) -> str: | |
| return f"{value:.4f}".replace(".", "p") | |
| def copy_top_k_images(records: list[dict], topk_root: Path, prompt_id: str) -> list[dict]: | |
| prompt_topk_dir = topk_root / prompt_id | |
| prompt_topk_dir.mkdir(parents=True, exist_ok=True) | |
| copied = [] | |
| for rank, record in enumerate(records, 1): | |
| src = Path(record["image_path"]) | |
| dst_name = f"rank_{rank:02d}_lpips_{lpips_tag(record['lpips'])}_{src.name}" | |
| dst = prompt_topk_dir / dst_name | |
| shutil.copy2(src, dst) | |
| copied_record = dict(record) | |
| copied_record["rank"] = rank | |
| copied_record["saved_path"] = str(dst) | |
| copied.append(copied_record) | |
| return copied | |
| def run(args: argparse.Namespace) -> Path: | |
| output_root = Path(args.output) | |
| generated_root = output_root / args.generated_subdir | |
| results_dir = output_root / args.results_subdir | |
| results_dir.mkdir(parents=True, exist_ok=True) | |
| topk_root = results_dir / args.top_k_image_dir | |
| device = torch.device(args.device) | |
| model = load_lpips_model(args.net, device) | |
| prompts = load_prompts(Path(args.prompts), args.max_prompts) | |
| reference_dir = Path(args.reference_dir) | |
| summary_records = [] | |
| all_score_records = [] | |
| for item in tqdm(prompts, desc="LPIPS reference matching"): | |
| prompt_id = str(item["id"]) | |
| reference_name = item.get("image") | |
| if not reference_name: | |
| continue | |
| prompt_dir = generated_root / prompt_id | |
| reference_path = reference_dir / reference_name | |
| if not prompt_dir.exists() or not reference_path.exists(): | |
| continue | |
| image_paths = sorted(prompt_dir.glob(args.image_glob)) | |
| if not image_paths: | |
| continue | |
| metadata_by_name = load_metadata(prompt_dir / "metadata.jsonl") | |
| scores = compute_lpips_scores( | |
| model=model, | |
| reference_path=reference_path, | |
| image_paths=image_paths, | |
| image_size=args.image_size, | |
| batch_size=args.batch_size, | |
| device=device, | |
| ) | |
| score_records = [] | |
| for image_path, lpips_value in scores: | |
| metadata = metadata_by_name.get(image_path.name, {}) | |
| record = { | |
| "id": prompt_id, | |
| "prompt": item.get("text"), | |
| "reference_image": reference_name, | |
| "reference_path": str(reference_path), | |
| "image_path": str(image_path), | |
| "image_name": image_path.name, | |
| "lpips": float(lpips_value), | |
| "base_seed": metadata.get("base_seed"), | |
| "branch_index": metadata.get("branch_index"), | |
| "output_index": metadata.get("output_index"), | |
| } | |
| score_records.append(record) | |
| score_records.sort(key=lambda record: record["lpips"]) | |
| lpips_values = [record["lpips"] for record in score_records] | |
| top_k = score_records[:args.top_k] | |
| top_k_distinct_base_seed = best_by_base_seed(score_records, args.top_k) | |
| saved_top_k = [] | |
| if args.save_top_k_images and top_k: | |
| saved_top_k = copy_top_k_images(top_k, topk_root, prompt_id) | |
| summary_records.append({ | |
| "id": prompt_id, | |
| "prompt": item.get("text"), | |
| "reference_image": reference_name, | |
| "reference_path": str(reference_path), | |
| "num_images": len(score_records), | |
| "best_lpips": float(min(lpips_values)), | |
| "mean_lpips": float(np.mean(lpips_values)), | |
| "median_lpips": float(np.median(lpips_values)), | |
| "top_k": top_k, | |
| "top_k_distinct_base_seed": top_k_distinct_base_seed, | |
| "saved_top_k": saved_top_k, | |
| }) | |
| all_score_records.extend(score_records) | |
| summary_path = results_dir / "reference_lpips_summary.jsonl" | |
| scores_path = results_dir / "reference_lpips_scores.jsonl" | |
| write_jsonl(summary_path, summary_records) | |
| write_jsonl(scores_path, all_score_records) | |
| print(f"LPIPS summary saved: {summary_path}") | |
| print(f"LPIPS scores saved: {scores_path}") | |
| if args.save_top_k_images: | |
| print(f"Top-k images saved under: {topk_root}") | |
| print(f"Processed prompts: {len(summary_records)}") | |
| return summary_path | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="计算生成图与 prompt reference image 的 LPIPS 距离") | |
| parser.add_argument("--output", type=str, required=True, help="pia_extract.py 输出目录") | |
| parser.add_argument("--prompts", type=str, default="prompts/prompts.json") | |
| parser.add_argument("--reference_dir", type=str, default="prompts/images") | |
| parser.add_argument("--generated_subdir", type=str, default="generated") | |
| parser.add_argument("--results_subdir", type=str, default="results") | |
| parser.add_argument("--image_glob", type=str, default="seed_*.png") | |
| parser.add_argument("--max_prompts", type=int, default=None) | |
| parser.add_argument("--top_k", type=int, default=10) | |
| parser.add_argument("--save_top_k_images", action=argparse.BooleanOptionalAction, default=True) | |
| parser.add_argument("--top_k_image_dir", type=str, default="reference_lpips_topk") | |
| parser.add_argument("--batch_size", type=int, default=16) | |
| parser.add_argument("--image_size", type=int, default=512) | |
| parser.add_argument("--device", type=str, default="cuda") | |
| parser.add_argument("--net", type=str, default="alex", choices=["alex", "vgg", "squeeze"]) | |
| return parser.parse_args() | |
| if __name__ == "__main__": | |
| run(parse_args()) | |