Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

U-Net Quick Reference

U-Net is a convolutional neural network architecture designed for semantic segmentation, particularly in biomedical image segmentation. It features a symmetric encoder-decoder structure with skip connections that preserve spatial information during upsampling.

What the Algorithm Does

U-Net performs pixel-wise classification to segment images into different regions or classes. The architecture consists of:

  • Encoder (Contracting Path): Captures context through downsampling with convolutional and pooling layers
  • Decoder (Expanding Path): Enables precise localization through upsampling with transposed convolutions
  • Skip Connections: Concatenates feature maps from encoder to decoder at corresponding levels, preserving fine-grained spatial information

The network outputs a segmentation mask where each pixel is classified into one of the target classes.

When to Use It

Problem Types

  • Medical image segmentation: Cell segmentation, organ delineation, tumor detection
  • Semantic segmentation: Object boundaries, scene understanding
  • Instance segmentation: When combined with post-processing techniques
  • Image restoration: Denoising, super-resolution with modifications

Data Characteristics

  • Small to medium datasets: Works well with limited training data (hundreds to thousands of images)
  • High spatial resolution: Preserves fine details through skip connections
  • Clear object boundaries: Excels when precise boundary delineation is critical
  • Balanced classes: Performs best when classes are not heavily imbalanced

Business Contexts

  • Medical imaging diagnostics
  • Autonomous driving (road segmentation)
  • Satellite imagery analysis
  • Industrial quality control and defect detection

Comparison with Alternatives

  • vs FCN: Better boundary preservation due to skip connections
  • vs DeepLab: Simpler architecture, better for small datasets
  • vs Mask R-CNN: Better for dense segmentation, Mask R-CNN better for instance segmentation

Strengths & Weaknesses

Strengths

  • Excellent boundary preservation: Skip connections maintain spatial resolution
  • Works with small datasets: Data augmentation and architecture design enable training on limited data
  • Fast inference: Relatively lightweight compared to other segmentation models
  • End-to-end training: Single network handles both feature extraction and segmentation
  • Interpretable: Clear encoder-decoder structure is easy to understand and modify

Weaknesses

  • Memory intensive: Skip connections require storing intermediate feature maps
  • Limited context modeling: Smaller receptive field compared to dilated convolution approaches
  • Class imbalance sensitivity: Struggles with heavily imbalanced segmentation tasks
  • Fixed input size: Original architecture requires fixed input dimensions
  • Limited multi-scale handling: Doesn't explicitly handle objects at multiple scales

Important Hyperparameters

Architecture Parameters

  • Depth levels: Number of encoder-decoder levels (typically 4-5)

    • Range: 3-6 levels
    • Tuning: More levels = larger receptive field but more memory
    • Default: 4 levels for 256x256 images
  • Base filters: Number of filters in first layer (typically 64)

    • Range: 32-128
    • Tuning: More filters = higher capacity but slower training
    • Default: 64 filters
  • Filter growth: How filters increase with depth (typically 2x per level)

    • Range: 1.5x to 2x
    • Default: 2x growth rate

Training Parameters

  • Learning rate:

    • Range: 1e-5 to 1e-2
    • Default: 1e-4 with Adam optimizer
    • Tuning: Use learning rate scheduling (reduce on plateau)
  • Batch size:

    • Range: 1-16 (limited by memory)
    • Default: 4-8 for high-resolution images
    • Tuning: Larger batches stabilize training but require more memory
  • Loss function weights: For handling class imbalance

    • Use weighted cross-entropy or focal loss
    • Calculate weights inverse to class frequency

Key Assumptions

Data Assumptions

  • Spatial coherence: Assumes objects have coherent spatial structure
  • Clear boundaries: Works best when target objects have well-defined edges
  • Consistent imaging conditions: Assumes similar contrast, brightness, and noise levels across dataset
  • Adequate resolution: Input images should have sufficient resolution to capture target details

Architectural Assumptions

  • Symmetrical structure: Encoder and decoder have corresponding levels
  • Skip connection compatibility: Feature maps at corresponding levels have compatible dimensions
  • Translation invariance: Assumes target objects can appear anywhere in the image

Violations and Solutions

  • Inconsistent illumination: Use histogram equalization or batch normalization
  • Multi-scale objects: Use U-Net++ or feature pyramid modifications
  • Class imbalance: Apply weighted loss functions or focal loss

Performance Characteristics

Time Complexity

  • Training: O(N × H × W × C × F²) per epoch
    • N: batch size, H×W: image dimensions, C: channels, F: filter size
  • Inference: O(H × W × C × F²) per image
  • Memory: O(H × W × D × max_filters) for feature maps storage

Space Complexity

  • Memory usage: 4-8GB GPU memory for 512×512 images with batch size 4
  • Model size: 20-50MB for standard architecture
  • Scalability: Memory grows quadratically with input resolution

Convergence Properties

  • Training epochs: Typically 50-200 epochs for convergence
  • Early stopping: Monitor validation Dice score or IoU
  • Plateau detection: Learning rate reduction after 10-20 epochs without improvement

Evaluation & Compare Models

Appropriate Metrics

  • Dice Score (F1-score): Primary metric for segmentation overlap
    • Formula: 2×|A∩B| / (|A|+|B|)
    • Range: 0-1, higher is better
  • Intersection over Union (IoU): Jaccard index for overlap measurement
  • Pixel accuracy: Overall percentage of correctly classified pixels
  • Hausdorff distance: For boundary accuracy assessment

Cross-validation Strategies

  • K-fold cross-validation: 5-fold typical for medical imaging
  • Patient-wise splitting: Ensure no data leakage between patients
  • Temporal splitting: For time-series medical data
  • Stratified sampling: Maintain class distribution across folds

Baseline Comparisons

  • Simple thresholding: Basic intensity-based segmentation
  • Classical ML: SVM with hand-crafted features
  • FCN-8s: Fully convolutional network baseline
  • DeepLab v3+: State-of-the-art comparison

Statistical Significance

  • Paired t-test: Compare Dice scores across test cases
  • Wilcoxon signed-rank test: Non-parametric alternative
  • Bootstrap confidence intervals: For robust statistical assessment

Practical Usage Guidelines

Implementation Tips

  • Data augmentation: Essential for small datasets (rotation, scaling, elastic deformation)
  • Batch normalization: Add after each convolution for training stability
  • Dropout: Use dropout (0.2-0.5) in encoder bottleneck to prevent overfitting
  • Weight initialization: Use He initialization for ReLU activations

Common Mistakes

  • Inadequate data augmentation: Leads to overfitting on small datasets
  • Wrong loss function: Using cross-entropy without class weighting for imbalanced data
  • Skip connection mismatch: Ensure encoder-decoder level correspondence
  • Insufficient training time: Segmentation requires longer training than classification

Debugging Strategies

  • Visualize predictions: Plot input, ground truth, and predictions side by side
  • Check loss curves: Monitor training and validation loss for overfitting
  • Gradient flow: Verify gradients flow properly through skip connections
  • Feature map analysis: Visualize intermediate activations to understand learning

Production Considerations

  • Model quantization: Use FP16 or INT8 for faster inference
  • Batch processing: Process multiple images simultaneously for efficiency
  • Post-processing: Apply morphological operations to clean predictions
  • Ensemble methods: Combine multiple models for robust predictions

Complete Example with Step-by-Step Explanation

Step 1: Data Preparation

# What's happening: Loading and preprocessing medical image dataset
# Why this step: U-Net requires paired input images and segmentation masks
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import cv2
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
import os
import glob

def load_data(image_dir, mask_dir, img_size=(256, 256)):
    """Load and preprocess images and masks"""
    image_paths = sorted(glob.glob(os.path.join(image_dir, "*.jpg")))
    mask_paths = sorted(glob.glob(os.path.join(mask_dir, "*.png")))

    images = []
    masks = []

    for img_path, mask_path in zip(image_paths, mask_paths):
        # Load image
        img = cv2.imread(img_path)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, img_size)
        img = img.astype(np.float32) / 255.0

        # Load mask
        mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
        mask = cv2.resize(mask, img_size)
        mask = mask.astype(np.float32) / 255.0
        mask = np.expand_dims(mask, axis=-1)

        images.append(img)
        masks.append(mask)

    return np.array(images), np.array(masks)

# What's happening: Creating data generators with augmentation
# Why this step: Data augmentation is crucial for U-Net to generalize well
def create_data_generator(images, masks, batch_size=8, augment=True):
    """Create data generator with augmentation"""
    if augment:
        data_gen_args = dict(
            rotation_range=20,
            width_shift_range=0.1,
            height_shift_range=0.1,
            horizontal_flip=True,
            zoom_range=0.1,
            fill_mode='nearest'
        )

        image_datagen = tf.keras.preprocessing.image.ImageDataGenerator(**data_gen_args)
        mask_datagen = tf.keras.preprocessing.image.ImageDataGenerator(**data_gen_args)

        # Ensure same random transformations for images and masks
        seed = 42
        image_generator = image_datagen.flow(images, batch_size=batch_size, seed=seed)
        mask_generator = mask_datagen.flow(masks, batch_size=batch_size, seed=seed)

        return zip(image_generator, mask_generator)
    else:
        dataset = tf.data.Dataset.from_tensor_slices((images, masks))
        dataset = dataset.batch(batch_size)
        return dataset

# Load data
X, y = load_data('path/to/images', 'path/to/masks')
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"Training data shape: {X_train.shape}, {y_train.shape}")
print(f"Validation data shape: {X_val.shape}, {y_val.shape}")

Step 2: Model Architecture

# What's happening: Implementing U-Net architecture with encoder-decoder structure
# Why this architecture: Skip connections preserve spatial information during upsampling

def conv_block(inputs, num_filters):
    """Standard convolution block with batch normalization and activation"""
    x = layers.Conv2D(num_filters, 3, padding="same")(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    x = layers.Conv2D(num_filters, 3, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    return x

def encoder_block(inputs, num_filters):
    """Encoder block with convolution and max pooling"""
    x = conv_block(inputs, num_filters)
    p = layers.MaxPool2D((2, 2))(x)
    return x, p

def decoder_block(inputs, skip_features, num_filters):
    """Decoder block with upsampling and skip connection"""
    x = layers.Conv2DTranspose(num_filters, (2, 2), strides=2, padding="same")(inputs)
    x = layers.Concatenate()([x, skip_features])  # Skip connection
    x = conv_block(x, num_filters)
    return x

def build_unet(input_shape=(256, 256, 3), num_classes=1):
    """Build U-Net model"""
    inputs = layers.Input(input_shape)

    # Encoder (Contracting Path)
    s1, p1 = encoder_block(inputs, 64)
    s2, p2 = encoder_block(p1, 128)
    s3, p3 = encoder_block(p2, 256)
    s4, p4 = encoder_block(p3, 512)

    # Bottleneck
    b1 = conv_block(p4, 1024)

    # Decoder (Expanding Path)
    d1 = decoder_block(b1, s4, 512)
    d2 = decoder_block(d1, s3, 256)
    d3 = decoder_block(d2, s2, 128)
    d4 = decoder_block(d3, s1, 64)

    # Output layer
    if num_classes == 1:
        outputs = layers.Conv2D(1, 1, padding="same", activation="sigmoid")(d4)
    else:
        outputs = layers.Conv2D(num_classes, 1, padding="same", activation="softmax")(d4)

    model = keras.Model(inputs, outputs, name="U-Net")
    return model

# What's happening: Building and compiling the model
# Why these parameters: Standard U-Net configuration for binary segmentation
model = build_unet(input_shape=(256, 256, 3), num_classes=1)
model.summary()

Step 3: Training Configuration

# What's happening: Setting up training components with appropriate loss function
# Why these choices: Dice loss handles class imbalance better than cross-entropy

def dice_coefficient(y_true, y_pred, smooth=1e-6):
    """Dice coefficient metric for segmentation"""
    y_true_f = tf.cast(tf.reshape(y_true, [-1]), tf.float32)
    y_pred_f = tf.cast(tf.reshape(y_pred, [-1]), tf.float32)

    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):
    """Dice loss function"""
    return 1 - dice_coefficient(y_true, y_pred)

def combined_loss(y_true, y_pred):
    """Combined binary crossentropy and dice loss"""
    bce = tf.keras.losses.binary_crossentropy(y_true, y_pred)
    dice = dice_loss(y_true, y_pred)
    return bce + dice

def iou_metric(y_true, y_pred, threshold=0.5):
    """Intersection over Union metric"""
    y_pred_binary = tf.cast(y_pred > threshold, tf.float32)
    y_true_binary = tf.cast(y_true, tf.float32)

    intersection = tf.reduce_sum(y_true_binary * y_pred_binary)
    union = tf.reduce_sum(y_true_binary) + tf.reduce_sum(y_pred_binary) - intersection

    return intersection / (union + 1e-6)

# Compile model with custom loss and metrics
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),
    loss=combined_loss,
    metrics=[dice_coefficient, iou_metric, 'binary_accuracy']
)

# Callbacks for training
callbacks = [
    tf.keras.callbacks.ModelCheckpoint(
        'best_unet_model.h5',
        monitor='val_dice_coefficient',
        mode='max',
        save_best_only=True,
        verbose=1
    ),
    tf.keras.callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.5,
        patience=10,
        min_lr=1e-7,
        verbose=1
    ),
    tf.keras.callbacks.EarlyStopping(
        monitor='val_dice_coefficient',
        mode='max',
        patience=20,
        restore_best_weights=True,
        verbose=1
    )
]

Step 4: Training Loop

# What's happening: Training the model with validation monitoring
# What the algorithm is learning: Pixel-wise classification with spatial consistency

# Create training and validation datasets
train_dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))
train_dataset = train_dataset.shuffle(1000).batch(8).prefetch(tf.data.AUTOTUNE)

val_dataset = tf.data.Dataset.from_tensor_slices((X_val, y_val))
val_dataset = val_dataset.batch(8).prefetch(tf.data.AUTOTUNE)

# Train the model
history = model.fit(
    train_dataset,
    epochs=100,
    validation_data=val_dataset,
    callbacks=callbacks,
    verbose=1
)

# Plot training history
def plot_training_history(history):
    """Plot training and validation metrics"""
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))

    # Loss
    axes[0, 0].plot(history.history['loss'], label='Training Loss')
    axes[0, 0].plot(history.history['val_loss'], label='Validation Loss')
    axes[0, 0].set_title('Model Loss')
    axes[0, 0].set_xlabel('Epoch')
    axes[0, 0].set_ylabel('Loss')
    axes[0, 0].legend()

    # Dice coefficient
    axes[0, 1].plot(history.history['dice_coefficient'], label='Training Dice')
    axes[0, 1].plot(history.history['val_dice_coefficient'], label='Validation Dice')
    axes[0, 1].set_title('Dice Coefficient')
    axes[0, 1].set_xlabel('Epoch')
    axes[0, 1].set_ylabel('Dice Score')
    axes[0, 1].legend()

    # IoU
    axes[1, 0].plot(history.history['iou_metric'], label='Training IoU')
    axes[1, 0].plot(history.history['val_iou_metric'], label='Validation IoU')
    axes[1, 0].set_title('IoU Metric')
    axes[1, 0].set_xlabel('Epoch')
    axes[1, 0].set_ylabel('IoU')
    axes[1, 0].legend()

    # Accuracy
    axes[1, 1].plot(history.history['binary_accuracy'], label='Training Accuracy')
    axes[1, 1].plot(history.history['val_binary_accuracy'], label='Validation Accuracy')
    axes[1, 1].set_title('Binary Accuracy')
    axes[1, 1].set_xlabel('Epoch')
    axes[1, 1].set_ylabel('Accuracy')
    axes[1, 1].legend()

    plt.tight_layout()
    plt.show()

# Plot training results
plot_training_history(history)

Step 5: Evaluation

# What's happening: Comprehensive model evaluation with multiple metrics
# How to interpret results: Dice > 0.8 indicates good segmentation performance

# Load best model
best_model = tf.keras.models.load_model(
    'best_unet_model.h5',
    custom_objects={
        'combined_loss': combined_loss,
        'dice_coefficient': dice_coefficient,
        'iou_metric': iou_metric
    }
)

def evaluate_model(model, X_test, y_test):
    """Evaluate model on test set"""
    predictions = model.predict(X_test)

    dice_scores = []
    iou_scores = []

    for i in range(len(X_test)):
        y_true = y_test[i]
        y_pred = predictions[i]

        # Calculate Dice score
        dice = dice_coefficient(y_true, y_pred).numpy()
        dice_scores.append(dice)

        # Calculate IoU
        iou = iou_metric(y_true, y_pred).numpy()
        iou_scores.append(iou)

    print(f'Average Dice Score: {np.mean(dice_scores):.4f} ± {np.std(dice_scores):.4f}')
    print(f'Average IoU Score: {np.mean(iou_scores):.4f} ± {np.std(iou_scores):.4f}')

    return dice_scores, iou_scores, predictions

# Evaluate on validation set
dice_scores, iou_scores, predictions = evaluate_model(best_model, X_val, y_val)

# Calculate additional metrics
def calculate_pixel_accuracy(y_true, y_pred, threshold=0.5):
    """Calculate pixel-wise accuracy"""
    y_pred_binary = (y_pred > threshold).astype(np.float32)
    y_true_binary = y_true.astype(np.float32)

    correct_pixels = np.sum(y_pred_binary == y_true_binary)
    total_pixels = y_true_binary.size

    return correct_pixels / total_pixels

# Calculate pixel accuracy for all predictions
pixel_accuracies = []
for i in range(len(predictions)):
    accuracy = calculate_pixel_accuracy(y_val[i], predictions[i])
    pixel_accuracies.append(accuracy)

print(f'Average Pixel Accuracy: {np.mean(pixel_accuracies):.4f} ± {np.std(pixel_accuracies):.4f}')

Step 6: Prediction and Visualization

# What's happening: Making predictions on new images and visualizing results
# How to use in practice: Apply post-processing for cleaner segmentation masks

def predict_and_visualize(model, image_path, threshold=0.5):
    """Make prediction and visualize results"""
    # Load and preprocess image
    image = cv2.imread(image_path)
    image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    original_size = image_rgb.shape[:2]

    # Preprocess for model
    image_resized = cv2.resize(image_rgb, (256, 256))
    image_normalized = image_resized.astype(np.float32) / 255.0
    input_array = np.expand_dims(image_normalized, axis=0)

    # Make prediction
    prediction = model.predict(input_array)[0]
    prediction_binary = (prediction > threshold).astype(np.uint8)

    # Resize prediction back to original size
    prediction_resized = cv2.resize(prediction_binary[:,:,0], (original_size[1], original_size[0]),
                                  interpolation=cv2.INTER_NEAREST)

    # Create visualizations
    fig, axes = plt.subplots(2, 3, figsize=(18, 12))

    # Original image
    axes[0, 0].imshow(image_rgb)
    axes[0, 0].set_title('Original Image')
    axes[0, 0].axis('off')

    # Predicted mask (probability)
    axes[0, 1].imshow(prediction[:,:,0], cmap='hot', vmin=0, vmax=1)
    axes[0, 1].set_title('Prediction Probability')
    axes[0, 1].axis('off')

    # Binary mask
    axes[0, 2].imshow(prediction_binary[:,:,0], cmap='gray')
    axes[0, 2].set_title(f'Binary Mask (threshold={threshold})')
    axes[0, 2].axis('off')

    # Original size prediction
    axes[1, 0].imshow(cv2.resize(image_rgb, (256, 256)))
    axes[1, 0].set_title('Resized Input')
    axes[1, 0].axis('off')

    # Overlay on resized image
    overlay_resized = cv2.resize(image_rgb, (256, 256)).copy()
    overlay_resized[prediction_binary[:,:,0] > 0] = [255, 0, 0]  # Red overlay
    axes[1, 1].imshow(overlay_resized)
    axes[1, 1].set_title('Overlay (256x256)')
    axes[1, 1].axis('off')

    # Overlay on original size
    overlay_original = image_rgb.copy()
    overlay_original[prediction_resized > 0] = [255, 0, 0]  # Red overlay
    axes[1, 2].imshow(overlay_original)
    axes[1, 2].set_title('Overlay (Original Size)')
    axes[1, 2].axis('off')

    plt.tight_layout()
    plt.show()

    return prediction, prediction_resized

def batch_predict_and_save(model, image_paths, output_dir, threshold=0.5):
    """Batch prediction and save results"""
    os.makedirs(output_dir, exist_ok=True)

    for i, image_path in enumerate(image_paths):
        # Load and preprocess
        image = cv2.imread(image_path)
        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        original_size = image_rgb.shape[:2]

        image_resized = cv2.resize(image_rgb, (256, 256))
        image_normalized = image_resized.astype(np.float32) / 255.0
        input_array = np.expand_dims(image_normalized, axis=0)

        # Predict
        prediction = model.predict(input_array, verbose=0)[0]
        prediction_binary = (prediction > threshold).astype(np.uint8) * 255

        # Resize back to original size
        prediction_resized = cv2.resize(prediction_binary[:,:,0],
                                      (original_size[1], original_size[0]),
                                      interpolation=cv2.INTER_NEAREST)

        # Save results
        filename = os.path.basename(image_path).split('.')[0]
        cv2.imwrite(os.path.join(output_dir, f'{filename}_mask.png'), prediction_resized)

        # Create and save overlay
        overlay = image_rgb.copy()
        overlay[prediction_resized > 0] = [255, 0, 0]
        overlay_bgr = cv2.cvtColor(overlay, cv2.COLOR_RGB2BGR)
        cv2.imwrite(os.path.join(output_dir, f'{filename}_overlay.jpg'), overlay_bgr)

        print(f'Processed {i+1}/{len(image_paths)}: {filename}')

# Example usage
prediction, prediction_resized = predict_and_visualize(best_model, 'test_image.jpg')

# Batch processing example
# image_paths = ['test1.jpg', 'test2.jpg', 'test3.jpg']
# batch_predict_and_save(best_model, image_paths, 'output_predictions/')

Summary

Key Takeaways

  • Architecture: Symmetric encoder-decoder with skip connections preserves spatial information
  • Applications: Excels in medical imaging and precise boundary segmentation tasks
  • Training: Requires careful loss function selection (Dice/IoU) for segmentation tasks
  • Data: Works well with small datasets when combined with aggressive data augmentation
  • Performance: Good balance between accuracy and computational efficiency
  • Evaluation: Use Dice score and IoU as primary metrics, pixel accuracy as secondary

Quick Reference Points

  • Best for: Small datasets, precise boundaries, medical imaging
  • Training time: 2-6 hours on modern GPU for typical dataset
  • Memory: 4-8GB GPU memory for 512×512 images
  • Key hyperparameter: Learning rate (1e-4) and batch size (4-8)
  • Common failure: Insufficient data augmentation leading to overfitting
  • Success metric: Dice score > 0.8 indicates good performance