File size: 15,101 Bytes
3b3c48f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Utility functions for the Interior Style Transfer Pipeline
"""
import cv2
import numpy as np
from PIL import Image
import os
from typing import Tuple, List, Optional, Union
import json
from pathlib import Path

def load_image_safe(image_path: str, target_size: Tuple[int, int] = None) -> np.ndarray:
    """
    Safely load an image with error handling
    
    Args:
        image_path: Path to the image file
        target_size: Optional target size (width, height)
        
    Returns:
        Loaded image as numpy array
        
    Raises:
        ValueError: If image cannot be loaded
    """
    if not os.path.exists(image_path):
        raise ValueError(f"Image file not found: {image_path}")
    
    # Try to load with OpenCV first
    image = cv2.imread(image_path)
    if image is None:
        # Fallback to PIL
        try:
            pil_image = Image.open(image_path)
            image = np.array(pil_image)
            if len(image.shape) == 3 and image.shape[2] == 3:
                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
            elif len(image.shape) == 3 and image.shape[2] == 4:
                image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR)
        except Exception as e:
            raise ValueError(f"Could not load image {image_path}: {e}")
    
    if target_size:
        image = cv2.resize(image, target_size)
    
    return image

def save_image_safe(image: np.ndarray, output_path: str, 
                   quality: int = 95) -> bool:
    """
    Safely save an image with error handling
    
    Args:
        image: Image to save as numpy array
        output_path: Output file path
        quality: JPEG quality (1-100)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Ensure output directory exists
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        # Save with OpenCV
        success = cv2.imwrite(output_path, image)
        
        if not success:
            # Fallback to PIL
            if len(image.shape) == 3 and image.shape[2] == 3:
                pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
            else:
                pil_image = Image.fromarray(image)
            
            pil_image.save(output_path, quality=quality)
            success = True
        
        return success
    except Exception as e:
        print(f"Error saving image to {output_path}: {e}")
        return False

def validate_image_pair(user_room: np.ndarray, inspiration_room: np.ndarray) -> Tuple[bool, str]:
    """
    Validate that two images are suitable for style transfer
    
    Args:
        user_room: User room image
        inspiration_room: Inspiration room image
        
    Returns:
        Tuple of (is_valid, error_message)
    """
    # Check image dimensions
    if user_room.shape != inspiration_room.shape:
        return False, f"Image dimensions don't match: {user_room.shape} vs {inspiration_room.shape}"
    
    # Check minimum size
    min_size = 256
    if user_room.shape[0] < min_size or user_room.shape[1] < min_size:
        return False, f"Images too small. Minimum size: {min_size}x{min_size}"
    
    # Check aspect ratio (should be roughly square for best results)
    aspect_ratio = user_room.shape[1] / user_room.shape[0]
    if aspect_ratio < 0.5 or aspect_ratio > 2.0:
        return False, f"Extreme aspect ratio: {aspect_ratio:.2f}. Square images work best."
    
    # Check if images are too dark or too bright
    user_brightness = np.mean(cv2.cvtColor(user_room, cv2.COLOR_BGR2GRAY))
    inspiration_brightness = np.mean(cv2.cvtColor(inspiration_room, cv2.COLOR_BGR2GRAY))
    
    if user_brightness < 30 or user_brightness > 225:
        return False, f"User room too {'dark' if user_brightness < 30 else 'bright'}: {user_brightness:.1f}"
    
    if inspiration_brightness < 30 or inspiration_brightness > 225:
        return False, f"Inspiration room too {'dark' if inspiration_brightness < 30 else 'bright'}: {inspiration_brightness:.1f}"
    
    return True, "Images are valid for style transfer"

def create_comparison_image(original: np.ndarray, result: np.ndarray, 
                           title: str = "Style Transfer Comparison") -> np.ndarray:
    """
    Create a side-by-side comparison image
    
    Args:
        original: Original user room image
        result: Style transfer result
        title: Title for the comparison
        
    Returns:
        Comparison image
    """
    # Ensure both images have the same dimensions
    if original.shape != result.shape:
        result = cv2.resize(result, (original.shape[1], original.shape[0]))
    
    # Create comparison image
    comparison = np.hstack([original, result])
    
    # Add title
    font = cv2.FONT_HERSHEY_SIMPLEX
    font_scale = 1.0
    thickness = 2
    
    # Calculate text position
    text_size = cv2.getTextSize(title, font, font_scale, thickness)[0]
    text_x = (comparison.shape[1] - text_size[0]) // 2
    text_y = 50
    
    # Add background for text
    cv2.rectangle(comparison, (text_x - 10, text_y - 30), 
                  (text_x + text_size[0] + 10, text_y + 10), (255, 255, 255), -1)
    
    # Add text
    cv2.putText(comparison, title, (text_x, text_y), font, font_scale, (0, 0, 0), thickness)
    
    # Add labels
    cv2.putText(comparison, "Original", (50, comparison.shape[0] - 30), 
                font, 0.7, (255, 255, 255), 2)
    cv2.putText(comparison, "Result", (original.shape[1] + 50, comparison.shape[0] - 30), 
                font, 0.7, (255, 255, 255), 2)
    
    return comparison

def create_multi_comparison_image(images: List[np.ndarray], 
                                 titles: List[str] = None,
                                 title: str = "Multi-Image Comparison") -> np.ndarray:
    """
    Create a comparison image with multiple images side by side
    
    Args:
        images: List of images to compare
        titles: List of titles for each image (optional)
        title: Main title for the comparison
        
    Returns:
        Comparison image
    """
    if not images:
        raise ValueError("At least one image is required")
    
    # Ensure all images have the same dimensions
    target_shape = images[0].shape
    resized_images = []
    for img in images:
        if img.shape != target_shape:
            resized_img = cv2.resize(img, (target_shape[1], target_shape[0]))
            resized_images.append(resized_img)
        else:
            resized_images.append(img)
    
    # Create horizontal stack of images
    comparison = np.hstack(resized_images)
    
    # Add main title
    font = cv2.FONT_HERSHEY_SIMPLEX
    font_scale = 1.0
    thickness = 2
    
    # Calculate text position for main title
    text_size = cv2.getTextSize(title, font, font_scale, thickness)[0]
    text_x = (comparison.shape[1] - text_size[0]) // 2
    text_y = 50
    
    # Add background for main title
    cv2.rectangle(comparison, (text_x - 10, text_y - 30), 
                  (text_x + text_size[0] + 10, text_y + 10), (255, 255, 255), -1)
    
    # Add main title
    cv2.putText(comparison, title, (text_x, text_y), font, font_scale, (0, 0, 0), thickness)
    
    # Add individual image titles if provided
    if titles and len(titles) == len(images):
        font_scale_small = 0.7
        thickness_small = 1
        
        for i, (img, img_title) in enumerate(zip(resized_images, titles)):
            # Calculate position for each image title
            img_width = img.shape[1]
            start_x = sum(img.shape[1] for img in resized_images[:i])
            
            # Add background for image title
            title_size = cv2.getTextSize(img_title, font, font_scale_small, thickness_small)[0]
            title_x = start_x + (img_width - title_size[0]) // 2
            title_y = comparison.shape[0] - 30
            
            # Add background rectangle
            cv2.rectangle(comparison, (title_x - 5, title_y - 20), 
                          (title_x + title_size[0] + 5, title_y + 5), (255, 255, 255), -1)
            
            # Add image title
            cv2.putText(comparison, img_title, (title_x, title_y), 
                        font, font_scale_small, (0, 0, 0), thickness_small)
    
    return comparison

def enhance_image_quality(image: np.ndarray, 
                         sharpness: float = 0.3,
                         contrast: float = 1.1,
                         saturation: float = 1.1) -> np.ndarray:
    """
    Enhance image quality with various filters
    
    Args:
        image: Input image
        sharpness: Sharpening strength (0.0 to 1.0)
        contrast: Contrast multiplier
        saturation: Saturation multiplier
        
    Returns:
        Enhanced image
    """
    enhanced = image.copy()
    
    # Sharpening
    if sharpness > 0:
        kernel = np.array([[-1, -1, -1],
                          [-1,  9, -1],
                          [-1, -1, -1]]) * sharpness
        enhanced = cv2.filter2D(enhanced, -1, kernel)
    
    # Contrast adjustment
    if contrast != 1.0:
        enhanced = np.clip(enhanced * contrast, 0, 255).astype(np.uint8)
    
    # Saturation adjustment
    if saturation != 1.0:
        hsv = cv2.cvtColor(enhanced, cv2.COLOR_BGR2HSV).astype(np.float32)
        hsv[:, :, 1] = np.clip(hsv[:, :, 1] * saturation, 0, 255)
        enhanced = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
    
    return enhanced

def create_progress_bar(total: int, description: str = "Processing") -> callable:
    """
    Create a simple progress bar function
    
    Args:
        total: Total number of steps
        description: Description of the process
        
    Returns:
        Function to update progress
    """
    def update_progress(current: int):
        percentage = (current / total) * 100
        bar_length = 30
        filled_length = int(bar_length * current // total)
        bar = '█' * filled_length + '-' * (bar_length - filled_length)
        print(f'\r{description}: |{bar}| {percentage:.1f}% ({current}/{total})', end='')
        if current == total:
            print()
    
    return update_progress

def save_metadata(metadata: dict, output_path: str) -> bool:
    """
    Save metadata to JSON file
    
    Args:
        metadata: Dictionary of metadata
        output_path: Output file path
        
    Returns:
        True if successful, False otherwise
    """
    try:
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        with open(output_path, 'w') as f:
            json.dump(metadata, f, indent=2, default=str)
        
        return True
    except Exception as e:
        print(f"Error saving metadata to {output_path}: {e}")
        return False

def load_metadata(metadata_path: str) -> Optional[dict]:
    """
    Load metadata from JSON file
    
    Args:
        metadata_path: Path to metadata file
        
    Returns:
        Loaded metadata dictionary or None if failed
    """
    try:
        with open(metadata_path, 'r') as f:
            return json.load(f)
    except Exception as e:
        print(f"Error loading metadata from {metadata_path}: {e}")
        return None

def calculate_image_similarity(img1: np.ndarray, img2: np.ndarray) -> float:
    """
    Calculate similarity between two images using structural similarity
    
    Args:
        img1: First image
        img2: Second image
        
    Returns:
        Similarity score (0.0 to 1.0, higher is more similar)
    """
    try:
        from skimage.metrics import structural_similarity as ssim
        
        # Ensure same dimensions
        if img1.shape != img2.shape:
            img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
        
        # Convert to grayscale for SSIM
        gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
        gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
        
        # Calculate SSIM
        similarity = ssim(gray1, gray2)
        return max(0.0, similarity)  # Ensure non-negative
        
    except ImportError:
        # Fallback to simple MSE-based similarity
        if img1.shape != img2.shape:
            img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
        
        mse = np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2)
        max_mse = 255 ** 2
        similarity = 1.0 - (mse / max_mse)
        return max(0.0, similarity)

def create_thumbnail(image: np.ndarray, max_size: int = 200) -> np.ndarray:
    """
    Create a thumbnail version of an image
    
    Args:
        image: Input image
        max_size: Maximum dimension size
        
    Returns:
        Thumbnail image
    """
    height, width = image.shape[:2]
    
    if height <= max_size and width <= max_size:
        return image.copy()
    
    # Calculate new dimensions maintaining aspect ratio
    if height > width:
        new_height = max_size
        new_width = int(width * max_size / height)
    else:
        new_width = max_size
        new_height = int(height * max_size / width)
    
    thumbnail = cv2.resize(image, (new_width, new_height))
    return thumbnail

def batch_resize_images(images: List[np.ndarray], 
                       target_size: Tuple[int, int]) -> List[np.ndarray]:
    """
    Resize a list of images to the same target size
    
    Args:
        images: List of input images
        target_size: Target size (width, height)
        
    Returns:
        List of resized images
    """
    resized_images = []
    
    for image in images:
        resized = cv2.resize(image, target_size)
        resized_images.append(resized)
    
    return resized_images

def create_image_grid(images: List[np.ndarray], 
                     grid_size: Tuple[int, int] = None) -> np.ndarray:
    """
    Create a grid layout of images
    
    Args:
        images: List of images to arrange in grid
        grid_size: Grid dimensions (rows, cols). If None, auto-calculate
        
    Returns:
        Grid image
    """
    if not images:
        return np.array([])
    
    if grid_size is None:
        # Auto-calculate grid size
        n_images = len(images)
        cols = int(np.ceil(np.sqrt(n_images)))
        rows = int(np.ceil(n_images / cols))
        grid_size = (rows, cols)
    
    rows, cols = grid_size
    
    # Ensure all images have the same size
    target_size = (images[0].shape[1], images[0].shape[0])
    resized_images = batch_resize_images(images, target_size)
    
    # Create grid
    grid_rows = []
    for i in range(rows):
        row_images = []
        for j in range(cols):
            idx = i * cols + j
            if idx < len(resized_images):
                row_images.append(resized_images[idx])
            else:
                # Fill empty space with black
                empty_image = np.zeros((target_size[1], target_size[0], 3), dtype=np.uint8)
                row_images.append(empty_image)
        
        row = np.hstack(row_images)
        grid_rows.append(row)
    
    grid = np.vstack(grid_rows)
    return grid