File size: 7,307 Bytes
2724e1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""
ImagePreprocessor.py
Step 1: Image preprocessing for mosaic generation.
Handles image loading, resizing, cropping, and color quantization.
"""

import cv2
import numpy as np
from PIL import Image
import os
from typing import Tuple, Optional
from sklearn.cluster import MiniBatchKMeans
import warnings

class ImagePreprocessor:
    """
    Handles image preprocessing for mosaic generation.
    Features: Smart resizing, grid-perfect cropping, fast color quantization.
    """
    
    def __init__(self, target_resolution: Tuple[int, int] = (800, 600), 
                 grid_size: Tuple[int, int] = (20, 15)):
        """
        Initialize the preprocessor.
        
        Args:
            target_resolution: Target (width, height) for processed images
            grid_size: Grid dimensions (cols, rows) for mosaic
        """
        self.target_resolution = target_resolution
        self.grid_size = grid_size
        
        # Calculate tile size for perfect grid alignment
        self.tile_width = target_resolution[0] // grid_size[0]
        self.tile_height = target_resolution[1] // grid_size[1]
        
        # Adjust target resolution to fit grid perfectly
        self.adjusted_width = self.tile_width * grid_size[0]
        self.adjusted_height = self.tile_height * grid_size[1]
        
        print(f"Target resolution: {self.adjusted_width}x{self.adjusted_height}")
        print(f"Grid size: {grid_size[0]}x{grid_size[1]}")
        print(f"Tile size: {self.tile_width}x{self.tile_height}")
    
    def load_and_preprocess_image(self, image_path: str, 
                                 apply_quantization: bool = False,
                                 n_colors: int = 16) -> Optional[np.ndarray]:
        """
        Load and preprocess image from file path.
        
        Args:
            image_path: Path to the image file
            apply_quantization: Whether to apply color quantization
            n_colors: Number of colors for quantization
            
        Returns:
            Preprocessed image as numpy array (RGB) or None if failed
        """
        try:
            # Load and convert image
            image = cv2.imread(image_path)
            if image is None:
                raise ValueError(f"Could not load image: {image_path}")
            
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            
            # Resize and crop to fit grid
            processed_image = self._resize_and_crop(image)
            
            # Apply color quantization if requested
            if apply_quantization:
                processed_image = self._apply_color_quantization(processed_image, n_colors)
            
            return processed_image
            
        except Exception as e:
            print(f"Error processing {image_path}: {str(e)}")
            return None
    
    def preprocess_numpy_image(self, image: np.ndarray,
                              apply_quantization: bool = False,
                              n_colors: int = 16) -> Optional[np.ndarray]:
        """
        Preprocess numpy image array (for Gradio integration).
        
        Args:
            image: Input image as numpy array
            apply_quantization: Whether to apply color quantization
            n_colors: Number of colors for quantization
            
        Returns:
            Preprocessed image as numpy array (RGB) or None if failed
        """
        try:
            if len(image.shape) != 3 or image.shape[2] != 3:
                raise ValueError("Image must be RGB format with shape (H, W, 3)")
            
            processed_image = image.copy()
            processed_image = self._resize_and_crop(processed_image)
            
            if apply_quantization:
                processed_image = self._apply_color_quantization(processed_image, n_colors)
            
            return processed_image
            
        except Exception as e:
            print(f"Error processing numpy image: {str(e)}")
            return None
    
    def _resize_and_crop(self, image: np.ndarray) -> np.ndarray:
        """
        Resize and crop image to fit target resolution while maintaining aspect ratio.
        """
        h, w = image.shape[:2]
        target_w, target_h = self.adjusted_width, self.adjusted_height
        
        # Scale to fill target size
        scale_w = target_w / w
        scale_h = target_h / h
        scale = max(scale_w, scale_h)
        
        # Resize image
        new_w = int(w * scale)
        new_h = int(h * scale)
        resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
        
        # Center crop to exact target size
        start_x = (new_w - target_w) // 2
        start_y = (new_h - target_h) // 2
        cropped = resized[start_y:start_y + target_h, start_x:start_x + target_w]
        
        return cropped
    
    def _apply_color_quantization(self, image: np.ndarray, n_colors: int) -> np.ndarray:
        """
        Apply color quantization using Mini-Batch K-means for speed.
        3-4x faster than regular K-means with similar quality.
        """
        h, w, c = image.shape
        pixels = image.reshape(-1, c)
        
        # Adaptive batch size based on image size
        total_pixels = len(pixels)
        batch_size = min(max(total_pixels // 100, 1000), 10000)
        
        print(f"Applying Mini-Batch K-means quantization:")
        print(f"  Total pixels: {total_pixels:,}")
        print(f"  Batch size: {batch_size:,}")
        print(f"  Target colors: {n_colors}")
        
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=UserWarning)
            warnings.filterwarnings("ignore", message=".*ConvergenceWarning.*")
            
            kmeans = MiniBatchKMeans(
                n_clusters=n_colors, 
                batch_size=batch_size,
                random_state=42, 
                n_init=3,
                max_iter=100
            )
            labels = kmeans.fit_predict(pixels)
        
        # Replace pixels with cluster centers
        quantized_pixels = kmeans.cluster_centers_[labels]
        quantized_image = quantized_pixels.reshape(h, w, c).astype(np.uint8)
        
        return quantized_image
    
    def save_preprocessed_image(self, image: np.ndarray, output_path: str):
        """Save preprocessed image to disk."""
        try:
            pil_image = Image.fromarray(image)
            pil_image.save(output_path, quality=95)
            print(f"Saved preprocessed image to: {output_path}")
        except Exception as e:
            print(f"Error saving image: {str(e)}")

if __name__ == "__main__":
    # Test preprocessing
    preprocessor = ImagePreprocessor(
        target_resolution=(1280, 1280),
        grid_size=(32, 32)
    )
    
    test_image = "EmmaPotrait.jpg"
    
    if os.path.exists(test_image):
        processed = preprocessor.load_and_preprocess_image(
            test_image, 
            apply_quantization=True,
            n_colors=8
        )
        
        if processed is not None:
            preprocessor.save_preprocessed_image(processed, "processed_quantized.jpg")
            print("Image preprocessing completed!")
    else:
        print(f"Test image not found: {test_image}")