File size: 5,193 Bytes
834d1cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import argparse
import sys
from pathlib import Path

if __package__ is None or __package__ == "":
    sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from mirrorppr.data.ffhq_wild_blend import (
    FFHQ_SHIFT_DIRECTIONS,
    FFHQWildFrameAugmentor,
    blend_aligned_face_into_wild,
    crop_aligned_from_quad,
    load_wild_info,
)
from mirrorppr.data.image_ops import bgr_to_pil, load_rgb
from mirrorppr.data.io import read_json
from mirrorppr.data.llw_face_editor import LLWFaceRetoucher, normalize_operations


def parse_operation_specs(args: argparse.Namespace) -> list[dict[str, object]]:
    specs = []
    if args.operations:
        specs.extend(item.strip() for item in args.operations.split(",") if item.strip())
    if not specs:
        raise ValueError("Provide --operations name:strength[,name:strength...].")
    return normalize_operations(specs)


def resolve_wild_info(args: argparse.Namespace) -> dict[str, object]:
    if args.wild_info_json:
        data = read_json(args.wild_info_json)
        if not isinstance(data, dict) or "face_quad" not in data:
            raise ValueError("--wild-info-json must contain a JSON object with face_quad.")
        return data
    if args.ffhq_metadata and args.image_id:
        return load_wild_info(args.ffhq_metadata, args.image_id)
    raise ValueError("Provide crop information with --ffhq-metadata plus --image-id, or --wild-info-json.")


def save_wild_frame_augmentations(
    source_wild,
    target_wild,
    wild_info: dict[str, object],
    output_dir: Path,
    count: int,
) -> int:
    output_dir.mkdir(parents=True, exist_ok=True)
    augmentor = FFHQWildFrameAugmentor(wild_info, wild_size=source_wild.size)
    directions = list(FFHQ_SHIFT_DIRECTIONS.items())[:count]
    for idx, (direction_name, direction_vec) in enumerate(directions):
        shifted_quad = augmentor.get_shifted_quad(direction_vec, magnitude_ratio=1.0)
        source_out = output_dir / f"simulated_{idx:03d}_{direction_name}_source.png"
        target_out = output_dir / f"simulated_{idx:03d}_{direction_name}_target.png"
        crop_aligned_from_quad(source_wild, shifted_quad).save(source_out)
        crop_aligned_from_quad(target_wild, shifted_quad).save(target_out)
    return len(directions)


def run_single(args: argparse.Namespace) -> None:
    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    stale_manifest = output_dir / "manifest.json"
    if stale_manifest.exists() or stale_manifest.is_symlink():
        stale_manifest.unlink()
    if args.num_augmentations > 0 and not args.wild_image:
        raise ValueError("Faithful simulated self-augmentation requires --wild-image and crop information. Use --num-augmentations 0 to only save the aligned pair.")

    operations = parse_operation_specs(args)
    retoucher = LLWFaceRetoucher()
    edited_bgr = retoucher.apply_operations(args.aligned_image, operations)
    edited_aligned = bgr_to_pil(edited_bgr)
    aligned = load_rgb(args.aligned_image)
    aligned_source_out = output_dir / "aligned_source.png"
    aligned_target_out = output_dir / "aligned_target.png"
    aligned.save(aligned_source_out)
    edited_aligned.save(aligned_target_out)

    if args.wild_image:
        wild = load_rgb(args.wild_image)
        wild_info = resolve_wild_info(args)
        face_quad = wild_info["face_quad"]
        wild_source = blend_aligned_face_into_wild(aligned, wild, face_quad)
        wild_source_out = output_dir / "wild_source.png"
        wild_source.save(wild_source_out)
        wild_retouched = blend_aligned_face_into_wild(edited_aligned, wild, face_quad)
        wild_target_out = output_dir / "wild_target.png"
        wild_retouched.save(wild_target_out)

        if args.num_augmentations > 0:
            save_wild_frame_augmentations(
                wild_source,
                wild_retouched,
                wild_info,
                output_dir / "augmentations",
                min(args.num_augmentations, len(FFHQ_SHIFT_DIRECTIONS)),
            )
    elif args.num_augmentations > 0:
        raise ValueError("Internal error: wild inputs are required before saving simulated augmentations.")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Construct MirrorPPR simulated subset samples.")
    parser.add_argument("--aligned-image", required=True, help="1024x1024 aligned FFHQ image.")
    parser.add_argument(
        "--operations",
        default=None,
        help="Comma-separated operations as name:strength, e.g. eye_resize:100,nose_alar:-100.",
    )
    parser.add_argument("--output-dir", required=True)
    parser.add_argument("--wild-image", default=None)
    parser.add_argument("--ffhq-metadata", default=None)
    parser.add_argument("--image-id", default=None)
    parser.add_argument("--wild-info-json", default=None, help="JSON file with face_quad and optional face_rect/pixel_size.")
    parser.add_argument("--num-augmentations", type=int, default=8)
    return parser


def main() -> None:
    args = build_parser().parse_args()
    run_single(args)


if __name__ == "__main__":
    main()