A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 22:35 UTC
feat(multimodal): Add customizable background color for RGBA to RGB conversion (#22052)
Signed-off-by: Jinheng Li <ahengljh@gmail.com> Co-authored-by: Jinheng Li <ahengljh@gmail.com>
Gamhang committed
Aug 1, 2025 at 21:07 UTC
0a6d305e0f7b63b06c87bb1f7564ae8d148a3311
3 files changed
+190
-6
docs/features/multimodal_inputs.md
+44
@@ -172,6 +172,36 @@ Multi-image input can be extended to perform video captioning. We show this with
172
print(generated_text)
173
```
174
175
+#### Custom RGBA Background Color
176
+
177
+When loading RGBA images (images with transparency), vLLM converts them to RGB format. By default, transparent pixels are replaced with white background. You can customize this background color using the `rgba_background_color` parameter in `media_io_kwargs`.
178
+
179
+??? code
180
+
181
+ ```python
182
+ from vllm import LLM
183
+
184
+ # Default white background (no configuration needed)
185
+ llm = LLM(model="llava-hf/llava-1.5-7b-hf")
186
+
187
+ # Custom black background for dark theme
188
+ llm = LLM(
189
+ model="llava-hf/llava-1.5-7b-hf",
190
+ media_io_kwargs={"image": {"rgba_background_color": [0, 0, 0]}}
191
+ )
192
+
193
+ # Custom brand color background (e.g., blue)
194
+ llm = LLM(
195
+ model="llava-hf/llava-1.5-7b-hf",
196
+ media_io_kwargs={"image": {"rgba_background_color": [0, 0, 255]}}
197
+ )
198
+ ```
199
+
200
+!!! note
201
+ - The `rgba_background_color` accepts RGB values as a list `[R, G, B]` or tuple `(R, G, B)` where each value is 0-255
202
+ - This setting only affects RGBA images with transparency; RGB images are unchanged
203
+ - If not specified, the default white background `(255, 255, 255)` is used for backward compatibility
204
+
205
### Video Inputs
206
207
You can pass a list of NumPy arrays directly to the `'video'` field of the multi-modal dictionary
@@ -478,6 +508,20 @@ Full example: <gh-file:examples/online_serving/openai_chat_completion_client_for
508
export VLLM_VIDEO_FETCH_TIMEOUT=<timeout>
509
```
510
511
+#### Custom RGBA Background Color
512
+
513
+To use a custom background color for RGBA images, pass the `rgba_background_color` parameter via `--media-io-kwargs`:
514
+
515
+```bash
516
+# Example: Black background for dark theme
517
+vllm serve llava-hf/llava-1.5-7b-hf \
518
+ --media-io-kwargs '{"image": {"rgba_background_color": [0, 0, 0]}}'
519
+
520
+# Example: Custom gray background
521
+vllm serve llava-hf/llava-1.5-7b-hf \
522
+ --media-io-kwargs '{"image": {"rgba_background_color": [128, 128, 128]}}'
523
+```
524
+
525
### Audio Inputs
526
527
Audio input is supported according to [OpenAI Audio API](https://platform.openai.com/docs/guides/audio?audio-generation-quickstart-example=audio-in).
tests/multimodal/test_image.py
+114
-1
@@ -3,9 +3,10 @@
3
from pathlib import Path
4
5
import numpy as np
6
+import pytest
7
from PIL import Image, ImageChops
8
8
-from vllm.multimodal.image import convert_image_mode
9
+from vllm.multimodal.image import ImageMediaIO, convert_image_mode
10
11
ASSETS_DIR = Path(__file__).parent / "assets"
12
assert ASSETS_DIR.exists()
@@ -35,3 +36,115 @@ def test_rgba_to_rgb():
36
assert converted_image_numpy[i][j][0] == 255
37
assert converted_image_numpy[i][j][1] == 255
38
assert converted_image_numpy[i][j][2] == 255
39
+
40
+
41
+def test_rgba_to_rgb_custom_background(tmp_path):
42
+ """Test RGBA to RGB conversion with custom background colors."""
43
+ # Create a simple RGBA image with transparent and opaque pixels
44
+ rgba_image = Image.new("RGBA", (10, 10),
45
+ (255, 0, 0, 255)) # Red with full opacity
46
+
47
+ # Make top-left quadrant transparent
48
+ for i in range(5):
49
+ for j in range(5):
50
+ rgba_image.putpixel((i, j), (0, 0, 0, 0)) # Fully transparent
51
+
52
+ # Save the test image to tmp_path
53
+ test_image_path = tmp_path / "test_rgba.png"
54
+ rgba_image.save(test_image_path)
55
+
56
+ # Test 1: Default white background (backward compatibility)
57
+ image_io_default = ImageMediaIO()
58
+ converted_default = image_io_default.load_file(test_image_path)
59
+ default_numpy = np.array(converted_default)
60
+
61
+ # Check transparent pixels are white
62
+ assert default_numpy[0][0][0] == 255 # R
63
+ assert default_numpy[0][0][1] == 255 # G
64
+ assert default_numpy[0][0][2] == 255 # B
65
+ # Check opaque pixels remain red
66
+ assert default_numpy[5][5][0] == 255 # R
67
+ assert default_numpy[5][5][1] == 0 # G
68
+ assert default_numpy[5][5][2] == 0 # B
69
+
70
+ # Test 2: Custom black background via kwargs
71
+ image_io_black = ImageMediaIO(rgba_background_color=(0, 0, 0))
72
+ converted_black = image_io_black.load_file(test_image_path)
73
+ black_numpy = np.array(converted_black)
74
+
75
+ # Check transparent pixels are black
76
+ assert black_numpy[0][0][0] == 0 # R
77
+ assert black_numpy[0][0][1] == 0 # G
78
+ assert black_numpy[0][0][2] == 0 # B
79
+ # Check opaque pixels remain red
80
+ assert black_numpy[5][5][0] == 255 # R
81
+ assert black_numpy[5][5][1] == 0 # G
82
+ assert black_numpy[5][5][2] == 0 # B
83
+
84
+ # Test 3: Custom blue background via kwargs (as list)
85
+ image_io_blue = ImageMediaIO(rgba_background_color=[0, 0, 255])
86
+ converted_blue = image_io_blue.load_file(test_image_path)
87
+ blue_numpy = np.array(converted_blue)
88
+
89
+ # Check transparent pixels are blue
90
+ assert blue_numpy[0][0][0] == 0 # R
91
+ assert blue_numpy[0][0][1] == 0 # G
92
+ assert blue_numpy[0][0][2] == 255 # B
93
+
94
+ # Test 4: Test with load_bytes method
95
+ with open(test_image_path, 'rb') as f:
96
+ image_data = f.read()
97
+
98
+ image_io_green = ImageMediaIO(rgba_background_color=(0, 255, 0))
99
+ converted_green = image_io_green.load_bytes(image_data)
100
+ green_numpy = np.array(converted_green)
101
+
102
+ # Check transparent pixels are green
103
+ assert green_numpy[0][0][0] == 0 # R
104
+ assert green_numpy[0][0][1] == 255 # G
105
+ assert green_numpy[0][0][2] == 0 # B
106
+
107
+
108
+def test_rgba_background_color_validation():
109
+ """Test that invalid rgba_background_color values are properly rejected."""
110
+
111
+ # Test invalid types
112
+ with pytest.raises(ValueError,
113
+ match="rgba_background_color must be a list or tuple"):
114
+ ImageMediaIO(rgba_background_color="255,255,255")
115
+
116
+ with pytest.raises(ValueError,
117
+ match="rgba_background_color must be a list or tuple"):
118
+ ImageMediaIO(rgba_background_color=255)
119
+
120
+ # Test wrong number of elements
121
+ with pytest.raises(ValueError,
122
+ match="rgba_background_color must be a list or tuple"):
123
+ ImageMediaIO(rgba_background_color=(255, 255))
124
+
125
+ with pytest.raises(ValueError,
126
+ match="rgba_background_color must be a list or tuple"):
127
+ ImageMediaIO(rgba_background_color=(255, 255, 255, 255))
128
+
129
+ # Test non-integer values
130
+ with pytest.raises(ValueError,
131
+ match="rgba_background_color must be a list or tuple"):
132
+ ImageMediaIO(rgba_background_color=(255.0, 255.0, 255.0))
133
+
134
+ with pytest.raises(ValueError,
135
+ match="rgba_background_color must be a list or tuple"):
136
+ ImageMediaIO(rgba_background_color=(255, "255", 255))
137
+
138
+ # Test out of range values
139
+ with pytest.raises(ValueError,
140
+ match="rgba_background_color must be a list or tuple"):
141
+ ImageMediaIO(rgba_background_color=(256, 255, 255))
142
+
143
+ with pytest.raises(ValueError,
144
+ match="rgba_background_color must be a list or tuple"):
145
+ ImageMediaIO(rgba_background_color=(255, -1, 255))
146
+
147
+ # Test that valid values work
148
+ ImageMediaIO(rgba_background_color=(0, 0, 0)) # Should not raise
149
+ ImageMediaIO(rgba_background_color=[255, 255, 255]) # Should not raise
150
+ ImageMediaIO(rgba_background_color=(128, 128, 128)) # Should not raise
vllm/multimodal/image.py
+32
-5
@@ -3,6 +3,7 @@
3
4
from io import BytesIO
5
from pathlib import Path
6
+from typing import Union
7
8
import pybase64
9
import torch
@@ -23,9 +24,10 @@ def rescale_image_size(image: Image.Image,
24
return image
25
26
26
-# TODO: Support customizable background color to fill in.
27
def rgba_to_rgb(
28
- image: Image.Image, background_color=(255, 255, 255)) -> Image.Image:
28
+ image: Image.Image,
29
+ background_color: Union[tuple[int, int, int], list[int]] = (255, 255, 255)
30
+) -> Image.Image:
31
"""Convert an RGBA image to RGB with filled background color."""
32
assert image.mode == "RGBA"
33
converted = Image.new("RGB", image.size, background_color)
@@ -55,10 +57,35 @@ class ImageMediaIO(MediaIO[Image.Image]):
57
# for flexible control.
58
self.kwargs = kwargs
59
60
+ # Extract RGBA background color from kwargs if provided
61
+ # Default to white background for backward compatibility
62
+ rgba_bg = kwargs.get('rgba_background_color', (255, 255, 255))
63
+ # Convert list to tuple for consistency
64
+ if isinstance(rgba_bg, list):
65
+ rgba_bg = tuple(rgba_bg)
66
+
67
+ # Validate rgba_background_color format
68
+ if not (isinstance(rgba_bg, tuple) and len(rgba_bg) == 3
69
+ and all(isinstance(c, int) and 0 <= c <= 255
70
+ for c in rgba_bg)):
71
+ raise ValueError(
72
+ "rgba_background_color must be a list or tuple of 3 integers "
73
+ "in the range [0, 255].")
74
+ self.rgba_background_color = rgba_bg
75
+
76
+ def _convert_image_mode(self, image: Image.Image) -> Image.Image:
77
+ """Convert image mode with custom background color."""
78
+ if image.mode == self.image_mode:
79
+ return image
80
+ elif image.mode == "RGBA" and self.image_mode == "RGB":
81
+ return rgba_to_rgb(image, self.rgba_background_color)
82
+ else:
83
+ return convert_image_mode(image, self.image_mode)
84
+
85
def load_bytes(self, data: bytes) -> Image.Image:
86
image = Image.open(BytesIO(data))
87
image.load()
61
- return convert_image_mode(image, self.image_mode)
88
+ return self._convert_image_mode(image)
89
90
def load_base64(self, media_type: str, data: str) -> Image.Image:
91
return self.load_bytes(pybase64.b64decode(data, validate=True))
@@ -66,7 +93,7 @@ class ImageMediaIO(MediaIO[Image.Image]):
93
def load_file(self, filepath: Path) -> Image.Image:
94
image = Image.open(filepath)
95
image.load()
69
- return convert_image_mode(image, self.image_mode)
96
+ return self._convert_image_mode(image)
97
98
def encode_base64(
99
self,
@@ -77,7 +104,7 @@ class ImageMediaIO(MediaIO[Image.Image]):
104
image = media
105
106
with BytesIO() as buffer:
80
- image = convert_image_mode(image, self.image_mode)
107
+ image = self._convert_image_mode(image)
108
image.save(buffer, image_format)
109
data = buffer.getvalue()
110