MR-IQA-2 / code /examples /quick_start.py
nishida_worker
Add logo and runnable inference bundle
d13a83d
Raw
History Blame Contribute Delete
12.7 kB
#!/usr/bin/env python3
"""Assess, edit, and evaluate one image with the released MR-IQA-2 models.
Actor, Editor, and Judge run sequentially, so all three stages can reuse one
physical GPU without sharing model memory.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, Callable
REPO_ID = "RobinY99/MR-IQA-2"
SEED = 764952063587760
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run MR-IQA-2 Actor, Editor, and Judge on one GPU."
)
parser.add_argument("image", help="Input image")
parser.add_argument("--output-dir", default="outputs/quick_start")
parser.add_argument("--gpu", default="0", help="Physical GPU index")
parser.add_argument("--actor-env", default="mr_iqa_actor_judge")
parser.add_argument("--editor-env", default="mr_iqa_editor")
parser.add_argument("--actor-python", default="")
parser.add_argument("--editor-python", default="")
parser.add_argument("--actor-model", default=REPO_ID)
parser.add_argument("--actor-subfolder", default="actor")
parser.add_argument("--judge-model", default=REPO_ID)
parser.add_argument("--judge-subfolder", default="judge")
parser.add_argument(
"--editor-model",
default="",
help="Local Editor directory; omitted means REPO_ID/editor",
)
parser.add_argument(
"--revision",
default="",
help="Optional Hugging Face revision; defaults to the current model release",
)
parser.add_argument("--max-pixels", type=int, default=196608)
parser.add_argument("--local-files-only", action="store_true")
parser.add_argument(
"--stage",
choices=("actor", "editor", "judge"),
help=argparse.SUPPRESS,
)
args = parser.parse_args(argv)
if args.max_pixels < 256:
parser.error("--max-pixels must be at least 256")
return args
def _actor_stage(args: argparse.Namespace, image_path: Path, output_dir: Path) -> None:
from actor_to_editor import (
atomic_write_json,
atomic_write_text,
generate_actor_completion,
parse_valid_actor_output,
)
actor_args = argparse.Namespace(
actor_model=args.actor_model,
actor_subfolder=args.actor_subfolder,
actor_revision=(
""
if Path(args.actor_model).expanduser().is_dir()
else args.revision
),
device="cuda:0",
dtype="bfloat16",
attn_implementation="sdpa",
max_new_tokens=512,
max_pixels=args.max_pixels,
seed=SEED,
local_files_only=args.local_files_only,
)
completion = generate_actor_completion(image_path, actor_args)
payload = parse_valid_actor_output(completion)
atomic_write_text(output_dir / "actor_raw.txt", completion)
atomic_write_json(output_dir / "assessment.json", payload)
print(json.dumps(payload, ensure_ascii=False, indent=2))
def _editor_size(width: int, height: int, max_pixels: int) -> tuple[int, int]:
scale = min(1.0, math.sqrt(float(max_pixels) / float(width * height)))
resized_width = max(16, int(width * scale) // 16 * 16)
resized_height = max(16, int(height * scale) // 16 * 16)
return resized_width, resized_height
def _editor_stage(args: argparse.Namespace, image_path: Path, output_dir: Path) -> None:
import torch
from diffusers import Flux2KleinPipeline
from huggingface_hub import snapshot_download
from PIL import Image
from actor_to_editor import atomic_write_json
assessment_path = output_dir / "assessment.json"
assessment = json.loads(assessment_path.read_text(encoding="utf-8"))
reasoning = assessment.get("reasoning")
if not isinstance(reasoning, dict):
raise ValueError("assessment.json has no reasoning object")
solution = reasoning.get("solution")
if not isinstance(solution, str) or not solution.strip():
raise ValueError("assessment.json has no usable solution")
if args.editor_model:
editor_path = Path(args.editor_model).expanduser().resolve(strict=True)
else:
download_kwargs: dict[str, Any] = {
"repo_id": REPO_ID,
"allow_patterns": ["editor/**"],
"local_files_only": args.local_files_only,
}
if args.revision:
download_kwargs["revision"] = args.revision
snapshot = Path(
snapshot_download(**download_kwargs)
)
editor_path = snapshot / "editor"
editor = Flux2KleinPipeline.from_pretrained(
editor_path,
torch_dtype=torch.bfloat16,
local_files_only=True,
).to("cuda")
with Image.open(image_path) as opened:
source = opened.convert("RGB")
edit_width, edit_height = _editor_size(
source.width,
source.height,
args.max_pixels,
)
editor_input = source.resize(
(edit_width, edit_height),
Image.Resampling.LANCZOS,
)
with torch.inference_mode():
edited = editor(
prompt=solution,
image=editor_input,
width=edit_width,
height=edit_height,
num_inference_steps=4,
sigmas=[1.0, 0.75, 0.5, 0.25],
guidance_scale=1.0,
generator=torch.Generator(device="cuda").manual_seed(SEED),
max_sequence_length=512,
text_encoder_out_layers=(9, 18, 27),
).images[0].convert("RGB")
if edited.size != source.size:
edited = edited.resize(source.size, Image.Resampling.LANCZOS)
edited_path = output_dir / "edited.png"
edited.save(edited_path)
result = {
"input_image": str(image_path),
"assessment": assessment,
"edited_image": str(edited_path),
"actor_model": (
f"{args.actor_model}/{args.actor_subfolder}"
if args.actor_subfolder and not Path(args.actor_model).expanduser().is_dir()
else args.actor_model
),
"editor_model": args.editor_model or f"{REPO_ID}/editor",
"revision": args.revision or None,
"seed": SEED,
"original_size": [source.width, source.height],
"inference_size": [edit_width, edit_height],
"solution_forwarded_verbatim": (
solution == assessment["reasoning"]["solution"]
),
"judge": None,
}
atomic_write_json(output_dir / "result.json", result)
def _quality_delta(original: dict[str, Any], edited: dict[str, Any]) -> float:
original_score = original.get("mean")
edited_score = edited.get("mean")
if not isinstance(original_score, (int, float)):
raise ValueError("Judge did not return a valid score for the input image")
if not isinstance(edited_score, (int, float)):
raise ValueError("Judge did not return a valid score for the edited image")
return float(edited_score) - float(original_score)
def _resolve_model_subfolder(
model: str,
subfolder: str,
revision: str,
local_files_only: bool,
) -> Path:
from huggingface_hub import snapshot_download
local_path = Path(model).expanduser()
if local_path.is_dir():
candidate = (
local_path
if (local_path / "config.json").is_file()
else local_path / subfolder
)
return candidate.resolve(strict=True)
download_kwargs: dict[str, Any] = {
"repo_id": model,
"allow_patterns": [f"{subfolder}/**"],
"local_files_only": local_files_only,
}
if revision:
download_kwargs["revision"] = revision
snapshot = Path(snapshot_download(**download_kwargs))
return (snapshot / subfolder).resolve(strict=True)
def _judge_stage(args: argparse.Namespace, image_path: Path, output_dir: Path) -> None:
from actor_to_editor import atomic_write_json
judge_path = _resolve_model_subfolder(
args.judge_model,
args.judge_subfolder,
args.revision,
args.local_files_only,
)
os.environ["VF_JUDGE_MODEL_PATH"] = str(judge_path)
os.environ["VF_JUDGE_MODEL_ID"] = "mr-iqa-2-e5-judge"
os.environ["VF_JUDGE_PROMPT_SCHEMA"] = "e5_training_reasoning_v5"
os.environ["VF_JUDGER_MAX_BATCH_SIZE"] = "1"
from judge.server import FrozenJudger
edited_path = (output_dir / "edited.png").resolve(strict=True)
judger = FrozenJudger(str(judge_path))
original = judger.score_image(str(image_path), repeats=1)
edited = judger.score_image(str(edited_path), repeats=1)
delta = _quality_delta(original, edited)
evaluation = {
"j0": original["mean"],
"j1": edited["mean"],
"j1_minus_j0": delta,
"original": original,
"edited": edited,
"judge_model": str(judge_path),
"revision": args.revision or None,
}
atomic_write_json(output_dir / "evaluation.json", evaluation)
result_path = output_dir / "result.json"
result = json.loads(result_path.read_text(encoding="utf-8"))
result["judge"] = evaluation
atomic_write_json(result_path, result)
def _stage_command(
*,
conda: str,
environment: str,
python_executable: str,
stage: str,
args: argparse.Namespace,
image_path: Path,
output_dir: Path,
) -> list[str]:
command = (
[python_executable]
if python_executable
else [conda, "run", "--no-capture-output", "-n", environment, "python"]
)
command.extend(
[
str(Path(__file__).resolve()),
str(image_path),
"--output-dir",
str(output_dir),
"--gpu",
"0",
"--max-pixels",
str(args.max_pixels),
"--actor-model",
args.actor_model,
"--actor-subfolder",
args.actor_subfolder,
"--judge-model",
args.judge_model,
"--judge-subfolder",
args.judge_subfolder,
"--editor-model",
args.editor_model,
"--stage",
stage,
]
)
if args.local_files_only:
command.append("--local-files-only")
if args.revision:
command.extend(["--revision", args.revision])
return command
def run_sequential(
args: argparse.Namespace,
*,
command_runner: Callable[..., Any] = subprocess.run,
) -> dict[str, Any]:
image_path = Path(args.image).expanduser().resolve(strict=True)
if not image_path.is_file():
raise FileNotFoundError(f"input image is not a file: {image_path}")
output_dir = Path(args.output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
conda = shutil.which("conda") or ""
if not conda and (not args.actor_python or not args.editor_python):
raise RuntimeError("conda is required to run the two validated environments")
child_environment = os.environ.copy()
child_environment["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
child_environment["PYTHONUNBUFFERED"] = "1"
for stage, environment, python_executable in (
("actor", args.actor_env, args.actor_python),
("editor", args.editor_env, args.editor_python),
("judge", args.actor_env, args.actor_python),
):
command_runner(
_stage_command(
conda=conda,
environment=environment,
python_executable=python_executable,
stage=stage,
args=args,
image_path=image_path,
output_dir=output_dir,
),
check=True,
env=child_environment,
)
return json.loads((output_dir / "result.json").read_text(encoding="utf-8"))
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
image_path = Path(args.image).expanduser().resolve(strict=True)
output_dir = Path(args.output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
if args.stage == "actor":
_actor_stage(args, image_path, output_dir)
return 0
if args.stage == "editor":
_editor_stage(args, image_path, output_dir)
return 0
if args.stage == "judge":
_judge_stage(args, image_path, output_dir)
return 0
result = run_sequential(args)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())