Saumith devarsetty
Updated Lab5 modular code
3fffbdc
"""
image_processor.py
Utility functions for image preprocessing used in the mosaic generator:
- Cropping an image so it's divisible by the grid
- Computing LAB cell means for FAISS-based tile matching
"""
import numpy as np
import cv2
from .utils import fast_rgb2lab
def crop_to_multiple(img, grid_n):
"""
Crop an RGB image so that its width and height are perfectly divisible
by the chosen grid size.
Parameters
----------
img : np.ndarray
RGB image array of shape (H, W, 3).
grid_n : int
Number of cells per side in the mosaic grid.
Returns
-------
np.ndarray
Cropped RGB image whose dimensions are multiples of `grid_n`.
Raises
------
ValueError
If `img` is not a valid image array or grid size is invalid.
Notes
-----
This does NOT resize the image — it simply trims extra pixels so that
(H % grid_n == 0) and (W % grid_n == 0).
"""
if img is None or not isinstance(img, np.ndarray):
raise ValueError("Input image must be a valid NumPy RGB array.")
if img.ndim != 3 or img.shape[2] != 3:
raise ValueError(f"Expected image shape (H, W, 3), got {img.shape}.")
if not isinstance(grid_n, int) or grid_n <= 0:
raise ValueError("grid_n must be a positive integer.")
h, w = img.shape[:2]
if h < grid_n or w < grid_n:
raise ValueError(
f"Image too small for grid size {grid_n}. "
f"Received image of size {w}x{h}."
)
new_w = (w // grid_n) * grid_n
new_h = (h // grid_n) * grid_n
return img[:new_h, :new_w]
def compute_cell_means_lab(img, grid_n):
"""
Compute LAB mean color for each grid cell in the image.
Parameters
----------
img : np.ndarray
Cropped RGB image array (H, W, 3).
grid_n : int
Grid size — number of cells per side.
Returns
-------
means : np.ndarray
Array of shape (grid_n * grid_n, 3). LAB mean per grid cell.
dims : tuple
(W, H, cell_w, cell_h)
- W, H : final image dimensions
- cell_w, cell_h : size of each grid cell in pixels
Raises
------
ValueError
If the image is not divisible by grid_n, or has unexpected shape.
Notes
-----
The function converts the full image to LAB **once**, then extracts
block means efficiently without redundant conversions.
"""
if img is None or not isinstance(img, np.ndarray):
raise ValueError("Input image must be a valid NumPy RGB array.")
if img.ndim != 3 or img.shape[2] != 3:
raise ValueError(f"Expected RGB image with 3 channels, got {img.shape}.")
if not isinstance(grid_n, int) or grid_n <= 0:
raise ValueError("grid_n must be a positive integer.")
h, w = img.shape[:2]
if h % grid_n != 0 or w % grid_n != 0:
raise ValueError(
f"Image size ({w}x{h}) is not divisible by grid size {grid_n}. "
"Call crop_to_multiple() first."
)
cell_h, cell_w = h // grid_n, w // grid_n
# Single conversion for full image
lab = fast_rgb2lab(img)
# Output: N cells × 3 channels
means = np.zeros((grid_n * grid_n, 3), dtype=np.float32)
k = 0
for gy in range(grid_n):
for gx in range(grid_n):
block = lab[gy*cell_h:(gy+1)*cell_h, gx*cell_w:(gx+1)*cell_w]
# Safe flatten + mean
means[k] = block.reshape(-1, 3).mean(axis=0)
k += 1
return means, (w, h, cell_w, cell_h)