CoolFace
Apppublic

ganeshkumar383/AI-Based-Image-Deblurring-App

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
cnn_deblurring.py907 linesDownload Raw Back to modules
1"""
2CNN Deblurring Module - Deep Learning Based Image Enhancement
3============================================================
4
5CNN inference system for image deblurring with TensorFlow/Keras.
6Includes model architecture, training utilities, and inference pipeline.
7"""
8
9import cv2
10import numpy as np
11import os
12import logging
13from typing import Optional, Tuple, List
14import pickle
15
16# Configure TensorFlow to reduce verbosity
17os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  # Reduce TensorFlow logging
18os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'  # Disable oneDNN messages
19
20import tensorflow as tf
21from tensorflow import keras
22from tensorflow.keras import layers, Model
23
24# Configure TensorFlow settings
25tf.get_logger().setLevel('ERROR')  # Only show errors
26tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
27
28# Configure logging
29logging.basicConfig(level=logging.INFO)
30logger = logging.getLogger(__name__)
31
32class CNNDeblurModel:
33    """CNN-based deblurring model with encoder-decoder architecture"""
34    
35    def __init__(self, input_shape: Tuple[int, int, int] = (256, 256, 3)):
36        self.input_shape = input_shape
37        self.model = None
38        self.is_trained = False
39        self.training_history = None
40        self.model_path = "models/cnn_deblur_model.h5"
41        self.dataset_path = "data/training_dataset"
42        
43    def build_model(self) -> Model:
44        """
45        Build CNN deblurring model with U-Net like architecture
46        
47        Returns:
48            keras.Model: Compiled CNN model
49        """
50        try:
51            # Input layer
52            inputs = keras.Input(shape=self.input_shape)
53            
54            # Encoder (Downsampling)
55            conv1 = layers.Conv2D(64, 3, activation='relu', padding='same')(inputs)
56            conv1 = layers.Conv2D(64, 3, activation='relu', padding='same')(conv1)
57            pool1 = layers.MaxPooling2D(pool_size=(2, 2))(conv1)
58            
59            conv2 = layers.Conv2D(128, 3, activation='relu', padding='same')(pool1)
60            conv2 = layers.Conv2D(128, 3, activation='relu', padding='same')(conv2)
61            pool2 = layers.MaxPooling2D(pool_size=(2, 2))(conv2)
62            
63            conv3 = layers.Conv2D(256, 3, activation='relu', padding='same')(pool2)
64            conv3 = layers.Conv2D(256, 3, activation='relu', padding='same')(conv3)
65            pool3 = layers.MaxPooling2D(pool_size=(2, 2))(conv3)
66            
67            # Bottleneck
68            conv4 = layers.Conv2D(512, 3, activation='relu', padding='same')(pool3)
69            conv4 = layers.Conv2D(512, 3, activation='relu', padding='same')(conv4)
70            
71            # Decoder (Upsampling)
72            up5 = layers.UpSampling2D(size=(2, 2))(conv4)
73            up5 = layers.Conv2D(256, 2, activation='relu', padding='same')(up5)
74            merge5 = layers.concatenate([conv3, up5], axis=3)
75            conv5 = layers.Conv2D(256, 3, activation='relu', padding='same')(merge5)
76            conv5 = layers.Conv2D(256, 3, activation='relu', padding='same')(conv5)
77            
78            up6 = layers.UpSampling2D(size=(2, 2))(conv5)
79            up6 = layers.Conv2D(128, 2, activation='relu', padding='same')(up6)
80            merge6 = layers.concatenate([conv2, up6], axis=3)
81            conv6 = layers.Conv2D(128, 3, activation='relu', padding='same')(merge6)
82            conv6 = layers.Conv2D(128, 3, activation='relu', padding='same')(conv6)
83            
84            up7 = layers.UpSampling2D(size=(2, 2))(conv6)
85            up7 = layers.Conv2D(64, 2, activation='relu', padding='same')(up7)
86            merge7 = layers.concatenate([conv1, up7], axis=3)
87            conv7 = layers.Conv2D(64, 3, activation='relu', padding='same')(merge7)
88            conv7 = layers.Conv2D(64, 3, activation='relu', padding='same')(conv7)
89            
90            # Output layer
91            outputs = layers.Conv2D(3, 1, activation='sigmoid')(conv7)
92            
93            # Create model
94            model = Model(inputs=inputs, outputs=outputs)
95            
96            # Compile model
97            model.compile(
98                optimizer='adam',
99                loss='mse',
100                metrics=['mae', 'mse']
101            )
102            
103            self.model = model
104            logger.info("CNN model built successfully")
105            return model
106            
107        except Exception as e:
108            logger.error(f"Error building CNN model: {e}")
109            return None
110    
111    def load_model(self, model_path: str) -> bool:
112        """
113        Load pre-trained model from file
114        
115        Args:
116            model_path: Path to saved model
117        
118        Returns:
119            bool: Success status
120        """
121        try:
122            if os.path.exists(model_path):
123                self.model = keras.models.load_model(model_path)
124                self.is_trained = True
125                logger.info(f"Model loaded from {model_path}")
126                return True
127            else:
128                logger.warning(f"Model file not found: {model_path}")
129                # Build new model as fallback
130                self.build_model()
131                return False
132                
133        except Exception as e:
134            logger.error(f"Error loading model: {e}")
135            self.build_model()  # Fallback to new model
136            return False
137    
138    def save_model(self, model_path: str) -> bool:
139        """
140        Save current model to file
141        
142        Args:
143            model_path: Path to save model
144        
145        Returns:
146            bool: Success status
147        """
148        try:
149            if self.model is not None:
150                self.model.save(model_path)
151                logger.info(f"Model saved to {model_path}")
152                return True
153            else:
154                logger.error("No model to save")
155                return False
156                
157        except Exception as e:
158            logger.error(f"Error saving model: {e}")
159            return False
160    
161    def preprocess_image(self, image: np.ndarray) -> np.ndarray:
162        """
163        Preprocess image for CNN input with color preservation
164        
165        Args:
166            image: Input image (BGR format)
167        
168        Returns:
169            np.ndarray: Preprocessed image
170        """
171        try:
172            # Convert BGR to RGB (preserve original precision)
173            if len(image.shape) == 3 and image.shape[2] == 3:
174                rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
175            else:
176                rgb_image = image
177            
178            # Resize to model input size with high-quality interpolation
179            resized = cv2.resize(rgb_image, 
180                               (self.input_shape[1], self.input_shape[0]), 
181                               interpolation=cv2.INTER_CUBIC)  # Better color preservation
182            
183            # Normalize to [0, 1] with high precision
184            normalized = resized.astype(np.float64) / 255.0  # Use float64 for precision
185            
186            # Add batch dimension
187            batched = np.expand_dims(normalized, axis=0)
188            
189            return batched.astype(np.float32)  # Convert to float32 for model
190            
191        except Exception as e:
192            logger.error(f"Error preprocessing image: {e}")
193            return np.array([])
194    
195    def postprocess_image(self, output: np.ndarray, original_shape: Tuple[int, int]) -> np.ndarray:
196        """
197        Postprocess CNN output to original image format with color preservation
198        
199        Args:
200            output: CNN model output
201            original_shape: Original image shape (height, width)
202        
203        Returns:
204            np.ndarray: Postprocessed image in BGR format
205        """
206        try:
207            # Remove batch dimension
208            if len(output.shape) == 4:
209                output = output[0]
210            
211            # Denormalize from [0, 1] to [0, 255] with high precision
212            denormalized = np.clip(output * 255.0, 0, 255)  # Clip before conversion
213            denormalized = np.round(denormalized).astype(np.uint8)  # Round to preserve colors
214            
215            # Resize to original size with high-quality interpolation
216            resized = cv2.resize(denormalized, 
217                               (original_shape[1], original_shape[0]), 
218                               interpolation=cv2.INTER_CUBIC)  # Better color preservation
219            
220            # Convert RGB back to BGR
221            bgr_image = cv2.cvtColor(resized, cv2.COLOR_RGB2BGR)
222            
223            return bgr_image
224            
225        except Exception as e:
226            logger.error(f"Error postprocessing image: {e}")
227            return np.zeros((*original_shape, 3), dtype=np.uint8)
228    
229    def enhance_image(self, image: np.ndarray) -> np.ndarray:
230        """
231        Enhance image using CNN model
232        
233        Args:
234            image: Input blurry image (BGR format)
235        
236        Returns:
237            np.ndarray: Enhanced image (BGR format)
238        """
239        try:
240            if self.model is None:
241                logger.warning("No model available, building new model")
242                self.build_model()
243            
244            # Store original shape
245            original_shape = image.shape[:2]
246            
247            # Preprocess
248            preprocessed = self.preprocess_image(image)
249            
250            if preprocessed.size == 0:
251                logger.error("Failed to preprocess image")
252                return image
253            
254            # If model is not trained, return enhanced version using traditional methods
255            if not self.is_trained:
256                logger.info("Using fallback enhancement (model not trained)")
257                return self._fallback_enhancement(image)
258            
259            # CNN inference
260            enhanced = self.model.predict(preprocessed, verbose=0)
261            
262            # Postprocess
263            result = self.postprocess_image(enhanced, original_shape)
264            
265            logger.info("CNN enhancement completed")
266            return result
267            
268        except Exception as e:
269            logger.error(f"Error in CNN enhancement: {e}")
270            return self._fallback_enhancement(image)
271    
272    def _fallback_enhancement(self, image: np.ndarray) -> np.ndarray:
273        """
274        Fallback enhancement when CNN model is not available - preserves original colors
275        
276        Args:
277            image: Input image
278        
279        Returns:
280            np.ndarray: Enhanced image using color-preserving traditional methods
281        """
282        try:
283            # Method 1: Gentle unsharp masking with color preservation
284            # Create a subtle blur for unsharp masking
285            gaussian = cv2.GaussianBlur(image, (5, 5), 1.0)
286            
287            # Apply very gentle unsharp masking to avoid color shifts
288            enhanced = cv2.addWeighted(image, 1.2, gaussian, -0.2, 0)
289            
290            # Method 2: Enhance sharpness without changing colors
291            # Convert to float for precision
292            img_float = image.astype(np.float64)
293            
294            # Apply high-pass filter for sharpening
295            kernel_sharpen = np.array([[-0.1, -0.1, -0.1],
296                                     [-0.1,  1.8, -0.1], 
297                                     [-0.1, -0.1, -0.1]])
298            
299            # Apply sharpening kernel to each channel separately
300            sharpened_channels = []
301            for i in range(3):  # Process each color channel
302                channel = img_float[:, :, i]
303                sharpened_channel = cv2.filter2D(channel, -1, kernel_sharpen)
304                sharpened_channels.append(sharpened_channel)
305            
306            sharpened = np.stack(sharpened_channels, axis=2)
307            
308            # Combine original with sharpened (gentle blend)
309            result = 0.7 * img_float + 0.3 * sharpened
310            
311            # Carefully clip and convert back
312            result = np.clip(result, 0, 255).astype(np.uint8)
313            
314            logger.info("Color-preserving fallback enhancement applied")
315            return result
316            
317        except Exception as e:
318            logger.error(f"Error in fallback enhancement: {e}")
319            return image
320
321class CNNTrainer:
322    """Training utilities for CNN deblurring model"""
323    
324    def __init__(self, model: CNNDeblurModel):
325        self.model = model
326    
327    def create_synthetic_data(self, clean_images: List[np.ndarray], 
328                            blur_types: List[str] = None) -> Tuple[np.ndarray, np.ndarray]:
329        """
330        Create synthetic training data by applying blur to clean images
331        
332        Args:
333            clean_images: List of clean images
334            blur_types: Types of blur to apply
335        
336        Returns:
337            tuple: (blurred_images, clean_images) for training
338        """
339        if blur_types is None:
340            blur_types = ['gaussian', 'motion', 'defocus']
341        
342        blurred_batch = []
343        clean_batch = []
344        
345        try:
346            for clean_img in clean_images:
347                # Random blur type
348                blur_type = np.random.choice(blur_types)
349                
350                if blur_type == 'gaussian':
351                    # Gaussian blur
352                    kernel_size = np.random.randint(5, 15)
353                    if kernel_size % 2 == 0:
354                        kernel_size += 1
355                    blurred = cv2.GaussianBlur(clean_img, (kernel_size, kernel_size), 0)
356                
357                elif blur_type == 'motion':
358                    # Motion blur
359                    length = np.random.randint(5, 20)
360                    angle = np.random.randint(0, 180)
361                    kernel = self._create_motion_kernel(length, angle)
362                    blurred = cv2.filter2D(clean_img, -1, kernel)
363                
364                else:  # defocus
365                    # Defocus blur (approximated with Gaussian)
366                    sigma = np.random.uniform(1, 5)
367                    blurred = cv2.GaussianBlur(clean_img, (0, 0), sigma)
368                
369                blurred_batch.append(blurred)
370                clean_batch.append(clean_img)
371            
372            return np.array(blurred_batch), np.array(clean_batch)
373            
374        except Exception as e:
375            logger.error(f"Error creating synthetic data: {e}")
376            return np.array([]), np.array([])
377    
378    def _create_motion_kernel(self, length: int, angle: float) -> np.ndarray:
379        """Create motion blur kernel"""
380        kernel = np.zeros((length, length))
381        center = length // 2
382        
383        cos_val = np.cos(np.radians(angle))
384        sin_val = np.sin(np.radians(angle))
385        
386        for i in range(length):
387            offset = i - center
388            y = int(center + offset * sin_val)
389            x = int(center + offset * cos_val)
390            if 0 <= y < length and 0 <= x < length:
391                kernel[y, x] = 1
392        
393        return kernel / kernel.sum()
394    
395    def _load_user_images(self) -> List[np.ndarray]:
396        """Load user's training images from training_dataset folder"""
397        user_images = []
398        
399        try:
400            if not os.path.exists(self.dataset_path):
401                return user_images
402                
403            # Supported image extensions
404            valid_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
405            
406            for filename in os.listdir(self.dataset_path):
407                if any(filename.lower().endswith(ext) for ext in valid_extensions):
408                    image_path = os.path.join(self.dataset_path, filename)
409                    try:
410                        # Load image
411                        image = cv2.imread(image_path)
412                        if image is not None:
413                            # Resize to model input size
414                            resized = cv2.resize(image, (self.input_shape[1], self.input_shape[0]))
415                            user_images.append(resized)
416                            logger.info(f"Loaded user image: {filename}")
417                    except Exception as e:
418                        logger.warning(f"Failed to load {filename}: {e}")
419            
420            logger.info(f"Loaded {len(user_images)} user training images")
421            return user_images
422            
423        except Exception as e:
424            logger.error(f"Error loading user images: {e}")
425            return []
426
427    def create_training_dataset(self, num_samples: int = 1000, save_dataset: bool = True) -> Tuple[np.ndarray, np.ndarray]:
428        """
429        Create comprehensive training dataset with various blur types
430        Incorporates user's real training images from data/training_dataset/
431        
432        Args:
433            num_samples: Number of training samples to generate
434            save_dataset: Whether to save dataset to disk
435        
436        Returns:
437            Tuple[np.ndarray, np.ndarray]: Blurred images and clean targets
438        """
439        try:
440            logger.info(f"Creating training dataset with {num_samples} samples...")
441            
442            # Ensure dataset directory exists
443            os.makedirs(self.dataset_path, exist_ok=True)
444            
445            # Load user's training images
446            user_images = self._load_user_images()
447            
448            all_blurred = []
449            all_clean = []
450            
451            # First, process user images if available
452            if user_images:
453                logger.info(f"Processing {len(user_images)} user training images...")
454                for user_img in user_images:
455                    # Use user image as clean target multiple times with different blur types
456                    for _ in range(3):  # Create 3 variations per user image
457                        blur_type = np.random.choice(['gaussian', 'motion', 'defocus'])
458                        
459                        if blur_type == 'gaussian':
460                            sigma = np.random.uniform(0.5, 3.0)
461                            blurred = cv2.GaussianBlur(user_img, (0, 0), sigma)
462                        elif blur_type == 'motion':
463                            length = np.random.randint(5, 25)
464                            angle = np.random.randint(0, 180)
465                            kernel = self._create_motion_kernel(length, angle)
466                            blurred = cv2.filter2D(user_img, -1, kernel)
467                        else:  # defocus
468                            sigma = np.random.uniform(1.0, 4.0)
469                            blurred = cv2.GaussianBlur(user_img, (0, 0), sigma)
470                        
471                        # Add slight noise for realism
472                        noise = np.random.normal(0, 3, blurred.shape).astype(np.float32)
473                        blurred = np.clip(blurred.astype(np.float32) + noise, 0, 255).astype(np.uint8)
474                        
475                        all_blurred.append(blurred)
476                        all_clean.append(user_img)
477            
478            # Generate remaining samples with synthetic images
479            remaining_samples = max(0, num_samples - len(all_blurred))
480            if remaining_samples > 0:
481                logger.info(f"Generating {remaining_samples} synthetic training samples...")
482                
483                batch_size = 50
484                num_batches = (remaining_samples + batch_size - 1) // batch_size
485                
486                for batch_idx in range(num_batches):
487                    current_batch_size = min(batch_size, remaining_samples - batch_idx * batch_size)
488                    
489                    # Create synthetic clean images
490                    clean_batch = self._generate_clean_images(current_batch_size)
491                
492                # Apply various blur types
493                blurred_batch = []
494                for clean_img in clean_batch:
495                    blur_type = np.random.choice(['gaussian', 'motion', 'defocus'])
496                    
497                    if blur_type == 'gaussian':
498                        sigma = np.random.uniform(0.5, 3.0)
499                        blurred = cv2.GaussianBlur(clean_img, (0, 0), sigma)
500                    elif blur_type == 'motion':
501                        length = np.random.randint(5, 25)
502                        angle = np.random.randint(0, 180)
503                        kernel = self._create_motion_kernel(length, angle)
504                        blurred = cv2.filter2D(clean_img, -1, kernel)
505                    else:  # defocus
506                        sigma = np.random.uniform(1.0, 4.0)
507                        blurred = cv2.GaussianBlur(clean_img, (0, 0), sigma)
508                    
509                    # Add slight noise for realism
510                    noise = np.random.normal(0, 5, blurred.shape).astype(np.float32)
511                    blurred = np.clip(blurred.astype(np.float32) + noise, 0, 255).astype(np.uint8)
512                    
513                    blurred_batch.append(blurred)
514                
515                all_blurred.extend(blurred_batch)
516                all_clean.extend(clean_batch)
517                
518                if (batch_idx + 1) % 5 == 0:
519                    logger.info(f"Generated batch {batch_idx + 1}/{num_batches}")
520            
521            # Convert to numpy arrays
522            blurred_dataset = np.array(all_blurred)
523            clean_dataset = np.array(all_clean)
524            
525            # Normalize to [0, 1]
526            blurred_dataset = blurred_dataset.astype(np.float32) / 255.0
527            clean_dataset = clean_dataset.astype(np.float32) / 255.0
528            
529            logger.info(f"Dataset created: {blurred_dataset.shape} blurred, {clean_dataset.shape} clean")
530            
531            # Save dataset if requested
532            if save_dataset:
533                np.save(os.path.join(self.dataset_path, 'blurred_images.npy'), blurred_dataset)
534                np.save(os.path.join(self.dataset_path, 'clean_images.npy'), clean_dataset)
535                logger.info(f"Dataset saved to {self.dataset_path}")
536            
537            return blurred_dataset, clean_dataset
538            
539        except Exception as e:
540            logger.error(f"Error creating training dataset: {e}")
541            return np.array([]), np.array([])
542    
543    def _generate_clean_images(self, num_images: int) -> List[np.ndarray]:
544        """Generate synthetic clean images for training"""
545        clean_images = []
546        
547        for _ in range(num_images):
548            # Create random patterns and shapes
549            img = np.zeros((self.input_shape[0], self.input_shape[1], 3), dtype=np.uint8)
550            
551            # Random background
552            bg_color = np.random.randint(0, 255, 3)
553            img[:] = bg_color
554            
555            # Add random shapes
556            num_shapes = np.random.randint(3, 8)
557            for _ in range(num_shapes):
558                shape_type = np.random.choice(['rectangle', 'circle', 'line'])
559                color = np.random.randint(0, 255, 3).tolist()
560                
561                if shape_type == 'rectangle':
562                    pt1 = (np.random.randint(0, img.shape[1]//2), np.random.randint(0, img.shape[0]//2))
563                    pt2 = (np.random.randint(img.shape[1]//2, img.shape[1]), 
564                          np.random.randint(img.shape[0]//2, img.shape[0]))
565                    cv2.rectangle(img, pt1, pt2, color, -1)
566                    
567                elif shape_type == 'circle':
568                    center = (np.random.randint(0, img.shape[1]), np.random.randint(0, img.shape[0]))
569                    radius = np.random.randint(10, 50)
570                    cv2.circle(img, center, radius, color, -1)
571                    
572                else:  # line
573                    pt1 = (np.random.randint(0, img.shape[1]), np.random.randint(0, img.shape[0]))
574                    pt2 = (np.random.randint(0, img.shape[1]), np.random.randint(0, img.shape[0]))
575                    thickness = np.random.randint(1, 5)
576                    cv2.line(img, pt1, pt2, color, thickness)
577            
578            # Add random text
579            if np.random.random() > 0.5:
580                text = ''.join(np.random.choice(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 
581                                              np.random.randint(3, 8)))
582                font = cv2.FONT_HERSHEY_SIMPLEX
583                font_scale = np.random.uniform(0.5, 2.0)
584                color = np.random.randint(0, 255, 3).tolist()
585                thickness = np.random.randint(1, 3)
586                position = (np.random.randint(0, img.shape[1]//2), np.random.randint(20, img.shape[0]))
587                cv2.putText(img, text, position, font, font_scale, color, thickness)
588            
589            clean_images.append(img)
590        
591        return clean_images
592    
593    def load_existing_dataset(self) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
594        """Load existing dataset from disk"""
595        try:
596            blurred_path = os.path.join(self.dataset_path, 'blurred_images.npy')
597            clean_path = os.path.join(self.dataset_path, 'clean_images.npy')
598            
599            if os.path.exists(blurred_path) and os.path.exists(clean_path):
600                blurred_data = np.load(blurred_path)
601                clean_data = np.load(clean_path)
602                logger.info(f"Loaded existing dataset: {blurred_data.shape} samples")
603                return blurred_data, clean_data
604            else:
605                logger.info("No existing dataset found")
606                return None, None
607                
608        except Exception as e:
609            logger.error(f"Error loading existing dataset: {e}")
610            return None, None
611    
612    def train_model(self, 
613                   epochs: int = 20, 
614                   batch_size: int = 16, 
615                   validation_split: float = 0.2,
616                   use_existing_dataset: bool = True,
617                   num_training_samples: int = 1000) -> bool:
618        """
619        Train the CNN model with comprehensive dataset
620        
621        Args:
622            epochs: Number of training epochs
623            batch_size: Training batch size
624            validation_split: Fraction of data for validation
625            use_existing_dataset: Whether to use existing saved dataset
626            num_training_samples: Number of samples to generate if creating new dataset
627        
628        Returns:
629            bool: Training success status
630        """
631        try:
632            logger.info("Starting CNN model training...")
633            
634            # Build model if not exists
635            if self.model is None:
636                self.build_model()
637            
638            # Load or create dataset
639            if use_existing_dataset:
640                blurred_data, clean_data = self.load_existing_dataset()
641                if blurred_data is None:
642                    logger.info("Creating new dataset...")
643                    blurred_data, clean_data = self.create_training_dataset(num_training_samples)
644            else:
645                logger.info("Creating new dataset...")
646                blurred_data, clean_data = self.create_training_dataset(num_training_samples)
647            
648            if len(blurred_data) == 0:
649                logger.error("Failed to create/load training dataset")
650                return False
651            
652            logger.info(f"Training on {len(blurred_data)} samples")
653            
654            # Setup callbacks
655            callbacks = [
656                keras.callbacks.EarlyStopping(
657                    monitor='val_loss', 
658                    patience=5, 
659                    restore_best_weights=True
660                ),
661                keras.callbacks.ReduceLROnPlateau(
662                    monitor='val_loss', 
663                    factor=0.5, 
664                    patience=3, 
665                    min_lr=1e-7
666                ),
667                keras.callbacks.ModelCheckpoint(
668                    filepath=self.model_path,
669                    monitor='val_loss',
670                    save_best_only=True,
671                    save_weights_only=False
672                )
673            ]
674            
675            # Train model
676            self.training_history = self.model.fit(
677                blurred_data, clean_data,
678                epochs=epochs,
679                batch_size=batch_size,
680                validation_split=validation_split,
681                callbacks=callbacks,
682                verbose=1
683            )
684            
685            # Save final model
686            self.save_model(self.model_path)
687            self.is_trained = True
688            
689            # Save training history
690            history_path = self.model_path.replace('.h5', '_history.pkl')
691            with open(history_path, 'wb') as f:
692                pickle.dump(self.training_history.history, f)
693            
694            logger.info("Training completed successfully!")
695            logger.info(f"Model saved to: {self.model_path}")
696            
697            # Print training summary
698            final_loss = self.training_history.history['loss'][-1]
699            final_val_loss = self.training_history.history['val_loss'][-1]
700            logger.info(f"Final training loss: {final_loss:.4f}")
701            logger.info(f"Final validation loss: {final_val_loss:.4f}")
702            
703            return True
704            
705        except Exception as e:
706            logger.error(f"Error during training: {e}")
707            return False
708    
709    def evaluate_model(self, test_images: np.ndarray = None, test_targets: np.ndarray = None) -> dict:
710        """
711        Evaluate model performance on test data
712        
713        Args:
714            test_images: Test images (if None, creates synthetic test set)
715            test_targets: Test targets (if None, creates synthetic test set)
716        
717        Returns:
718            dict: Evaluation metrics
719        """
720        try:
721            if self.model is None or not self.is_trained:
722                logger.error("Model not trained. Train the model first.")
723                return {}
724            
725            # Create test data if not provided
726            if test_images is None or test_targets is None:
727                logger.info("Creating test dataset...")
728                test_images, test_targets = self.create_training_dataset(num_samples=100, save_dataset=False)
729            
730            # Evaluate
731            results = self.model.evaluate(test_images, test_targets, verbose=0)
732            
733            metrics = {
734                'loss': results[0],
735                'mae': results[1],
736                'mse': results[2]
737            }
738            
739            logger.info("Model Evaluation Results:")
740            for metric, value in metrics.items():
741                logger.info(f"  {metric}: {value:.4f}")
742            
743            return metrics
744            
745        except Exception as e:
746            logger.error(f"Error during evaluation: {e}")
747            return {}
748
749# Convenience functions
750def load_cnn_model(model_path: str = "models/cnn_model.h5") -> CNNDeblurModel:
751    """
752    Load CNN deblurring model
753    
754    Args:
755        model_path: Path to model file
756    
757    Returns:
758        CNNDeblurModel: Loaded model instance
759    """
760    model = CNNDeblurModel()
761    model.load_model(model_path)
762    return model
763
764def enhance_with_cnn(image: np.ndarray, model_path: str = "models/cnn_model.h5") -> np.ndarray:
765    """
766    Enhance image using CNN model
767    
768    Args:
769        image: Input image
770        model_path: Path to model file
771    
772    Returns:
773        np.ndarray: Enhanced image
774    """
775    model = load_cnn_model(model_path)
776    return model.enhance_image(image)
777
778# Training utility functions
779def train_new_model(num_samples: int = 1000, epochs: int = 20, input_shape: Tuple[int, int, int] = (256, 256, 3)):
780    """
781    Train a new CNN deblurring model from scratch
782    
783    Args:
784        num_samples: Number of training samples to generate
785        epochs: Number of training epochs
786        input_shape: Input image shape
787    
788    Returns:
789        CNNDeblurModel: Trained model
790    """
791    print("๐Ÿš€ Training New CNN Deblurring Model")
792    print("=" * 50)
793    
794    # Ensure directories exist
795    os.makedirs("models", exist_ok=True)
796    os.makedirs("data/training_dataset", exist_ok=True)
797    
798    # Initialize model
799    model = CNNDeblurModel(input_shape=input_shape)
800    
801    # Train model
802    success = model.train_model(
803        epochs=epochs,
804        batch_size=16,
805        validation_split=0.2,
806        use_existing_dataset=True,
807        num_training_samples=num_samples
808    )
809    
810    if success:
811        print("โœ… Training completed successfully!")
812        
813        # Evaluate model
814        metrics = model.evaluate_model()
815        if metrics:
816            print(f"๐Ÿ“Š Model Performance:")
817            print(f"   Loss: {metrics['loss']:.4f}")
818            print(f"   MAE: {metrics['mae']:.4f}")
819            print(f"   MSE: {metrics['mse']:.4f}")
820        
821        return model
822    else:
823        print("โŒ Training failed!")
824        return None
825
826def quick_train():
827    """Quick training with default parameters"""
828    return train_new_model(num_samples=500, epochs=10)
829
830def full_train():
831    """Full training with comprehensive dataset"""
832    return train_new_model(num_samples=2000, epochs=30)
833
834# Example usage and testing
835if __name__ == "__main__":
836    import argparse
837    
838    parser = argparse.ArgumentParser(description='CNN Deblurring Module')
839    parser.add_argument('--train', action='store_true', help='Train the model')
840    parser.add_argument('--quick-train', action='store_true', help='Quick training (500 samples, 10 epochs)')
841    parser.add_argument('--full-train', action='store_true', help='Full training (2000 samples, 30 epochs)')
842    parser.add_argument('--samples', type=int, default=1000, help='Number of training samples')
843    parser.add_argument('--epochs', type=int, default=20, help='Number of training epochs')
844    parser.add_argument('--test', action='store_true', help='Test the model')
845    
846    args = parser.parse_args()
847    
848    print("๐ŸŽฏ CNN Deblurring Module")
849    print("=" * 30)
850    
851    if args.quick_train:
852        print("๐Ÿš€ Quick Training Mode")
853        model = quick_train()
854        
855    elif args.full_train:
856        print("๐Ÿš€ Full Training Mode")
857        model = full_train()
858        
859    elif args.train:
860        print(f"๐Ÿš€ Custom Training Mode")
861        model = train_new_model(num_samples=args.samples, epochs=args.epochs)
862        
863    elif args.test:
864        print("๐Ÿงช Testing Mode")
865        
866        # Create test image
867        test_image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
868        
869        # Initialize model
870        cnn_model = CNNDeblurModel()
871        
872        # Try to load existing model
873        if cnn_model.load_model(cnn_model.model_path):
874            print(f"โœ… Loaded existing trained model")
875        else:
876            print(f"โ„น๏ธ No trained model found, building new model")
877            cnn_model.build_model()
878        
879        print(f"Model input shape: {cnn_model.input_shape}")
880        print(f"Model built: {cnn_model.model is not None}")
881        print(f"Model trained: {cnn_model.is_trained}")
882        
883        # Test enhancement
884        enhanced = cnn_model.enhance_image(test_image)
885        print(f"Original shape: {test_image.shape}")
886        print(f"Enhanced shape: {enhanced.shape}")
887        
888        if cnn_model.is_trained:
889            # Evaluate on test data
890            metrics = cnn_model.evaluate_model()
891            if metrics:
892                print("๐Ÿ“Š Model Performance:")
893                for metric, value in metrics.items():
894                    print(f"   {metric}: {value:.4f}")
895    
896    else:
897        print("โ„น๏ธ Usage options:")
898        print("  --test          Test existing model or build new one")
899        print("  --quick-train   Quick training (500 samples, 10 epochs)")
900        print("  --full-train    Full training (2000 samples, 30 epochs)")
901        print("  --train         Custom training (use --samples and --epochs)")
902        print("\nExamples:")
903        print("  python -m modules.cnn_deblurring --test")
904        print("  python -m modules.cnn_deblurring --quick-train")
905        print("  python -m modules.cnn_deblurring --train --samples 1500 --epochs 25")
906    
907    print("\n๐ŸŽฏ CNN deblurring module ready!")