视觉审核完成30%
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Deterministically build G01 background candidates from saved ImageGen masters."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
|
||||
def process_chroma_image(source: Image.Image, config: dict[str, Any]) -> Image.Image:
|
||||
"""Convert a green-screen master to clean RGBA with edge despill."""
|
||||
start = int(config["dominanceStart"])
|
||||
end = int(config["dominanceEnd"])
|
||||
allowance = int(config["despillAllowance"])
|
||||
if end <= start:
|
||||
raise ValueError("dominanceEnd must be greater than dominanceStart")
|
||||
|
||||
output = Image.new("RGBA", source.size, (0, 0, 0, 0))
|
||||
converted = []
|
||||
for red, green, blue, source_alpha in source.convert("RGBA").get_flattened_data():
|
||||
dominance = green - max(red, blue)
|
||||
if green >= 100 and dominance > start:
|
||||
removal = min(1.0, (dominance - start) / (end - start))
|
||||
alpha = round(source_alpha * (1.0 - removal))
|
||||
if alpha <= 2:
|
||||
converted.append((0, 0, 0, 0))
|
||||
continue
|
||||
green = min(green, max(red, blue) + allowance)
|
||||
converted.append((red, green, blue, alpha))
|
||||
else:
|
||||
converted.append((red, green, blue, source_alpha))
|
||||
output.putdata(converted)
|
||||
return output
|
||||
|
||||
|
||||
def process_opaque_image(source: Image.Image) -> Image.Image:
|
||||
"""Normalize a paper-backed master to deterministic opaque RGBA."""
|
||||
output = source.convert("RGBA")
|
||||
output.putalpha(255)
|
||||
return output
|
||||
|
||||
|
||||
def save_png(image: Image.Image, path: Path) -> None:
|
||||
"""Save a deterministic PNG with an explicit standard-sRGB chunk."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
png_info = PngImagePlugin.PngInfo()
|
||||
png_info.add(b"sRGB", b"\x00")
|
||||
image.save(path, format="PNG", optimize=True, compress_level=9, pnginfo=png_info)
|
||||
|
||||
|
||||
def _resolve_inside(workspace: Path, relative_path: str) -> Path:
|
||||
absolute = (workspace / relative_path).resolve()
|
||||
try:
|
||||
absolute.relative_to(workspace.resolve())
|
||||
except ValueError as error:
|
||||
raise ValueError(f"path escapes workspace: {relative_path}") from error
|
||||
return absolute
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def build(manifest_path: Path, workspace: Path, report_path: Path) -> list[dict[str, Any]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
chroma = manifest["chromaKey"]
|
||||
reports = []
|
||||
|
||||
for candidate in manifest["candidates"]:
|
||||
source_path = _resolve_inside(workspace, candidate["master"])
|
||||
output_path = _resolve_inside(workspace, candidate["generatedOutput"])
|
||||
with Image.open(source_path) as opened:
|
||||
expected = candidate["sourcePixels"]
|
||||
if opened.size != (expected["width"], expected["height"]):
|
||||
raise ValueError(
|
||||
f"{candidate['id']} expected {expected['width']}x{expected['height']}, got {opened.width}x{opened.height}"
|
||||
)
|
||||
if candidate["processingMode"] == "chroma-key":
|
||||
output = process_chroma_image(opened, chroma)
|
||||
elif candidate["processingMode"] == "opaque-paper":
|
||||
output = process_opaque_image(opened)
|
||||
else:
|
||||
raise ValueError(f"unsupported processingMode: {candidate['processingMode']}")
|
||||
|
||||
save_png(output, output_path)
|
||||
reports.append(
|
||||
{
|
||||
"id": candidate["id"],
|
||||
"mode": candidate["processingMode"],
|
||||
"source": candidate["master"],
|
||||
"output": candidate["generatedOutput"],
|
||||
"width": output.width,
|
||||
"height": output.height,
|
||||
"bytes": output_path.stat().st_size,
|
||||
"sha256": _sha256(output_path),
|
||||
}
|
||||
)
|
||||
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(reports, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return reports
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("manifest", type=Path)
|
||||
parser.add_argument("--workspace", type=Path, required=True)
|
||||
parser.add_argument("--report", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
reports = build(args.manifest, args.workspace.resolve(), args.report)
|
||||
for report in reports:
|
||||
print(f"G01-BACKGROUND-BUILD PASS {report['id']} {report['width']}x{report['height']} {report['sha256']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user