File size: 4,564 Bytes
2c527f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""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