Use float32 instead of float64 for DCT operations

Reduces peak memory usage by ~50%:
- ENCODE: 211 MB -> 107 MB
- DECODE: 104 MB -> 52 MB

float32 provides sufficient precision for 8-bit images
(DCT roundtrip error ~1.8e-7, well under 0.5 threshold).

Significant improvement for Pi deployments with limited RAM.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-09 22:10:20 -05:00
parent c0fe85ac83
commit 55d54717f8

View File

@@ -408,28 +408,30 @@ def _safe_idct2(block: np.ndarray) -> np.ndarray:
def _to_grayscale(image_data: bytes) -> np.ndarray:
img = Image.open(io.BytesIO(image_data))
gray = img.convert("L")
return np.array(gray, dtype=np.float64, copy=True, order="C")
return np.array(gray, dtype=np.float32, copy=True, order="C")
def _extract_y_channel(image_data: bytes) -> np.ndarray:
"""Extract Y (luminance) channel - float32 for memory efficiency."""
img = Image.open(io.BytesIO(image_data))
if img.mode != "RGB":
img = img.convert("RGB")
rgb = np.array(img, dtype=np.float64, copy=True, order="C")
rgb = np.array(img, dtype=np.float32, copy=True, order="C")
Y = 0.299 * rgb[:, :, 0] + 0.587 * rgb[:, :, 1] + 0.114 * rgb[:, :, 2]
return np.array(Y, dtype=np.float64, copy=True, order="C")
return np.array(Y, dtype=np.float32, copy=True, order="C")
def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
"""Pad image to block boundaries - uses float32 for memory efficiency."""
h, w = image.shape
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
new_w = ((w + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
if new_h == h and new_w == w:
return np.array(image, dtype=np.float64, copy=True, order="C"), (h, w)
return np.array(image, dtype=np.float32, copy=True, order="C"), (h, w)
padded = np.zeros((new_h, new_w), dtype=np.float64, order="C")
padded = np.zeros((new_h, new_w), dtype=np.float32, order="C")
padded[:h, :w] = image
# Simple edge replication for padding
@@ -446,8 +448,9 @@ def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
def _unpad_image(image: np.ndarray, original_size: tuple[int, int]) -> np.ndarray:
"""Remove padding - uses float32 for memory efficiency."""
h, w = original_size
return np.array(image[:h, :w], dtype=np.float64, copy=True, order="C")
return np.array(image[:h, :w], dtype=np.float32, copy=True, order="C")
def _embed_bit_in_coeff(coef: float, bit: int, quant_step: int = QUANT_STEP) -> float:
@@ -545,20 +548,23 @@ def _rgb_to_ycbcr(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
- Cb/Cr are often subsampled (4:2:0) so Y has more capacity anyway
The coefficients here are from ITU-R BT.601 - the standard for video.
Uses float32 to reduce memory usage (~50% savings vs float64).
"""
R = rgb[:, :, 0].astype(np.float64)
G = rgb[:, :, 1].astype(np.float64)
B = rgb[:, :, 2].astype(np.float64)
# Use float32 - sufficient precision for 8-bit images, halves memory
R = rgb[:, :, 0].astype(np.float32)
G = rgb[:, :, 1].astype(np.float32)
B = rgb[:, :, 2].astype(np.float32)
# Y = luminance (brightness). Green contributes most because eyes are most sensitive to it.
Y = np.array(0.299 * R + 0.587 * G + 0.114 * B, dtype=np.float64, copy=True, order="C")
Y = np.array(0.299 * R + 0.587 * G + 0.114 * B, dtype=np.float32, copy=True, order="C")
# Cb = blue-difference chroma (centered at 128)
Cb = np.array(
128 - 0.168736 * R - 0.331264 * G + 0.5 * B, dtype=np.float64, copy=True, order="C"
128 - 0.168736 * R - 0.331264 * G + 0.5 * B, dtype=np.float32, copy=True, order="C"
)
# Cr = red-difference chroma (centered at 128)
Cr = np.array(
128 + 0.5 * R - 0.418688 * G - 0.081312 * B, dtype=np.float64, copy=True, order="C"
128 + 0.5 * R - 0.418688 * G - 0.081312 * B, dtype=np.float32, copy=True, order="C"
)
return Y, Cb, Cr
@@ -571,11 +577,12 @@ def _ycbcr_to_rgb(Y: np.ndarray, Cb: np.ndarray, Cr: np.ndarray) -> np.ndarray:
After embedding in the Y channel, we need to reconstruct RGB for display.
The Cb/Cr channels are unchanged - we only touched luminance.
"""
# Use float32 for memory efficiency
R = Y + 1.402 * (Cr - 128)
G = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128)
B = Y + 1.772 * (Cb - 128)
rgb = np.zeros((Y.shape[0], Y.shape[1], 3), dtype=np.float64, order="C")
rgb = np.zeros((Y.shape[0], Y.shape[1], 3), dtype=np.float32, order="C")
rgb[:, :, 0] = R
rgb[:, :, 1] = G
rgb[:, :, 2] = B
@@ -820,8 +827,8 @@ def _embed_scipy_dct_safe(
if img.mode == "RGBA":
img = img.convert("RGB")
# Process color image
rgb = np.array(img, dtype=np.float64, copy=True, order="C")
# Process color image (float32 for memory efficiency)
rgb = np.array(img, dtype=np.float32, copy=True, order="C")
img.close()
Y, Cb, Cr = _rgb_to_ycbcr(rgb)
@@ -899,8 +906,8 @@ def _embed_in_channel_safe(
"""
h, w = channel.shape
# Create result with explicit new memory
result = np.array(channel, dtype=np.float64, copy=True, order="C")
# Create result with explicit new memory (float32 for memory efficiency)
result = np.array(channel, dtype=np.float32, copy=True, order="C")
# Pre-compute embed positions as numpy indices
embed_rows = np.array([pos[0] for pos in DEFAULT_EMBED_POSITIONS])
@@ -923,8 +930,8 @@ def _embed_in_channel_safe(
batch_order = block_order[block_idx:batch_end]
batch_count = len(batch_order)
# Extract blocks into 3D array
blocks = np.zeros((batch_count, BLOCK_SIZE, BLOCK_SIZE), dtype=np.float64)
# Extract blocks into 3D array (float32 for memory efficiency)
blocks = np.zeros((batch_count, BLOCK_SIZE, BLOCK_SIZE), dtype=np.float32)
block_positions = []
for i, block_num in enumerate(batch_order):
by = (block_num // blocks_x) * BLOCK_SIZE
@@ -1227,8 +1234,8 @@ def _extract_scipy_dct_safe(
batch_order = block_order[block_idx:batch_end]
batch_count = len(batch_order)
# Extract blocks into 3D array (batch_count, 8, 8)
blocks = np.zeros((batch_count, BLOCK_SIZE, BLOCK_SIZE), dtype=np.float64)
# Extract blocks into 3D array (batch_count, 8, 8) - float32 for memory efficiency
blocks = np.zeros((batch_count, BLOCK_SIZE, BLOCK_SIZE), dtype=np.float32)
for i, block_num in enumerate(batch_order):
by = (block_num // blocks_x) * BLOCK_SIZE
bx = (block_num % blocks_x) * BLOCK_SIZE