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.