Skip to content

Commit 418fa0a

Browse files
committed
Add new image compressors: JPEG2000, JPEG XL, and WebP - Implemented JPEG2000Compressor with configurable quality and lossless options. - Implemented JPEGXLCompressor with support for lossless compression and adjustable effort. - Implemented WebPCompressor with options for quality, lossless mode, and compression method. - Updated __init__.py to include new compressors in the module. - Added unit tests for JPEG2000, JPEG XL, and WebP compressors to ensure functionality. - Updated test script to include tests for new compressors. - Updated requirements to include necessary dependencies for new compressors.
1 parent 7b88c0c commit 418fa0a

File tree

9 files changed

+1143
-7
lines changed

9 files changed

+1143
-7
lines changed

docs/api_reference.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,12 @@ Image compressor models, including standard and neural network-based methods.
477477

478478
BPGCompressor
479479
BaseImageCompressor
480+
JPEG2000Compressor
480481
JPEGCompressor
482+
JPEGXLCompressor
481483
NeuralCompressor
482484
PNGCompressor
485+
WebPCompressor
483486

484487

485488
Modulations

kaira/models/image/compressors/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
from .base import BaseImageCompressor
44
from .bpg import BPGCompressor
55
from .jpeg import JPEGCompressor
6+
from .jpeg2000 import JPEG2000Compressor
7+
from .jpegxl import JPEGXLCompressor
68
from .neural import NeuralCompressor
79
from .png import PNGCompressor
10+
from .webp import WebPCompressor
811

912
__all__ = [
1013
"BaseImageCompressor",
1114
"BPGCompressor",
15+
"JPEG2000Compressor",
1216
"JPEGCompressor",
17+
"JPEGXLCompressor",
1318
"NeuralCompressor",
1419
"PNGCompressor",
20+
"WebPCompressor",
1521
]
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""JPEG 2000 image compressor using PIL/Pillow."""
2+
3+
import io
4+
from typing import Any, Optional, Tuple, Union
5+
6+
from PIL import Image
7+
8+
from kaira.models.image.compressors.base import BaseImageCompressor
9+
10+
11+
class JPEG2000Compressor(BaseImageCompressor):
12+
"""JPEG 2000 image compressor using JPEG 2000 via PIL/Pillow.
13+
14+
This class provides JPEG 2000 compression with configurable quality settings and advanced features.
15+
JPEG 2000 is a wavelet-based image compression standard that provides superior compression efficiency
16+
compared to traditional JPEG, especially at lower bit rates. It supports both lossy and lossless
17+
compression modes.
18+
19+
The quality parameter ranges from 1 (worst quality, highest compression) to 100 (best quality,
20+
lowest compression). JPEG 2000 also supports a special lossless mode when quality is set to 100.
21+
22+
Example:
23+
# Fixed quality compression
24+
compressor = JPEG2000Compressor(quality=85)
25+
compressed_images = compressor(image_batch)
26+
27+
# Bit-constrained compression
28+
compressor = JPEG2000Compressor(max_bits_per_image=4000)
29+
compressed_images, bits_used = compressor(image_batch)
30+
31+
# Lossless compression
32+
compressor = JPEG2000Compressor(quality=100)
33+
compressed_images = compressor(image_batch)
34+
35+
# With compression statistics
36+
compressor = JPEG2000Compressor(quality=90, collect_stats=True, return_bits=True)
37+
compressed_images, bits_per_image = compressor(image_batch)
38+
stats = compressor.get_stats()
39+
"""
40+
41+
def __init__(
42+
self,
43+
max_bits_per_image: Optional[int] = None,
44+
quality: Optional[int] = None,
45+
irreversible: Optional[bool] = None,
46+
progression_order: str = "LRCP",
47+
num_resolutions: int = 6,
48+
collect_stats: bool = False,
49+
return_bits: bool = True,
50+
return_compressed_data: bool = False,
51+
*args: Any,
52+
**kwargs: Any,
53+
):
54+
"""Initialize the JPEG 2000 compressor.
55+
56+
Args:
57+
max_bits_per_image: Maximum bits allowed per compressed image. If provided without
58+
quality, the compressor will find the highest quality that
59+
produces files smaller than this limit.
60+
quality: JPEG 2000 quality level (1-100, higher = better quality, larger file size).
61+
If provided, this exact quality will be used regardless of resulting file size.
62+
Quality 100 enables lossless mode unless irreversible=True is explicitly set.
63+
irreversible: Force irreversible (lossy) compression even at high quality.
64+
If None, automatically determined based on quality (>= 100 = reversible).
65+
progression_order: Progression order for encoding ("LRCP", "RLCP", "RPCL", "PCRL", "CPRL").
66+
num_resolutions: Number of resolution levels (1-33). More levels = better scalability.
67+
collect_stats: Whether to collect and return compression statistics
68+
return_bits: Whether to return bits per image in forward pass
69+
return_compressed_data: Whether to return the compressed binary data
70+
*args: Variable positional arguments passed to the base class.
71+
**kwargs: Variable keyword arguments passed to the base class.
72+
"""
73+
super().__init__(
74+
max_bits_per_image,
75+
quality,
76+
collect_stats,
77+
return_bits,
78+
return_compressed_data,
79+
*args,
80+
**kwargs,
81+
)
82+
83+
self.irreversible = irreversible
84+
self.progression_order = progression_order
85+
self.num_resolutions = num_resolutions
86+
87+
# Validate progression order
88+
valid_orders = ["LRCP", "RLCP", "RPCL", "PCRL", "CPRL"]
89+
if progression_order not in valid_orders:
90+
raise ValueError(f"Progression order must be one of {valid_orders}")
91+
92+
# Validate number of resolutions
93+
if not isinstance(num_resolutions, int) or num_resolutions < 1 or num_resolutions > 33:
94+
raise ValueError("Number of resolutions must be an integer between 1 and 33")
95+
96+
def _validate_quality(self, quality: Union[int, float]) -> None:
97+
"""Validate that the quality is within the acceptable range for JPEG 2000.
98+
99+
Args:
100+
quality: Quality level to validate (1-100 for JPEG 2000)
101+
102+
Raises:
103+
ValueError: If quality is not between 1 and 100
104+
"""
105+
if not isinstance(quality, (int, float)) or quality < 1 or quality > 100:
106+
raise ValueError("JPEG 2000 quality must be between 1 and 100")
107+
108+
def _get_quality_range(self) -> Tuple[int, int]:
109+
"""Get the valid quality range for JPEG 2000 compression.
110+
111+
Returns:
112+
Tuple of (min_quality=1, max_quality=100)
113+
"""
114+
return (1, 100)
115+
116+
def _compress_single_image(self, image: Image.Image, quality: Union[int, float], **kwargs: Any) -> Tuple[bytes, int]:
117+
"""Compress a single PIL Image using JPEG 2000.
118+
119+
Args:
120+
image: PIL Image to compress
121+
quality: JPEG 2000 quality level (1-100)
122+
**kwargs: Additional compression parameters
123+
124+
Returns:
125+
Tuple of (compressed_data_bytes, size_in_bits)
126+
"""
127+
# Ensure image is in appropriate mode for JPEG 2000
128+
# JPEG 2000 supports RGB, RGBA, L (grayscale)
129+
if image.mode not in ["RGB", "RGBA", "L"]:
130+
if image.mode == "CMYK":
131+
image = image.convert("RGB")
132+
else:
133+
image = image.convert("RGB")
134+
135+
# Create bytes buffer
136+
buffer = io.BytesIO()
137+
138+
# Determine compression mode
139+
use_irreversible = self.irreversible
140+
if use_irreversible is None:
141+
# Auto-determine based on quality
142+
use_irreversible = quality < 100
143+
144+
# Prepare save parameters
145+
save_params = {
146+
"format": "JPEG2000",
147+
"irreversible": use_irreversible,
148+
"progression": self.progression_order,
149+
"num_resolutions": self.num_resolutions,
150+
}
151+
152+
if not use_irreversible:
153+
# Lossless mode - ignore quality
154+
pass
155+
else:
156+
# Lossy mode - map quality to compression ratio
157+
# Quality 1 -> high compression ratio (100:1)
158+
# Quality 99 -> low compression ratio (2:1)
159+
compression_ratio = 100 - (quality - 1) * 98 / 98
160+
save_params["quality_mode"] = "rates"
161+
save_params["quality_layers"] = [compression_ratio]
162+
163+
# Save image as JPEG 2000
164+
try:
165+
image.save(buffer, **save_params) # type: ignore[arg-type]
166+
except Exception:
167+
# Fallback with minimal parameters if advanced features aren't supported
168+
try:
169+
if use_irreversible and quality < 100:
170+
image.save(buffer, format="JPEG2000", irreversible=True)
171+
else:
172+
image.save(buffer, format="JPEG2000")
173+
except Exception:
174+
# Final fallback - basic JPEG2000 save
175+
image.save(buffer, format="JPEG2000")
176+
177+
# Get compressed data
178+
compressed_data = buffer.getvalue()
179+
size_in_bits = len(compressed_data) * 8
180+
181+
return compressed_data, size_in_bits
182+
183+
def _decompress_single_image(self, data: bytes, **kwargs: Any) -> Image.Image:
184+
"""Decompress JPEG 2000 bytes back to a PIL Image.
185+
186+
Args:
187+
data: Compressed JPEG 2000 data as bytes
188+
**kwargs: Additional decompression parameters
189+
190+
Returns:
191+
Reconstructed PIL Image
192+
"""
193+
buffer = io.BytesIO(data)
194+
pil_image = Image.open(buffer) # type: ignore
195+
196+
# Ensure we load the image data
197+
pil_image.load()
198+
199+
# Convert to RGB if not already (for consistency, but preserve grayscale)
200+
if pil_image.mode not in ["RGB", "L", "RGBA"]:
201+
pil_image = pil_image.convert("RGB") # type: ignore
202+
203+
return pil_image
204+
205+
def compress(self, image: Image.Image, quality: Optional[int] = None) -> bytes:
206+
"""Compress a PIL Image to JPEG 2000 bytes.
207+
208+
This is a convenience method for direct compression without the full forward pass.
209+
210+
Args:
211+
image: PIL Image to compress
212+
quality: JPEG 2000 quality level (uses instance quality if not provided)
213+
214+
Returns:
215+
Compressed JPEG 2000 data as bytes
216+
"""
217+
actual_quality: Union[int, float]
218+
if quality is None:
219+
if self.quality is None:
220+
raise ValueError("Quality must be provided either during initialization or method call")
221+
actual_quality = self.quality
222+
else:
223+
actual_quality = quality
224+
225+
compressed_data, _ = self._compress_single_image(image, actual_quality)
226+
return compressed_data
227+
228+
def decompress(self, data: bytes) -> Image.Image:
229+
"""Decompress JPEG 2000 bytes to PIL Image.
230+
231+
This is a convenience method for direct decompression.
232+
233+
Args:
234+
data: Compressed JPEG 2000 data as bytes
235+
236+
Returns:
237+
Reconstructed PIL Image
238+
"""
239+
return self._decompress_single_image(data)

0 commit comments

Comments
 (0)