Spaces:
Sleeping
Sleeping
File size: 15,625 Bytes
0f1fcd8 | 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 | # In this file, we will define the core encoder–decoder architecture used in our AdaIN Neural Style Transfer project.
# Here we will create two classes:-
# 1. VGGEncoder Class :-
# Purpose: Extracts hierarchical feature maps from an image using a pretrained VGG network.
# Uses a pretrained VGG network (trained on ImageNet) to extract hierarchical features from images.
# These features capture content structure and style patterns at different levels (edges, textures, shapes, semantics).
# 2. Decoder Class :-
# Purpose: Reconstructs an image back from encoded features after AdaIN has blended content + style.
# How They Work Together :-
# Encoder (VGGEncoder) :
# - Input: content image + style image.
# - Output: feature maps at different levels.
# AdaIN (Adaptive Instance Normalization) :
# - Aligns content features with style statistics (mean & std).
# Decoder (Decoder) :
# - Input: AdaIN‑modified features.
# - Output: stylized image in RGB space.
import torch
# It brings in PyTorch’s neural network module (torch.nn) and gives it the shorthand name nn.
import torch.nn as nn
# What torch.nn Provides :-
# torch.nn is the toolbox for building neural networks in PyTorch. It contains:
# Layers :
# nn.Linear → fully connected (dense) layer.
# nn.Conv2d → convolutional layer for images.
# nn.LSTM, nn.GRU → recurrent layers for sequences.
# Activations :
# nn.ReLU, nn.Sigmoid, nn.Softmax, etc.
# Loss Functions :
# nn.CrossEntropyLoss, nn.MSELoss, etc.
# Model Container :
# nn.Module → the base class for all models and layers.
# You subclass nn.Module to define custom models.
# Here this encoder remains fixed i.e we will use pre-trained VGG model only & will not perform any training for it.
# But for decoder we will perform training i.e we will actually trained that.
# What encode does :-
# Uses a pretrained VGG network (trained on ImageNet) to extract hierarchical features from images.
# These features capture content structure and style patterns at different levels (edges, textures, shapes, semantics).
# Why it’s fixed :-
# VGG is already trained to recognize rich visual features.
# In style transfer, we don’t want to retrain it — we only use it as a feature extractor backbone.
# That’s why requires_grad=False is set: no gradient updates, no training.
# Output :-
# Multi‑level feature maps (h1, h2, h3, h4) or just the deepest features (h4 if is_test=True).
# This is the encoder class
# VGG (Visual Geometry Group network) is a famous CNN architecture used for image recognition and feature extraction
# It’s built from repeated blocks of:
# Convolutions (nn.Conv2d) → learn filters like edges, textures, shapes.
# ReLU activations (nn.ReLU) → add non‑linearity.
# Pooling (nn.MaxPool2d) → downsample feature maps.
# Padding (nn.ReflectionPad2d) → preserve spatial dimensions and reduce border artifacts.
class VGGEncoder(nn.Module): #here we are inheriting this nn.Module class
# vgg_path → an argument passed when you create the encoder, telling it where to load the pretrained VGG weights from.
def __init__(self, vgg_path):
super(VGGEncoder, self).__init__() # here we are passing this ANN custom class as parameter for its parent class
# Defining the architecture of our Convolutional Neural Network (CNN) using PyTorch’s nn.Sequential container.
# nn.Sequential is a container that lets you stack layers in the order they should be applied.
# Instead of writing a custom forward() method that manually calls each layer, you can just list them inside nn.Sequential, and PyTorch will apply them one after another.
# THe disadvantage of using nn.Sequential is that as it is a container, we will only get the final result & not the intermediate results even of we want them
# But fron VGG, we want those intermediate results actually
self.vgg = nn.Sequential(
# This here is actually the architecture of VGG
# Here we are actually rebuilding the VGG network architecture layer by layer inside an nn.Sequential container
# Here these relu1-1 or relu2-1 etc are defined by authors for taking the output from these layers when we want intermediate layers output actually
# Block 1
nn.Conv2d(3, 3, (1, 1)),
# Reflection padding :- Used instead of zero padding to avoid sharp edges at borders, which is important for style transfer.
# This reflection padding, the last values of edges gets copied as padding actually
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(3, 64, (3, 3)),
nn.ReLU(), # relu1-1 :- 1st relu of this block 1
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(64, 64, (3, 3)),
nn.ReLU(), # relu1-2 :- 2nd relu of this block 1
nn.MaxPool2d((2, 2), (2, 2), (0, 0), ceil_mode=True), # Downsamples the feature maps by half (reduces spatial size, keeps important features).
# Block 2
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(64, 128, (3, 3)),
nn.ReLU(), # relu2-1 :- 1st relu of this block 2
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(128, 128, (3, 3)),
nn.ReLU(), # relu2-2 :- 2nd relu of this block 2
nn.MaxPool2d((2, 2), (2, 2), (0, 0), ceil_mode=True),
# Block 3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(128, 256, (3, 3)),
nn.ReLU(), # relu3-1 :- 1st relu of this block 3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(), # relu3-2 :- 2nd relu of this block 3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(), # relu3-3 :- 3rd relu of this block 3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(), # relu3-4 :- 4th relu of this block 3
nn.MaxPool2d((2, 2), (2, 2), (0, 0), ceil_mode=True),
# Block 4
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 512, (3, 3)),
nn.ReLU(), # relu4-1, this is the last layer used
# so we will actually only take till this layer as we kind of want intermediate layer output which is these layers output
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu4-2
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu4-3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu4-4
nn.MaxPool2d((2, 2), (2, 2), (0, 0), ceil_mode=True),
# Block 5
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu5-1
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu5-2
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU(), # relu5-3
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(512, 512, (3, 3)),
nn.ReLU() # relu5-4
)
# Now we will load the pre-trained weight for this VGG model from this file at vgg_path
# so this file (i.e actually vgg_normalised.pth) actually contains the pre-trained weights of VGG
# Loads pretrained weights from the file at vgg_path to this VGG model
self.vgg.load_state_dict(torch.load(vgg_path))
# torch.load(vgg_path) :- Reads the file at vgg_path (e.g., "/vgg_normalized.pth").
# That file contains a Python dictionary mapping layer names → parameter tensors (weights and biases).
# self.vgg.load_state_dict(...) :- Loads those parameters into your self.vgg model (the nn.Sequential you defined).
# Ensures each layer (Conv2d, ReLU, etc.) gets the correct pretrained values.
# After this, your encoder is no longer randomly initialized — it’s using the pretrained VGG weights.
# self.vgg.children() → returns all layers inside the nn.Sequential VGG model.
# list(... )[:31] → keeps only the first 31 layers (up to relu4-1), since deeper layers are too abstract for style transfer.
# nn.Sequential(*...) → rebuilds a new sequential model with just those layers.
self.vgg = nn.Sequential(*list(self.vgg.children())[:31])
# But currently this will give the output after all these layers from 0 till 31 & not give me the output of intermediate layers, so for that we will separate these layers into blocks
# Deeper layers capture very high‑level semantics (e.g., “dog vs cat”), not fine visual details.
# For style transfer, we need textures, colors, and mid‑level patterns, not classification features.
# In AdaIN (and earlier NST papers), relu4-1 is chosen as the “sweet spot” for content features.
# Shallower layers (conv1_x, conv2_x, conv3_x) are still used for style statistics, but the deepest block is avoided.
# Think of VGG as a hierarchy of vision:
# Early layers → detect edges, colors, textures.
# Middle layers → detect shapes, motifs, patterns.
# Deep layers → detect objects and categories
# For style transfer, we care about textures + patterns + structure, not “object identity.” That’s why we stop at relu4-1.
# Converts the truncated VGG layers into a Python list.
# Now you can slice them into groups (blocks).
enc_layers = list(self.vgg.children())
# enc_1 (layers 0–3) → Block 1 (conv1_x): Low‑level features like edges and textures.
# enc_2 (layers 4–10) → Block 2 (conv2_x): Mid‑level features like simple patterns.
# enc_3 (layers 11–17) → Block 3 (conv3_x): Higher‑level features like shapes and object parts.
# enc_4 (layers 18–30) → Block 4 (conv4_x): Semantic features (content structure). This is the last block used in AdaIN.
self.enc_1 = nn.Sequential(*enc_layers[:4])
self.enc_2 = nn.Sequential(*enc_layers[4:11])
self.enc_3 = nn.Sequential(*enc_layers[11:18])
self.enc_4 = nn.Sequential(*enc_layers[18:31])
# Here these relu1-1 i.e 0 to 4 or relu2-1 etc are defined by authors for taking the output from these layers when we want intermediate layers output actually
# Here we are actually freezing the encoder’s parameters so they don’t get updated during training
for name in ['enc_1', 'enc_2', 'enc_3', 'enc_4']:
# getattr(self, name) → dynamically fetches each block like this (self.enc_1, self.enc_2, etc.).
# .parameters() returns all trainable tensors (weights and biases) inside that block.
for param in getattr(self, name).parameters():
param.requires_grad = False # tells PyTorch not to compute gradients for these parameters.
# Without gradients, the optimizer won’t update them during training.
# That forward method defines how data flows through our encoder when we call it.
def forward(self, input, is_test=False):
# The image tensor goes through the first block of VGG (conv1_x). Produces low‑level features (edges, textures). Stored in h1.
h1 = self.enc_1(input)
h2 = self.enc_2(h1)
h3 = self.enc_3(h2)
h4 = self.enc_4(h3)
if is_test: # i.e during testing time
# Only returns h4 (deepest features).
# Useful when you just want the final content representation (e.g., during evaluation).
return h4
# Returns all four feature maps (h1, h2, h3, h4).
# Useful during training, since style transfer often compares statistics at multiple layers (shallow for style, deep for content).
# SInce during training, we need to calculate loss & for that we need all these but during test, we do not need to calculate loss, so no need of all these & only need h4
return h1, h2, h3, h4
# Decoder :-
# It is actually the mirror image of the encoder
# What it does :-
# Takes the AdaIN‑modified features (content aligned with style statistics).
# Reconstructs them back into a full RGB image.
# Uses upsampling + convolution layers to progressively build the image.
# Why it’s trained :-
# Unlike VGG, the decoder starts untrained.
# It must learn how to invert VGG features back into pixel space while preserving the blended style.
# Training teaches it to generate visually coherent stylized images.
# Output :-
# A 3‑channel RGB image (the stylized result).
# In Decoder, instead of using MaxPool2d layer, we actually uses UpSampling layer
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.net = nn.Sequential(
# Adds a 1‑pixel border around the feature map by reflecting edge values. Prevents sharp artifacts compared to zero padding. Important in style transfer to keep smooth edges.
nn.ReflectionPad2d((1, 1, 1, 1)),
# Convolution layer: reduces channels from 512 → 256. Learns filters to start reconstructing finer details. Kernel size 3×3 captures local patterns.
nn.Conv2d(512, 256, (3, 3)),
# Non‑linear activation. Ensures the network can learn complex mappings, not just linear ones.
nn.ReLU(),
# Doubles the spatial resolution (height × width). Gradually reconstructs the image size back to original. “Nearest” means pixels are duplicated (simple but effective).
nn.Upsample(scale_factor=2, mode='nearest'),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 256, (3, 3)),
nn.ReLU(),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(256, 128, (3, 3)),
nn.ReLU(),
nn.Upsample(scale_factor=2, mode='nearest'),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(128, 128, (3, 3)),
nn.ReLU(),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(128, 64, (3, 3)),
nn.ReLU(),
nn.Upsample(scale_factor=2, mode='nearest'),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(64, 64, (3, 3)),
nn.ReLU(),
nn.ReflectionPad2d((1, 1, 1, 1)),
nn.Conv2d(64, 3, (3, 3)),
)
# Now here no need to assign weights & all because currently this is untrained decoder & we need to actually train this model
# input here is the encoded feature map (after AdaIN has blended content + style).
def forward(self, input):
# Return this output which is a 3‑channel RGB image tensor. This is the reconstructed stylized image, generated from the
return self.net(input)
# self.net(input) :- self.net is the decoder architecture you defined earlier using nn.Sequential.
# It contains all the layers: reflection padding, convolutions, ReLU activations, and upsampling.
# When you call self.net(input), PyTorch automatically passes the input through each layer in order.
|