Skip to content

Commit f29d1ae

Browse files
committed
feat: faster image resizing
1 parent 70fdf2b commit f29d1ae

1 file changed

Lines changed: 82 additions & 17 deletions

File tree

src/athena_client/client/transformers/image_resizer.py

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,119 @@
1-
"""Image resizer that ensures all images match expected dimensions."""
1+
"""Optimized image resizer that ensures all images match expected dimensions."""
22

33
import asyncio
44
from collections.abc import AsyncIterator
55
from io import BytesIO
66

77
from PIL import Image
8-
from PIL.Image import Image as PILImage
98

109
from athena_client.client.consts import EXPECTED_HEIGHT, EXPECTED_WIDTH
1110
from athena_client.client.models import ImageData
1211
from athena_client.client.transformers.async_transformer import (
1312
AsyncTransformer,
1413
)
1514

15+
# Global optimization caches and constants
16+
_MAX_BUFFER_POOL_SIZE = 10
17+
_buffer_pool = []
18+
_target_size = (EXPECTED_WIDTH, EXPECTED_HEIGHT)
19+
_expected_raw_size = EXPECTED_WIDTH * EXPECTED_HEIGHT * 3
20+
21+
22+
def _get_buffer() -> BytesIO:
23+
"""Get a reusable BytesIO buffer from pool."""
24+
if _buffer_pool:
25+
buffer = _buffer_pool.pop()
26+
buffer.seek(0)
27+
buffer.truncate(0)
28+
return buffer
29+
return BytesIO()
30+
31+
32+
def _return_buffer(buffer: BytesIO) -> None:
33+
"""Return buffer to pool for reuse."""
34+
if len(_buffer_pool) < _MAX_BUFFER_POOL_SIZE:
35+
_buffer_pool.append(buffer)
36+
37+
38+
def _is_raw_rgb(data: bytes) -> bool:
39+
"""Detect if data is already a raw 448x448x3 RGB array."""
40+
return (
41+
len(data) == _expected_raw_size
42+
and not data.startswith(b"\x89PNG")
43+
and not data.startswith(b"\xff\xd8\xff") # JPEG
44+
and not data.startswith(b"GIF")
45+
and not data.startswith(b"BM") # BMP
46+
and not data.startswith(b"RIFF") # WebP
47+
)
48+
1649

1750
class ImageResizer(AsyncTransformer[ImageData, ImageData]):
18-
"""Transform ImageData to ensure expected dimensions."""
51+
"""Transform ImageData to ensure expected dimensions with optimization."""
1952

2053
def __init__(self, source: AsyncIterator[ImageData]) -> None:
21-
"""Initialize with source iterator and optional size overrides.
54+
"""Initialize with source iterator.
2255
2356
Args:
2457
source: Iterator yielding ImageData objects
2558
2659
"""
2760
super().__init__(source)
28-
self.target_size = (EXPECTED_WIDTH, EXPECTED_HEIGHT)
29-
30-
def _resize_image(self, image: PILImage) -> PILImage:
31-
"""Resize image to expected dimensions."""
32-
return image.resize(self.target_size)
3361

3462
async def transform(self, data: ImageData) -> ImageData:
3563
"""Transform ImageData by resizing to expected dimensions."""
3664

3765
def process_image() -> bytes:
38-
with Image.open(BytesIO(data.data)) as image:
39-
image_to_resize = image
66+
# Fast path for raw RGB arrays
67+
if _is_raw_rgb(data.data):
68+
# Already correct size raw RGB, convert to PNG efficiently
69+
image = Image.frombytes("RGB", _target_size, data.data)
70+
output_buffer = _get_buffer()
71+
try:
72+
image.save(output_buffer, format="PNG", optimize=False)
73+
return output_buffer.getvalue()
74+
finally:
75+
_return_buffer(output_buffer)
76+
77+
# Standard path for encoded images
78+
input_buffer = BytesIO(data.data)
4079

80+
with Image.open(input_buffer) as image:
81+
# Early exit for already-correct images
82+
if image.size == _target_size and image.mode == "RGB":
83+
output_buffer = _get_buffer()
84+
try:
85+
image.save(output_buffer, format="PNG", optimize=False)
86+
return output_buffer.getvalue()
87+
finally:
88+
_return_buffer(output_buffer)
89+
90+
# Convert to RGB if needed
4191
if image.mode != "RGB":
42-
image_to_resize = image.convert("RGB")
92+
rgb_image = image.convert("RGB")
93+
else:
94+
rgb_image = image
4395

44-
resized = self._resize_image(image_to_resize)
96+
# Resize if needed
97+
if rgb_image.size != _target_size:
98+
resized_image = rgb_image.resize(
99+
_target_size, Image.Resampling.LANCZOS
100+
)
101+
else:
102+
resized_image = rgb_image
45103

46-
# Save as PNG to maintain quality and preserve format
47-
output_buffer = BytesIO()
48-
resized.save(output_buffer, format="PNG")
49-
return output_buffer.getvalue()
104+
# Save with optimized settings
105+
output_buffer = _get_buffer()
106+
try:
107+
resized_image.save(
108+
output_buffer, format="PNG", optimize=False
109+
)
110+
return output_buffer.getvalue()
111+
finally:
112+
_return_buffer(output_buffer)
50113

114+
# Use thread pool for CPU-intensive processing
51115
resized_bytes = await asyncio.to_thread(process_image)
116+
52117
# Modify existing ImageData with new bytes and add transformation hashes
53118
data.data = resized_bytes
54119
data.add_transformation_hashes()

0 commit comments

Comments
 (0)