""" U-Net Segmentation Models with Attention Mechanisms for Brain Tumor Detection """ import tensorflow as tf from tensorflow.keras import layers, Model import numpy as np def convolution_block(input_tensor, num_filters, kernel_size=3, dropout_rate=0.2, use_batchnorm=True): """ Double convolution block for U-Net Args: input_tensor: Input tensor num_filters: Number of filters in convolution layers kernel_size: Kernel size for convolutions dropout_rate: Dropout rate for regularization use_batchnorm: Whether to use batch normalization Returns: Output tensor after two convolution operations """ x = layers.Conv2D( num_filters, kernel_size, activation='relu', padding='same', kernel_initializer='he_normal' )(input_tensor) if use_batchnorm: x = layers.BatchNormalization()(x) x = layers.Dropout(dropout_rate)(x) x = layers.Conv2D( num_filters, kernel_size, activation='relu', padding='same', kernel_initializer='he_normal' )(x) if use_batchnorm: x = layers.BatchNormalization()(x) return x def attention_gate(input_tensor, gating_tensor, num_filters): """ Attention gate mechanism for U-Net skip connections Args: input_tensor: Feature map from encoder (skip connection) gating_tensor: Gating signal from decoder (upsampled path) num_filters: Number of filters Returns: Attended feature map """ # Interpolate gating tensor to match input_tensor spatial dimensions gating_tensor = layers.Resizing( height=input_tensor.shape[1], width=input_tensor.shape[2], interpolation='bilinear' )(gating_tensor) # Attention mechanism x1 = layers.Conv2D(num_filters, 1, padding='same', use_bias=False)(input_tensor) x2 = layers.Conv2D(num_filters, 1, padding='same', use_bias=False)(gating_tensor) x = layers.Add()([x1, x2]) x = layers.Activation('relu')(x) x = layers.Conv2D(1, 1, padding='same', use_bias=False)(x) x = layers.Activation('sigmoid')(x) # Apply attention weights attended_tensor = layers.Multiply()([input_tensor, x]) return attended_tensor def encoder_block(input_tensor, num_filters, dropout_rate=0.2, use_batchnorm=True): """ Encoder block: convolution + max pooling Args: input_tensor: Input tensor num_filters: Number of filters dropout_rate: Dropout rate use_batchnorm: Whether to use batch normalization Returns: Encoded tensor and skip connection """ x = convolution_block(input_tensor, num_filters, dropout_rate=dropout_rate, use_batchnorm=use_batchnorm) p = layers.MaxPooling2D(pool_size=(2, 2))(x) return x, p def decoder_block(input_tensor, skip_tensor, num_filters, use_attention=False, dropout_rate=0.2, use_batchnorm=True): """ Decoder block: upsampling + concatenation with skip connection + convolution Args: input_tensor: Input tensor from previous decoder block skip_tensor: Skip connection from encoder num_filters: Number of filters use_attention: Whether to use attention gate on skip connection dropout_rate: Dropout rate use_batchnorm: Whether to use batch normalization Returns: Decoded tensor """ # Upsampling x = layers.Conv2DTranspose( num_filters, (2, 2), strides=(2, 2), padding='same' )(input_tensor) # Apply attention gate if enabled if use_attention: skip_tensor = attention_gate(skip_tensor, x, num_filters) # Concatenate with skip connection x = layers.Concatenate()([x, skip_tensor]) # Convolution block x = convolution_block(x, num_filters, dropout_rate=dropout_rate, use_batchnorm=use_batchnorm) return x def build_unet( input_shape=(224, 224, 3), num_classes=1, base_filters=64, dropout_rate=0.2, use_batchnorm=True, use_attention=False ): """ Build U-Net model for brain tumor segmentation Args: input_shape: Input image shape (height, width, channels) num_classes: Number of output classes (1 for binary segmentation) base_filters: Number of filters in first encoder block dropout_rate: Dropout rate for regularization use_batchnorm: Whether to use batch normalization use_attention: Whether to use attention gates in skip connections Returns: U-Net model """ inputs = layers.Input(shape=input_shape, name='image_input') # Normalize input x = layers.Rescaling(1.0 / 255)(inputs) # Encoder (downsampling path) filters = base_filters skip_connections = [] for i in range(4): s, x = encoder_block(x, filters, dropout_rate=dropout_rate, use_batchnorm=use_batchnorm) skip_connections.append(s) filters *= 2 # Bottleneck x = convolution_block(x, filters, dropout_rate=dropout_rate, use_batchnorm=use_batchnorm) # Decoder (upsampling path) filters //= 2 for i in range(4): skip = skip_connections.pop() x = decoder_block( x, skip, filters, use_attention=use_attention, dropout_rate=dropout_rate, use_batchnorm=use_batchnorm ) filters //= 2 # Output layer if num_classes == 1: outputs = layers.Conv2D(num_classes, (1, 1), activation='sigmoid', padding='same')(x) else: outputs = layers.Conv2D(num_classes, (1, 1), activation='softmax', padding='same')(x) model = Model(inputs=[inputs], outputs=[outputs], name='unet') return model def build_attention_unet(input_shape=(224, 224, 3), num_classes=1, base_filters=64, dropout_rate=0.2): """ Build Attention U-Net model (U-Net with attention gates) Args: input_shape: Input image shape num_classes: Number of output classes base_filters: Number of filters in first encoder block dropout_rate: Dropout rate Returns: Attention U-Net model """ return build_unet( input_shape=input_shape, num_classes=num_classes, base_filters=base_filters, dropout_rate=dropout_rate, use_attention=True ) def build_res_unet( input_shape=(224, 224, 3), num_classes=1, base_filters=64, dropout_rate=0.2 ): """ Build Residual U-Net model (U-Net with residual connections) Args: input_shape: Input image shape num_classes: Number of output classes base_filters: Number of filters in first encoder block dropout_rate: Dropout rate Returns: Residual U-Net model """ inputs = layers.Input(shape=input_shape, name='image_input') # Normalize input x = layers.Rescaling(1.0 / 255)(inputs) # Initial convolution x = layers.Conv2D(base_filters, 3, padding='same', kernel_initializer='he_normal')(x) # Encoder with residual connections filters = base_filters skip_connections = [] for i in range(4): residual = layers.Conv2D( filters, 1, padding='same', kernel_initializer='he_normal' )(x) x = convolution_block(x, filters, dropout_rate=dropout_rate) # Add residual connection x = layers.Add()([x, residual]) skip_connections.append(x) x = layers.MaxPooling2D(pool_size=(2, 2))(x) filters *= 2 # Bottleneck x = convolution_block(x, filters, dropout_rate=dropout_rate) # Decoder filters //= 2 for i in range(4): x = layers.Conv2DTranspose( filters, (2, 2), strides=(2, 2), padding='same' )(x) skip = skip_connections.pop() x = layers.Concatenate()([x, skip]) x = convolution_block(x, filters, dropout_rate=dropout_rate) filters //= 2 # Output layer if num_classes == 1: outputs = layers.Conv2D(num_classes, (1, 1), activation='sigmoid', padding='same')(x) else: outputs = layers.Conv2D(num_classes, (1, 1), activation='softmax', padding='same')(x) model = Model(inputs=[inputs], outputs=[outputs], name='res_unet') return model def build_multi_modal_unet( input_shapes=[(224, 224, 3), (224, 224, 3)], num_classes=1, base_filters=64, dropout_rate=0.2, fusion_method='attention' ): """ Build multi-modal U-Net with attention fusion for combining multiple input modalities Args: input_shapes: List of input shapes for different modalities num_classes: Number of output classes base_filters: Number of filters in first encoder block dropout_rate: Dropout rate fusion_method: Method for fusing modalities ('attention', 'concat', 'add') Returns: Multi-modal U-Net model """ inputs = [layers.Input(shape=shape, name=f'modality_{i}_input') for i, shape in enumerate(input_shapes)] # Process each modality through initial convolutions modality_features = [] for i, inp in enumerate(inputs): x = layers.Rescaling(1.0 / 255)(inp) x = layers.Conv2D(base_filters, 3, padding='same', kernel_initializer='he_normal')(x) modality_features.append(x) # Fuse modalities if fusion_method == 'attention': # Attention-based fusion fused = attention_fusion(modality_features, base_filters) elif fusion_method == 'concat': fused = layers.Concatenate()(modality_features) fused = layers.Conv2D(base_filters, 1, padding='same')(fused) elif fusion_method == 'add': fused = layers.Add()(modality_features) else: raise ValueError(f"Unknown fusion method: {fusion_method}") # Continue with U-Net architecture filters = base_filters skip_connections = [] # Encoder for i in range(4): s, fused = encoder_block(fused, filters, dropout_rate=dropout_rate) skip_connections.append(s) filters *= 2 # Bottleneck fused = convolution_block(fused, filters, dropout_rate=dropout_rate) # Decoder filters //= 2 for i in range(4): skip = skip_connections.pop() fused = decoder_block(fused, skip, filters, dropout_rate=dropout_rate) filters //= 2 # Output layer if num_classes == 1: outputs = layers.Conv2D(num_classes, (1, 1), activation='sigmoid', padding='same')(fused) else: outputs = layers.Conv2D(num_classes, (1, 1), activation='softmax', padding='same')(fused) model = Model(inputs=inputs, outputs=outputs, name='multi_modal_unet') return model def attention_fusion(feature_maps, num_filters): """ Attention-based fusion of multiple feature maps Args: feature_maps: List of feature maps to fuse num_filters: Number of filters for attention computation Returns: Fused feature map """ if len(feature_maps) == 1: return feature_maps[0] # Compute attention weights for each feature map attention_weights = [] for fm in feature_maps: # Global average pooling to get channel-wise statistics gap = layers.GlobalAveragePooling2D()(fm) # Learn attention weights w = layers.Dense(num_filters, activation='relu')(gap) w = layers.Dense(num_filters, activation='softmax')(w) w = layers.Reshape((1, 1, num_filters))(w) attention_weights.append(w) # Apply attention weights and sum weighted_features = [layers.Multiply()([fm, w]) for fm, w in zip(feature_maps, attention_weights)] fused = layers.Add()(weighted_features) return fused def dice_coefficient(y_true, y_pred, smooth=1e-6): """ Dice coefficient metric for segmentation evaluation Args: y_true: Ground truth masks y_pred: Predicted masks smooth: Smoothing factor to avoid division by zero Returns: Dice coefficient value """ y_true_f = tf.reshape(y_true, [-1]) y_pred_f = tf.reshape(y_pred, [-1]) intersection = tf.reduce_sum(y_true_f * y_pred_f) return (2. * intersection + smooth) / (tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f) + smooth) def dice_loss(y_true, y_pred, smooth=1e-6): """ Dice loss function for training segmentation models Args: y_true: Ground truth masks y_pred: Predicted masks smooth: Smoothing factor Returns: Dice loss value """ return 1 - dice_coefficient(y_true, y_pred, smooth) def combined_loss(weights=None): """ Combined loss function (weighted sum of dice loss and binary crossentropy) Args: weights: Weights for each loss component [dice_weight, bce_weight] Returns: Combined loss function """ if weights is None: weights = [0.5, 0.5] def loss(y_true, y_pred): bce = tf.keras.losses.binary_crossentropy(y_true, y_pred) dice = dice_loss(y_true, y_pred) return weights[0] * dice + weights[1] * bce return loss def iou_metric(y_true, y_pred, smooth=1e-6): """ Intersection over Union (IoU) metric for segmentation Args: y_true: Ground truth masks y_pred: Predicted masks smooth: Smoothing factor Returns: IoU value """ y_true_f = tf.reshape(y_true, [-1]) y_pred_f = tf.reshape(y_pred, [-1]) intersection = tf.reduce_sum(y_true_f * y_pred_f) union = tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f) - intersection return (intersection + smooth) / (union + smooth) def get_segmentation_model(model_name='unet', **kwargs): """ Factory function to get segmentation models Args: model_name: Name of the model ('unet', 'attention_unet', 'res_unet', 'multi_modal_unet') **kwargs: Additional arguments passed to the model builder Returns: Segmentation model """ models = { 'unet': build_unet, 'attention_unet': build_attention_unet, 'res_unet': build_res_unet, 'multi_modal_unet': build_multi_modal_unet } if model_name not in models: raise ValueError(f"Unknown model name: {model_name}. Available models: {list(models.keys())}") return models[model_name](**kwargs)