CarPurchaseAdvisor / src /vision_features.py
ochsncon's picture
Upload 19 files
2c527f4 verified
Raw
History Blame Contribute Delete
4.56 kB
"""Feature extraction helpers for the local vehicle image classifier."""
from __future__ import annotations
import numpy as np
from PIL import Image
def ensure_pil_image(image: Image.Image | np.ndarray | None) -> Image.Image:
if image is None:
raise ValueError("No image provided.")
if isinstance(image, np.ndarray):
return Image.fromarray(image.astype("uint8"))
if not isinstance(image, Image.Image):
raise TypeError("Input is not a valid image.")
return image
def extract_image_features(image: Image.Image | np.ndarray | None) -> np.ndarray:
"""Extract rich feature set from car images using multiple methods.
Combines:
- Color statistics (RGB/HSV/LAB mean/std, histograms)
- HOG (Histogram of Oriented Gradients) for shape/edge info
- LBP (Local Binary Pattern) for texture
- Spatial grid features (multi-scale)
- Edge density and contrast
"""
pil_image = ensure_pil_image(image).convert("RGB").resize((128, 128))
arr = np.asarray(pil_image, dtype=np.float32) / 255.0
# === Color Features ===
# RGB statistics
rgb_mean = arr.mean(axis=(0, 1))
rgb_std = arr.std(axis=(0, 1))
# Per-channel histograms (16 bins each = better granularity)
color_hist_features = []
for channel_index in range(3):
ch_hist, _ = np.histogram(arr[:, :, channel_index], bins=16, range=(0.0, 1.0), density=True)
color_hist_features.append(ch_hist.astype(np.float32))
color_hist = np.concatenate(color_hist_features)
# Saturation & Value channels (approximated from RGB)
# S = (max - min) / max, V = max
rgb_max = arr.max(axis=2)
rgb_min = arr.min(axis=2)
saturation = (rgb_max - rgb_min) / np.clip(rgb_max, 1e-6, 1.0)
value = rgb_max
s_mean = float(saturation.mean())
s_std = float(saturation.std())
v_mean = float(value.mean())
v_std = float(value.std())
# Grayscale features
gray = arr.mean(axis=2)
gray_hist, _ = np.histogram(gray, bins=32, range=(0.0, 1.0), density=True)
# === Edge & Gradient Features (simplified HOG) ===
grad_x = np.gradient(gray, axis=1)
grad_y = np.gradient(gray, axis=0)
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
grad_dir = np.arctan2(grad_y, grad_x)
# Quantized gradient direction histogram (8 bins)
grad_hist, _ = np.histogram(grad_dir, bins=8, range=(-np.pi, np.pi), density=True)
grad_mag_mean = float(grad_mag.mean())
grad_mag_std = float(grad_mag.std())
# === Texture Features (LBP-inspired) ===
# Local Binary Pattern: compare each pixel with 8 neighbors
lbp_features = _compute_lbp_features(gray)
# === Spatial Features (multi-scale grids) ===
# Coarse grids at different scales for spatial information
coarse_4x4 = np.asarray(pil_image.convert("L").resize((4, 4)), dtype=np.float32) / 255.0
coarse_8x8 = np.asarray(pil_image.convert("L").resize((8, 8)), dtype=np.float32) / 255.0
coarse_16x16 = np.asarray(pil_image.convert("L").resize((16, 16)), dtype=np.float32) / 255.0
# === Contrast & Edge Density ===
contrast = float(gray.std())
edge_density = float(np.mean(grad_mag > 0.1)) # Proportion of "edgy" pixels
# Concatenate all features
features = np.concatenate([
rgb_mean,
rgb_std,
color_hist,
np.array([s_mean, s_std, v_mean, v_std], dtype=np.float32),
gray_hist.astype(np.float32),
grad_hist.astype(np.float32),
np.array([grad_mag_mean, grad_mag_std], dtype=np.float32),
lbp_features.astype(np.float32),
coarse_4x4.flatten().astype(np.float32),
coarse_8x8.flatten().astype(np.float32),
coarse_16x16.flatten().astype(np.float32),
np.array([contrast, edge_density], dtype=np.float32),
])
return features
def _compute_lbp_features(gray: np.ndarray, n_bins: int = 10) -> np.ndarray:
"""Compute simplified LBP (Local Binary Pattern) histogram.
Compare each pixel with its 8 neighbors to extract local texture patterns.
"""
h, w = gray.shape
lbp = np.zeros((h, w), dtype=np.uint8)
# Simplified LBP: compare with mean of neighbors
for i in range(1, h - 1):
for j in range(1, w - 1):
center = gray[i, j]
neighbors = gray[i-1:i+2, j-1:j+2].flatten()
# Count neighbors brighter than center (0-8 possible)
lbp[i, j] = np.sum(neighbors > center)
# Histogram of LBP values (0-8 range)
hist, _ = np.histogram(lbp, bins=n_bins, range=(0, 9), density=True)
return hist