File size: 21,645 Bytes
9ecc0ac
 
0adb7f6
9ecc0ac
 
 
 
 
 
 
 
 
 
0adb7f6
 
9ecc0ac
 
 
0adb7f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
 
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
 
 
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
9ecc0ac
0adb7f6
 
 
9ecc0ac
 
0adb7f6
 
 
 
 
 
9ecc0ac
0adb7f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
0adb7f6
 
 
9ecc0ac
0adb7f6
9ecc0ac
0adb7f6
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
0adb7f6
9ecc0ac
0adb7f6
 
 
9ecc0ac
0adb7f6
 
 
 
 
 
 
 
 
9ecc0ac
 
 
0adb7f6
9ecc0ac
 
0adb7f6
 
 
9ecc0ac
0adb7f6
 
 
cd2a10d
 
0adb7f6
9ecc0ac
cd2a10d
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
 
9ecc0ac
 
cd2a10d
 
 
9ecc0ac
 
 
cd2a10d
 
 
0adb7f6
 
 
cd2a10d
 
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
 
 
 
 
 
 
 
cd2a10d
 
9ecc0ac
cd2a10d
0adb7f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ecc0ac
0adb7f6
9ecc0ac
 
 
0adb7f6
9ecc0ac
 
0adb7f6
 
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
c2d58b1
9ecc0ac
 
 
 
 
 
cd2a10d
9ecc0ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0adb7f6
9ecc0ac
 
 
0adb7f6
9ecc0ac
0adb7f6
 
 
 
 
 
 
 
 
 
 
9ecc0ac
 
 
 
 
 
0adb7f6
 
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
import numpy as np
import cv2
from PIL import Image
import gradio as gr
import time
import os
import glob
from skimage.metrics import structural_similarity as ssim

class FaceImageMosaicGenerator:
    def __init__(self, faces_directory="Real_Images/"):
        self.faces_directory = faces_directory
        self.tile_size = 32
        self.face_tiles = {}
        self.load_status = self.load_face_tiles()
        
    def load_face_tiles(self):
        """Load face images from the Real_Images directory"""
        try:
            if not os.path.exists(self.faces_directory):
                return f"❌ Faces directory '{self.faces_directory}' not found!"
            
            # Load all image files from faces directory
            face_extensions = ['*.png', '*.jpg', '*.jpeg', '*.bmp', '*.tiff']
            face_files = []
            
            for extension in face_extensions:
                face_files.extend(glob.glob(os.path.join(self.faces_directory, extension)))
            
            if len(face_files) == 0:
                return f"❌ No face images found in {self.faces_directory}"
            
            print(f"Loading {len(face_files)} face images from {self.faces_directory}")
            
            for i, face_path in enumerate(face_files):
                try:
                    # Load face image
                    face_image = Image.open(face_path)
                    
                    # Convert to RGB if needed
                    if face_image.mode != 'RGB':
                        face_image = face_image.convert('RGB')
                    
                    # Resize to standard tile size while maintaining aspect ratio
                    face_image = self.resize_face_tile(face_image, self.tile_size)
                    
                    # Convert to numpy array
                    face_array = np.array(face_image)
                    
                    # Calculate average color for this face
                    avg_color = np.mean(face_array, axis=(0, 1))
                    
                    # Calculate brightness for sorting
                    brightness = np.mean(avg_color)
                    
                    # Extract filename for identification
                    face_name = os.path.splitext(os.path.basename(face_path))[0]
                    
                    # Store face tile with metadata
                    self.face_tiles[face_name] = {
                        'image': face_array,
                        'avg_color': avg_color,
                        'brightness': brightness,
                        'path': face_path,
                        'dominant_color': self.get_dominant_color(face_array)
                    }
                    
                    if i % 100 == 0 and i > 0:  # Progress indicator
                        print(f"Processed {i}/{len(face_files)} face images...")
                        
                except Exception as e:
                    print(f"Error loading face {face_path}: {e}")
                    continue
            
            if not self.face_tiles:
                return f"❌ No valid face images loaded from {self.faces_directory}"
            
            print(f"βœ… Successfully loaded {len(self.face_tiles)} face tiles")
            return f"βœ… Loaded {len(self.face_tiles)} face images"
            
        except Exception as e:
            return f"❌ Error loading faces: {str(e)}"
    
    def resize_face_tile(self, face_image, target_size):
        """Resize face image to tile size while maintaining aspect ratio"""
        # Calculate aspect ratio
        width, height = face_image.size
        aspect_ratio = width / height
        
        if aspect_ratio > 1:  # Wide image
            new_width = target_size
            new_height = int(target_size / aspect_ratio)
        else:  # Tall or square image
            new_height = target_size
            new_width = int(target_size * aspect_ratio)
        
        # Resize image
        resized = face_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
        
        # Create square canvas and paste resized image in center
        square_image = Image.new('RGB', (target_size, target_size), (128, 128, 128))
        paste_x = (target_size - new_width) // 2
        paste_y = (target_size - new_height) // 2
        square_image.paste(resized, (paste_x, paste_y))
        
        return square_image
    
    def get_dominant_color(self, image_array):
        """Get the dominant color of an image using uniform quantization"""
        # Apply uniform quantization to reduce color space
        quantized = self.quantize_colors(image_array, n_levels=8)
        
        # Flatten to get list of colors
        colors = quantized.reshape(-1, 3)
        
        # Find most frequent color
        unique_colors, counts = np.unique(colors, axis=0, return_counts=True)
        most_frequent_idx = np.argmax(counts)
        
        return unique_colors[most_frequent_idx].astype(float)
    
    def find_best_matching_face(self, target_color, matching_method='color_distance'):
        """Find the face tile that best matches the target color"""
        if not self.face_tiles:
            return None, "no_faces"
            
        best_face_name = None
        best_score = float('inf') if matching_method == 'color_distance' else float('-inf')
        
        for face_name, face_data in self.face_tiles.items():
            if matching_method == 'color_distance':
                # Euclidean distance in RGB space
                face_avg_color = face_data['avg_color']
                distance = np.sqrt(np.sum((target_color - face_avg_color) ** 2))
                score = distance
                is_better = score < best_score
                
            elif matching_method == 'brightness':
                # Match based on brightness similarity
                target_brightness = np.mean(target_color)
                face_brightness = face_data['brightness']
                distance = abs(target_brightness - face_brightness)
                score = distance
                is_better = score < best_score
                
            elif matching_method == 'dominant_color':
                # Match based on dominant color
                face_dominant = face_data['dominant_color']
                distance = np.sqrt(np.sum((target_color - face_dominant) ** 2))
                score = distance
                is_better = score < best_score
            
            if is_better:
                best_score = score
                best_face_name = face_name
        
        if best_face_name:
            return self.face_tiles[best_face_name]['image'], best_face_name
        else:
            return None, "no_match"
    
    def quantize_colors(self, image, n_levels=4):
        """Apply uniform color quantization to reduce color variations"""
        # Calculate quantization step size
        step_size = 256 // n_levels
        
        # Apply uniform quantization to each channel
        quantized_image = (image // step_size) * step_size
        
        # Add half step to center the quantized values
        quantized_image = quantized_image + step_size // 2
        
        # Ensure values stay within [0, 255] range
        quantized_image = np.clip(quantized_image, 0, 255).astype(np.uint8)
        
        return quantized_image
    
    def preprocess_image(self, image, target_size=(512, 512), quantize_colors=False, quantization_levels=4):
        """Preprocess the input image"""
        # Convert PIL to numpy array
        if isinstance(image, Image.Image):
            image = np.array(image)
        
        # Resize image
        image = cv2.resize(image, target_size)
        
        # Optional uniform color quantization
        if quantize_colors:
            image = self.quantize_colors(image, n_levels=quantization_levels)
        
        return image
    
    def create_grid_vectorized(self, image, grid_size):
        """Vectorized grid division and analysis"""
        h, w = image.shape[:2]
        cell_h, cell_w = h // grid_size, w // grid_size
        
        # Crop image to fit exact grid
        cropped_image = image[:cell_h * grid_size, :cell_w * grid_size]
        
        # Reshape image into grid cells using vectorized operations
        grid_cells = cropped_image.reshape(
            grid_size, cell_h, grid_size, cell_w, 3
        ).transpose(0, 2, 1, 3, 4)
        
        # Calculate average color for each cell
        avg_colors = np.mean(grid_cells, axis=(2, 3))
        
        return grid_cells, avg_colors, (cell_h, cell_w)
    
    def create_face_mosaic(self, image, grid_size=32, quantize=False, matching_method='color_distance', quantization_levels=4):
        """Create mosaic from image using FACE IMAGE TILES"""
        if not self.face_tiles:
            return None, None, 0, {"error": "No face tiles loaded"}
            
        start_time = time.time()
        
        try:
            # Preprocess image
            processed_image = self.preprocess_image(image, quantize_colors=quantize, 
                                                   quantization_levels=quantization_levels)
            
            # Create grid
            grid_cells, avg_colors, (cell_h, cell_w) = self.create_grid_vectorized(processed_image, grid_size)
            
            # Create mosaic using FACE IMAGE TILES
            mosaic_height = grid_size * self.tile_size
            mosaic_width = grid_size * self.tile_size
            mosaic = np.zeros((mosaic_height, mosaic_width, 3), dtype=np.uint8)
            
            # Classification visualization
            classified_image = np.zeros_like(processed_image[:grid_size*cell_h, :grid_size*cell_w])
            
            # Keep track of which faces were used
            used_faces = {}
            
            print(f"Creating {grid_size}x{grid_size} mosaic using face tiles...")
            
            for i in range(grid_size):
                for j in range(grid_size):
                    # Get target color for this cell
                    target_color = avg_colors[i, j]
                    
                    # Find the best matching FACE IMAGE TILE
                    best_face_tile, face_name = self.find_best_matching_face(
                        target_color, matching_method
                    )
                    
                    if best_face_tile is not None:
                        # Track face usage
                        if face_name in used_faces:
                            used_faces[face_name] += 1
                        else:
                            used_faces[face_name] = 1
                        
                        # Place the FACE IMAGE TILE in mosaic
                        y_start, y_end = i * self.tile_size, (i + 1) * self.tile_size
                        x_start, x_end = j * self.tile_size, (j + 1) * self.tile_size
                        mosaic[y_start:y_end, x_start:x_end] = best_face_tile
                    
                    # Fill classified image (for debugging)
                    cy_start, cy_end = i * cell_h, (i + 1) * cell_h
                    cx_start, cx_end = j * cell_w, (j + 1) * cell_w
                    classified_image[cy_start:cy_end, cx_start:cx_end] = target_color
                
                # Progress indicator
                if (i + 1) % 8 == 0 or i == grid_size - 1:
                    print(f"Completed {i + 1}/{grid_size} rows")
            
            processing_time = time.time() - start_time
            
            # Create face usage analysis
            face_analysis = {
                'used_faces': used_faces,
                'unique_faces': len(used_faces),
                'total_positions': grid_size * grid_size,
            }
            
            return mosaic, classified_image, processing_time, face_analysis
            
        except Exception as e:
            print(f"Error creating mosaic: {e}")
            return None, None, 0, {"error": str(e)}

def process_face_image(image, grid_size, use_quantization, matching_method, quantization_levels):
    """Main processing function for Gradio interface"""
    if image is None:
        return None, None, "❌ Please upload an image first."
    
    try:
        # Initialize generator (this will load faces once)
        if not hasattr(process_face_image, 'generator'):
            process_face_image.generator = FaceImageMosaicGenerator()
        
        generator = process_face_image.generator
        
        # Check if faces loaded successfully
        if not generator.face_tiles:
            return None, None, f"❌ {generator.load_status}\n\nPlease ensure:\n- Real_Images/ folder exists\n- It contains valid image files\n- Images are readable"
        
        # Create face mosaic
        mosaic, classified, processing_time, face_analysis = generator.create_face_mosaic(
            image, grid_size, use_quantization, matching_method, quantization_levels
        )
        
        if mosaic is None:
            error_msg = face_analysis.get('error', 'Unknown error occurred')
            return None, None, f"❌ Error creating mosaic: {error_msg}"
        
        # Calculate similarity metrics
        try:
            mse, ssim_score = calculate_similarity_metrics(np.array(image), mosaic)
        except Exception as e:
            print(f"Error calculating similarity metrics: {e}")
            mse, ssim_score = 0, 0
        
        # Create results text with safe division
        unique_faces = face_analysis.get('unique_faces', 0)
        total_positions = face_analysis.get('total_positions', 0)
        used_faces = face_analysis.get('used_faces', {})
        
        # Safe division for average reuse calculation
        if unique_faces > 0:
            average_reuse = total_positions / unique_faces
            utilization_percentage = 100 * unique_faces / len(generator.face_tiles)
        else:
            average_reuse = 0
            utilization_percentage = 0
        
        results_text = f"""
🎭 FACE MOSAIC RESULTS

Processing Time: {processing_time:.3f} seconds
Grid Size: {grid_size}Γ—{grid_size} = {grid_size*grid_size} tiles
Color Quantization: {'Enabled' if use_quantization else 'Disabled'}
Quantization Levels: {quantization_levels if use_quantization else 'N/A'}
Matching Method: {matching_method.replace('_', ' ').title()}

πŸ“Š SIMILARITY METRICS
MSE: {mse:.2f}
SSIM: {ssim_score:.4f}

πŸ‘₯ FACE DATASET INFO
Total Face Images: {len(generator.face_tiles)}
Directory: {generator.faces_directory}

🎨 MOSAIC STATISTICS
Unique Faces Used: {unique_faces} / {len(generator.face_tiles)} ({utilization_percentage:.1f}%)
Total Tile Positions: {total_positions}
Average Reuse: {average_reuse:.1f} times per face

Most Used Faces:"""
        
        # Add most frequently used faces with safe checking
        if used_faces:
            sorted_faces = sorted(used_faces.items(), 
                                key=lambda x: x[1], reverse=True)
            
            for face_name, count in sorted_faces[:5]:  # Top 5 most used faces
                if total_positions > 0:
                    percentage = 100 * count / total_positions
                    results_text += f"\n- {face_name}: {count} times ({percentage:.1f}%)"
                else:
                    results_text += f"\n- {face_name}: {count} times"
        else:
            results_text += "\n- No faces were successfully matched"
        
        # Additional debug info for matching issues
        if unique_faces == 0:
            results_text += f"\n\n⚠️ DEBUG INFO:"
            results_text += f"\nMatching method: {matching_method}"
            results_text += f"\nTotal face tiles loaded: {len(generator.face_tiles)}"
            results_text += f"\nSample face data available: {'Yes' if generator.face_tiles else 'No'}"
        
        return (
            Image.fromarray(mosaic),
            Image.fromarray(classified),
            results_text
        )
        
    except Exception as e:
        import traceback
        error_msg = f"❌ ERROR: {str(e)}\n\nDebug info:\n{traceback.format_exc()}\n\nPlease check:\n- Real_Images/ directory exists\n- Directory contains valid image files (.jpg, .png, etc.)\n- Images are readable"
        return None, None, error_msg
    
def calculate_similarity_metrics(original, mosaic):
    """Calculate similarity metrics between original and mosaic"""
    try:
        # Resize original to match mosaic size for fair comparison
        original_resized = cv2.resize(original, (mosaic.shape[1], mosaic.shape[0]))
        
        # Convert to grayscale for SSIM
        orig_gray = cv2.cvtColor(original_resized, cv2.COLOR_RGB2GRAY)
        mosaic_gray = cv2.cvtColor(mosaic, cv2.COLOR_RGB2GRAY)
        
        # Calculate MSE
        mse = np.mean((original_resized.astype(float) - mosaic.astype(float)) ** 2)
        
        # Calculate SSIM
        ssim_score = ssim(orig_gray, mosaic_gray)
        
        return mse, ssim_score
    except:
        return 0, 0

# Create Gradio interface
def create_interface():
    with gr.Blocks(title="Face Image Mosaic Generator", theme=gr.themes.Soft()) as interface:
        gr.Markdown("# 🎭 Face Image Mosaic Generator")
        gr.Markdown("""
        Upload an image and convert it into an artistic mosaic using **real human face images**!
        
        Each grid cell in your input image will be replaced with the face image that best matches its color characteristics.
        
        ⚠️ **Important**: Make sure you have uploaded face images to the `Real_Images/` folder in this Space.
        """)
        
        with gr.Row():
            with gr.Column(scale=1):
                image_input = gr.Image(
                    type="pil", 
                    label="πŸ“· Upload Image to Convert",
                    height=300
                )
                
                with gr.Accordion("βš™οΈ Mosaic Settings", open=True):
                    grid_size = gr.Slider(
                        minimum=8, maximum=64, value=16, step=8,
                        label="Grid Size (tiles per row/column)",
                        info="Higher = more detail, slower processing"
                    )
                    
                    matching_method = gr.Dropdown(
                        choices=[
                            ("Color Distance", "color_distance")
                        ],
                        value="color_distance",
                        label="Face Matching Method",
                        info="How to choose the best face for each tile"
                    )
                    
                    use_quantization = gr.Checkbox(
                        label="Uniform Color Quantization", 
                        value=False,
                        info="Reduce colors for cleaner matching"
                    )
                    
                    quantization_levels = gr.Slider(
                        minimum=2, maximum=16, value=4, step=1,
                        label="Quantization Levels",
                        info="Number of levels per RGB channel (only if quantization enabled)",
                        visible=False
                    )
                
                # Show/hide quantization levels based on checkbox
                use_quantization.change(
                    fn=lambda x: gr.update(visible=x),
                    inputs=[use_quantization],
                    outputs=[quantization_levels]
                )
                
                process_btn = gr.Button(
                    "🎨 Generate Face Mosaic", 
                    variant="primary", 
                    size="lg"
                )
            
            with gr.Column(scale=2):
                with gr.Tab("🎭 Mosaic Result"):
                    mosaic_output = gr.Image(
                        label="Face Mosaic Result",
                        height=400
                    )
                
                with gr.Tab("🎯 Color Analysis"):
                    classified_output = gr.Image(
                        label="Color Classification Grid",
                        height=400
                    )
                
                with gr.Tab("πŸ“Š Statistics"):
                    results_text = gr.Textbox(
                        label="Detailed Analysis",
                        lines=20,
                        max_lines=30
                    )
        
        # Process button click
        process_btn.click(
            fn=process_face_image,
            inputs=[image_input, grid_size, use_quantization, matching_method, quantization_levels],
            outputs=[mosaic_output, classified_output, results_text]
        )
        
        # Instructions
        gr.Markdown("""
        ### πŸ“‹ Instructions:
        1. **Upload face images** to the `Real_Images/` folder in this Space's Files tab
        2. **Upload an image** you want to convert into a mosaic
        3. **Adjust settings** (grid size, matching method, etc.)
        4. **Click "Generate Face Mosaic"**
        
        ### 🎯 Tips:
        - Start with smaller grid sizes (8Γ—8, 16Γ—16) for faster processing
        - Use "Color Distance" matching for most images
        - Enable quantization for noisy or complex images
        - More face images = better variety in the mosaic
        """)
    
    return interface

if __name__ == "__main__":
    # Create and launch interface
    interface = create_interface()
    interface.launch()