diff --git a/examples/offline_inference/text_to_image/text_to_image.py b/examples/offline_inference/text_to_image/text_to_image.py index 81acebe00..a09a0d5fd 100644 --- a/examples/offline_inference/text_to_image/text_to_image.py +++ b/examples/offline_inference/text_to_image/text_to_image.py @@ -604,18 +604,7 @@ def main(): raise ValueError("No output generated from omni.generate()") logger.info(f"Outputs: {outputs}") - images = None - for output in outputs: - images = getattr(output, "images", None) - if images: - break - req_out = getattr(output, "request_output", None) - images = getattr(req_out, "images", None) if req_out is not None else None - if images: - break - - if not images: - images = extract_images_from_outputs(outputs) + images = extract_images_from_outputs(outputs) if not images: raise ValueError("No images found in request_output") diff --git a/tests/diffusion/test_image_output.py b/tests/diffusion/test_image_output.py new file mode 100644 index 000000000..f9cebde32 --- /dev/null +++ b/tests/diffusion/test_image_output.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import numpy as np +import pytest +from PIL import Image + +from vllm_omni.diffusion.utils.image_output import extract_images_from_outputs +from vllm_omni.outputs import OmniRequestOutput + +pytestmark = [pytest.mark.diffusion, pytest.mark.core_model, pytest.mark.cpu] + + +@pytest.mark.parametrize( + ("payload", "expected_pixel"), + [ + (np.array([[[-1.0, 0.0, 1.0]]], dtype=np.float32), (0, 127, 255)), + (np.array([[[0, 128, 255]]], dtype=np.uint8), (0, 128, 255)), + ], +) +def test_extract_images_normalizes_numpy_payload( + payload: np.ndarray, + expected_pixel: tuple[int, int, int], + tmp_path, +) -> None: + output = OmniRequestOutput(images=[payload]) + + [image] = extract_images_from_outputs(output) + + assert isinstance(image, Image.Image) + assert image.mode == "RGB" + assert image.size == (1, 1) + assert image.getpixel((0, 0)) == expected_pixel + + output_path = tmp_path / "image.png" + image.save(output_path) + assert output_path.is_file() diff --git a/vllm_omni/diffusion/utils/image_output.py b/vllm_omni/diffusion/utils/image_output.py index 5f8c9903a..5b5e02720 100644 --- a/vllm_omni/diffusion/utils/image_output.py +++ b/vllm_omni/diffusion/utils/image_output.py @@ -4,6 +4,7 @@ from collections.abc import Iterable, Mapping, Sequence from typing import Any +import numpy as np import torch from PIL import Image from vllm.outputs import RequestOutput @@ -20,6 +21,8 @@ def extract_images_from_outputs(outputs: ImageOutputs) -> list[Image.Image]: Supported inputs: - OmniRequestOutput / list[OmniRequestOutput] - RequestOutput / list[RequestOutput] (compatibility fallback) + + PIL images, NumPy arrays, and PyTorch tensors are normalized to PIL images. """ for output in _iter_known_outputs(outputs): if isinstance(output, OmniRequestOutput): @@ -58,6 +61,9 @@ def _iter_multimodal_image_payloads(output: OmniRequestOutput | RequestOutput) - def _iter_request_output_payloads(request_output: RequestOutput) -> Iterable[Any]: + images = getattr(request_output, "images", None) + if images is not None: + yield images mm = getattr(request_output, "multimodal_output", None) if mm is not None: yield from _image_values_from_mapping_like(mm) @@ -82,6 +88,8 @@ def _coerce_images(payload: Any) -> list[Image.Image]: return [] if isinstance(payload, Image.Image): return [payload] + if isinstance(payload, np.ndarray): + return _ndarray_to_images(payload) if isinstance(payload, torch.Tensor): return _tensor_to_images(payload) if isinstance(payload, list | tuple): @@ -92,6 +100,37 @@ def _coerce_images(payload: Any) -> list[Image.Image]: return [] +def _ndarray_to_images(array: np.ndarray) -> list[Image.Image]: + if array.ndim == 4: + images: list[Image.Image] = [] + for single in array: + images.extend(_ndarray_to_images(single)) + return images + if array.ndim != 3 or array.size == 0: + return [] + + image = array + if image.shape[-1] not in (1, 3, 4): + if image.shape[0] not in (1, 3, 4): + return [] + image = np.moveaxis(image, 0, -1) + + if np.issubdtype(image.dtype, np.floating): + image = image.astype(np.float32, copy=False) + if image.min() < 0.0: + image = image / 2 + 0.5 + image = np.clip(image, 0.0, 1.0) * 255 + elif np.issubdtype(image.dtype, np.integer) or np.issubdtype(image.dtype, np.bool_): + image = np.clip(image, 0, 255) + else: + return [] + + image = image.astype(np.uint8) + if image.shape[-1] == 1: + image = image[..., 0] + return [Image.fromarray(np.ascontiguousarray(image))] + + def _tensor_to_images(tensor: torch.Tensor) -> list[Image.Image]: img = tensor.detach().to("cpu", dtype=torch.float32) if img.ndim == 4: