File size: 8,781 Bytes
84468dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"""
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)


@torch.no_grad()
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())