Spaces:
Sleeping
Sleeping
File size: 7,927 Bytes
dc9e606 | 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 | import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""
DoubleConv Module
=================
A standard building block for UNet, consisting of two consecutive convolution layers.
Each 3x3 convolution is followed by Batch Normalization and ReLU activation.
Structure:
Input -> [Conv3x3 -> BatchNorm -> ReLU] -> [Conv3x3 -> BatchNorm -> ReLU] -> Output
"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.double_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class Down(nn.Module):
"""
Down Module
===========
Handles the downsampling step in the encoder part of the UNet.
It supports two modes of downsampling:
1. 'maxpool': Uses MaxPool2d(2) to halve the spatial dimensions.
2. 'strided': Uses a Strided Conv (kernel=3, stride=2) to halve dimensions while learning features.
After downsampling, a DoubleConv block processes the features.
"""
def __init__(self, in_channels, out_channels, mode='maxpool'):
super().__init__()
self.mode = mode
if mode == 'maxpool':
# Option 1: MaxPool downsampling (Standard UNet)
self.down_layer = nn.MaxPool2d(2)
self.conv = DoubleConv(in_channels, out_channels)
elif mode == 'strided':
# Option 2: Strided Convolution downsampling
# Replaces the pooling operation with a learnable strided convolution
self.down_layer = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
# The strided conv handles the channel change (in -> out).
# The following DoubleConv refines these features (out -> out).
self.conv = DoubleConv(out_channels, out_channels)
else:
raise ValueError(f"Unknown downsample mode: {mode}")
def forward(self, x):
if self.mode == 'maxpool':
x = self.down_layer(x)
return self.conv(x)
else:
# Strided path
x = self.down_layer(x) # [B, OutCh, H/2, W/2]
return self.conv(x) # [B, OutCh, H/2, W/2]
class Up(nn.Module):
"""
Up Module
=========
Handles the upsampling step in the decoder part of the UNet.
It supports two modes of upsampling:
1. 'transpose': Uses ConvTranspose2d to learn how to upsample.
2. 'upsample': Uses bilinear interpolation (nn.Upsample).
Steps:
1. Upsample the input tensor (x1) from the previous lower layer.
2. Concatenate it with the corresponding feature map from the encoder (x2) (Skip Connection).
- Handles padding if dimensions don't match perfectly.
3. Process the combined features with a DoubleConv block.
"""
def __init__(self, in_channels, out_channels, mode='transpose'):
super().__init__()
if mode == 'transpose':
# Option 1: Transpose Convolution
# Typical for original UNet. Upsamples and reduces channels by half.
# in_channels is the dimension of the deep feature map coming UP.
self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
self.up_mode = 'transpose'
elif mode == 'upsample':
# Option 2: Bilinear Upsampling
# Does not reduce channels itself, so we need a 1x1 conv to reduce channels
# to match the skip connection size before DoubleConv.
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
self.conv_adjust = nn.Conv2d(in_channels, in_channels // 2, kernel_size=1)
self.up_mode = 'upsample'
else:
raise ValueError(f"Unknown upsample mode: {mode}")
# DoubleConv takes the concatenated input.
# Channels = (in_channels // 2 from Up) + (in_channels // 2 from Skip) = in_channels
# Outputs count = out_channels
self.conv = DoubleConv(in_channels, out_channels)
def forward(self, x1, x2):
"""
x1: Input from the previous decoder layer (to be upsampled)
x2: Input from the encoder layer (skip connection)
"""
x1 = self.up(x1)
if hasattr(self, 'conv_adjust'):
x1 = self.conv_adjust(x1)
# Handle padding if x1 and x2 have slightly different sizes due to odd dimensions
# input is CHW
diffY = x2.size()[2] - x1.size()[2]
diffX = x2.size()[3] - x1.size()[3]
x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
diffY // 2, diffY - diffY // 2])
# Concatenate x2 (skip) and x1 (upsampled) along the channel dimension
x = torch.cat([x2, x1], dim=1)
return self.conv(x)
class UNet(nn.Module):
"""
UNet Architecture
=================
A U-shaped encoder-decoder architecture for image segmentation.
Configurable Parameters:
- n_channels: Number of input image channels (e.g., 3 for RGB).
- n_classes: Number of output classes (e.g., 1 for binary mask).
- downsample_mode: 'maxpool' or 'strided'.
- upsample_mode: 'transpose' or 'upsample' (bilinear).
"""
def __init__(self, n_channels, n_classes, downsample_mode='maxpool', upsample_mode='transpose'):
super(UNet, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.downsample_mode = downsample_mode
self.upsample_mode = upsample_mode
# Initial Feature Extraction
# Input: [B, n_channels, H, W] -> Output: [B, 64, H, W]
self.inc = DoubleConv(n_channels, 64)
# Encoder (Downsampling Path)
# Each step reduces H,W by 2 and doubles Channels
# Down 1: 64 -> 128
self.down1 = Down(64, 128, mode=downsample_mode)
# Down 2: 128 -> 256
self.down2 = Down(128, 256, mode=downsample_mode)
# Down 3: 256 -> 512
self.down3 = Down(256, 512, mode=downsample_mode)
# Bridge / Bottleneck
# Standard UNet goes to 1024.
self.down4 = Down(512, 1024, mode=downsample_mode)
# Decoder (Upsampling Path)
# Each step doubles H,W and halves Channels (logic handled in Up block)
self.up1 = Up(1024, 512, mode=upsample_mode)
self.up2 = Up(512, 256, mode=upsample_mode)
self.up3 = Up(256, 128, mode=upsample_mode)
self.up4 = Up(128, 64, mode=upsample_mode)
# Final Classification Layer
# Reduces 64 channels to n_classes (1 per pixel for binary)
self.outc = nn.Conv2d(64, n_classes, kernel_size=1)
def forward(self, x):
# Encoder Path with Skip Connections
x1 = self.inc(x) # [B, 64, H, W]
x2 = self.down1(x1) # [B, 128, H/2, W/2]
x3 = self.down2(x2) # [B, 256, H/4, W/4]
x4 = self.down3(x3) # [B, 512, H/8, W/8]
x5 = self.down4(x4) # [B, 1024, H/16, W/16] (Bottleneck)
# Decoder Path
# Pass skip connections (x4, x3, x2, x1) to Up modules
x = self.up1(x5, x4) # [B, 512, H/8, W/8]
x = self.up2(x, x3) # [B, 256, H/4, W/4]
x = self.up3(x, x2) # [B, 128, H/2, W/2]
x = self.up4(x, x1) # [B, 64, H, W]
logits = self.outc(x) # [B, n_classes, H, W]
return logits
|