"""Image tensor output helpers."""
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import subprocess
import tempfile
import numpy as np
from PIL import Image
import torch
from diffusion_cli.config import (
DEFAULT_OUTPUT_EXTENSION,
DEFAULT_OUTPUT_QUALITY,
)
from diffusion_cli.errors import DiffusionCliError
def outputPathWithExtension(output: Path, extension: str) -> Path:
"""Return output with the normalized image extension."""
return output.with_suffix(f".{extension}")
def outputPaths(
output: Path,
batch_size: int,
output_extension: str = DEFAULT_OUTPUT_EXTENSION,
) -> list[Path]:
"""Return output file names for a batch."""
output = outputPathWithExtension(output, output_extension)
if batch_size == 1:
return [output]
return [
output.with_name(f"{output.stem}_{index:04d}{output.suffix}")
for index in range(batch_size)
]
def saveImages(
images: torch.Tensor,
output: Path,
output_extension: str = DEFAULT_OUTPUT_EXTENSION,
output_quality: int = DEFAULT_OUTPUT_QUALITY,
) -> list[Path]:
"""Save an NCHW image tensor in [0, 1] as final image files."""
image_arrays = imageArrays(images)
paths = outputPaths(output, len(image_arrays), output_extension)
for image_array, path in zip(image_arrays, paths, strict=True):
if output_extension == "png":
Image.fromarray(np.asarray(image_array), mode="RGB").save(path)
else:
with tempfile.TemporaryDirectory() as temp_dir:
temp_png = Path(temp_dir) / "input.png"
Image.fromarray(np.asarray(image_array), mode="RGB").save(
temp_png,
)
convertPngFile(
temp_png,
path,
output_extension,
output_quality,
)
return paths
def imageArrays(images: torch.Tensor) -> np.ndarray:
"""Convert an NCHW image tensor in [0, 1] to NHWC uint8 arrays."""
if images.ndim != 4:
raise ValueError(f"Expected NCHW image tensor, got {images.shape}")
if images.shape[1] != 3:
raise ValueError(f"Expected three image channels, got {images.shape}")
return (
images.detach()
.float()
.clamp(0, 1)
.permute(0, 2, 3, 1)
.mul(255)
.round()
.to(torch.uint8)
.cpu()
.numpy()
)
def encodeImages(
images: torch.Tensor,
output_extension: str = DEFAULT_OUTPUT_EXTENSION,
output_quality: int = DEFAULT_OUTPUT_QUALITY,
) -> list[bytes]:
"""Encode an NCHW image tensor in [0, 1] to final image bytes."""
image_arrays = imageArrays(images)
return [
encodeArray(image_array, output_extension, output_quality)
for image_array in image_arrays
]
def convertPngFile(
png_path: Path,
output_path: Path,
output_extension: str,
output_quality: int,
) -> None:
"""Convert one PNG file to the requested output format."""
try:
subprocess.run(
[
"magick",
str(png_path),
"-quality",
str(output_quality),
str(output_path),
],
check=True,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise DiffusionCliError(
"ImageMagick executable not found: magick"
) from exc
except subprocess.CalledProcessError as exc:
message = (exc.stderr or "").strip() or (exc.stdout or "").strip()
if not message:
message = f"exit code {exc.returncode}"
raise DiffusionCliError(
f"ImageMagick failed to write {output_extension} output: "
f"{message}"
) from exc
if not output_path.is_file():
raise DiffusionCliError(
f"ImageMagick did not create output file: {output_path}"
)
def encodeArray(
image_array: np.ndarray,
output_extension: str,
output_quality: int,
) -> bytes:
"""Encode one RGB image array to final image bytes."""
if output_extension == "png":
output = BytesIO()
Image.fromarray(np.asarray(image_array), mode="RGB").save(
output,
format="PNG",
)
return output.getvalue()
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
input_png = temp_path / "input.png"
output_path = temp_path / f"output.{output_extension}"
Image.fromarray(np.asarray(image_array), mode="RGB").save(input_png)
convertPngFile(
input_png,
output_path,
output_extension,
output_quality,
)
return output_path.read_bytes()