Dan Vancea commited on
Commit
b63f3bc
·
1 Parent(s): 9818215

Added architectures

Browse files
.gitignore CHANGED
@@ -1 +1,2 @@
1
- venv
 
 
1
+ venv
2
+ __pycache__
api.py CHANGED
@@ -8,7 +8,7 @@ import io
8
  import base64
9
  import logging
10
  from PIL import Image
11
- #from architectures import *
12
 
13
  # Logging configuration for observability and debugging
14
  logging.basicConfig(level=logging.INFO)
 
8
  import base64
9
  import logging
10
  from PIL import Image
11
+ from architectures import *
12
 
13
  # Logging configuration for observability and debugging
14
  logging.basicConfig(level=logging.INFO)
architectures/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from .average import Average
2
+ from .cnn import CNNUpscaler
3
+ from .reswish import Reswish
4
+ from .espcn import ESPCN
5
+ from .srgan import SRGAN
6
+ from .srrn import SRRN
7
+
architectures/average.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import keras
3
+ import tensorflow as tf
4
+ from keras import ops
5
+ from keras import layers
6
+
7
+ @keras.saving.register_keras_serializable()
8
+ class Upscaler(layers.Layer):
9
+ """Upscales images by superposing grids and averaging colors."""
10
+
11
+ def __init__(self, up_ratio: float, name="upscaler", **kwargs):
12
+ super().__init__(name=name, **kwargs)
13
+ self.up_ratio = up_ratio
14
+
15
+ def call(self, inputs):
16
+ shape = tf.shape(inputs)
17
+ height = shape[1]
18
+ width = shape[2]
19
+
20
+ # We round the new width and height casting twice (tf is weird)
21
+ new_height = tf.cast(height, tf.float32) * self.up_ratio
22
+ new_width = tf.cast(width, tf.float32) * self.up_ratio
23
+
24
+ new_height = tf.cast(new_height, tf.int32)
25
+ new_width = tf.cast(new_width, tf.int32)
26
+
27
+ # Resize
28
+ return tf.image.resize(inputs, [new_height, new_width], method='bilinear')
29
+
30
+ def get_config(self):
31
+ config = super().get_config()
32
+ config.update({"up_ratio": self.up_ratio})
33
+ return config
34
+
35
+ @keras.saving.register_keras_serializable()
36
+ class Average(keras.Model):
37
+ """Defines a model which upscales images by averaging. Training does not modify its behavior"""
38
+
39
+ def __init__(self, up_ratio=2.0, name="average", **kwargs):
40
+ super().__init__(name=name, **kwargs)
41
+ self.up_ratio = up_ratio
42
+ self.upscaler = Upscaler(up_ratio)
43
+
44
+ def call(self, inputs):
45
+ return self.upscaler(inputs)
46
+
47
+ def get_config(self):
48
+ config = super().get_config()
49
+ config.update({"up_ratio": self.up_ratio})
50
+ return config
architectures/cnn.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import keras
2
+ import tensorflow as tf
3
+ from keras import layers
4
+ from .average import Upscaler
5
+
6
+ @keras.saving.register_keras_serializable()
7
+ class CNNUpscaler(keras.Model):
8
+ """Convolutional Neural Network for image upscaling. It's just a simple model: upscales first, then applies convolution corrections."""
9
+
10
+ def __init__(self, up_ratio: float, name="cnnupscaler", **kwargs):
11
+ super().__init__(name=name, **kwargs)
12
+ self.up_ratio = up_ratio
13
+
14
+ self.upscaler = Upscaler(up_ratio)
15
+
16
+ self.conv1 = layers.Conv2D(64, (3, 3), activation="relu", padding="same")
17
+ self.conv2 = layers.Conv2D(32, (3, 3), activation="relu", padding="same")
18
+ self.conv3 = layers.Conv2D(3, (3, 3), padding="same")
19
+
20
+ def call(self, inputs):
21
+ # Upscale first
22
+ x_up = self.upscaler(inputs)
23
+
24
+ # Calculate the correction factor
25
+ x = self.conv1(x_up)
26
+ x = self.conv2(x)
27
+ correction = self.conv3(x)
28
+ return x_up + correction
29
+
30
+ def get_config(self):
31
+ config = super().get_config()
32
+ config.update({"up_ratio": self.up_ratio})
33
+ return config
architectures/espcn.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import keras
2
+ import tensorflow as tf
3
+ from keras import layers
4
+
5
+ @keras.saving.register_keras_serializable()
6
+ class ESPCN(keras.Model):
7
+ """
8
+ Efficient Sub-Pixel Convolutional Neural Network for image upscaling.
9
+ Works in LR space, uses pixel shuffle at the end for fast and stable super-resolution.
10
+ """
11
+
12
+ def __init__(self, up_ratio, name="espcn", **kwargs):
13
+ super().__init__(name=name, **kwargs)
14
+ self.up_ratio = up_ratio
15
+
16
+ self.conv1= layers.Conv2D(64, 3, padding='same', activation='relu')
17
+ self.conv2 = layers.Conv2D(64, 3, padding='same', activation='relu')
18
+ self.conv3 = layers.Conv2D(32, 3, padding='same', activation='relu')
19
+ self.conv4 = layers.Conv2D(3 * (self.up_ratio ** 2), 3, padding='same')
20
+
21
+ self.pixel_shuffle = layers.Lambda(lambda x: tf.nn.depth_to_space(x, block_size=self.up_ratio))
22
+
23
+ def call(self, inputs):
24
+ # Calculate the corrections
25
+ x = self.conv1(inputs)
26
+ x = self.conv2(x)
27
+ x = self.conv3(x)
28
+ x = self.conv4(x)
29
+
30
+ # Upscale by pixel_suffle
31
+ x = self.pixel_shuffle(x)
32
+
33
+ return tf.clip_by_value(x, 0.0, 1.0)
34
+
35
+ def get_config(self):
36
+ config = super().get_config()
37
+ config.update({"up_ratio": self.up_ratio})
38
+ return config
architectures/reswish.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import keras
3
+ from keras import layers
4
+
5
+
6
+ @keras.saving.register_keras_serializable()
7
+ class ChannelAttention(layers.Layer):
8
+ """
9
+ Channel attention https://arxiv.org/pdf/1807.02758
10
+ """
11
+
12
+ def __init__(self, channels: int, reduction: int = 16, **kwargs):
13
+ super().__init__(**kwargs)
14
+ self.channels = channels
15
+ self.reduction = reduction
16
+
17
+ # Global Average Pooling
18
+ self.gap = layers.GlobalAveragePooling2D()
19
+
20
+ # Little bit of dense
21
+ self.dense1 = layers.Dense(channels // reduction, activation="relu")
22
+ self.dense2 = layers.Dense(channels, activation="sigmoid")
23
+
24
+ # Reshape
25
+ self.reshape = layers.Reshape((1, 1, channels))
26
+
27
+ def call(self, inputs):
28
+ x = self.gap(inputs)
29
+ x = self.dense1(x)
30
+ x = self.dense2(x)
31
+ x = self.reshape(x)
32
+
33
+ return inputs * x
34
+
35
+ def get_config(self):
36
+ config = super().get_config()
37
+ config.update({"channels": self.channels, "reduction": self.reduction})
38
+ return config
39
+
40
+
41
+ @keras.saving.register_keras_serializable()
42
+ class ReswishLayer(layers.Layer):
43
+ """
44
+ A ReswishLayer is a residual block which aims to combine convolutions with trainable activation layers in order to produce complex models capable of learning different behaviors.
45
+ """
46
+
47
+ def __init__(self, filters: int, **kwargs):
48
+ """Generates the layers in a ReswishLayer.
49
+
50
+ Args:
51
+ filters (int): Amount of filters wanted after convoluting
52
+ """
53
+ super().__init__(**kwargs)
54
+
55
+ # Save stuff for config
56
+ self.filters = filters
57
+
58
+ # 2D Convolution
59
+ self.conv = layers.Conv2D(filters, (3, 3), padding="same")
60
+ # Swish it
61
+ self.sw1 = layers.Activation("swish")
62
+
63
+ # Conv again
64
+ self.conv2 = layers.Conv2D(filters, (3, 3), padding="same")
65
+
66
+ # Channel attention
67
+ self.ca = ChannelAttention(filters, reduction=16)
68
+
69
+ def call(self, inputs):
70
+ res = inputs
71
+
72
+ x = self.conv(inputs)
73
+ x = self.sw1(x)
74
+ x = self.conv2(x)
75
+ x = self.ca(x)
76
+
77
+ return layers.add([res, x])
78
+
79
+ def get_config(self):
80
+ config = super().get_config()
81
+ config.update({"filters": self.filters})
82
+ return config
83
+
84
+
85
+ @keras.saving.register_keras_serializable()
86
+ class PixelShuffleUpscale(layers.Layer):
87
+ """
88
+ Upscales by rearranging additional channels into pixels
89
+ """
90
+
91
+ def __init__(self, shuffle_num: int, filters: int, **kwargs):
92
+ super().__init__(**kwargs)
93
+ # Save stuff for config
94
+ self.filters = filters
95
+ self.shuffle_num = shuffle_num
96
+
97
+ # Actual shuffle layers
98
+ self.conv = layers.Conv2D(
99
+ filters * shuffle_num * shuffle_num, (3, 3), padding="same"
100
+ )
101
+
102
+ def call(self, inputs):
103
+ x = self.conv(inputs)
104
+ x = tf.nn.depth_to_space(x, block_size=self.shuffle_num)
105
+ return x
106
+
107
+ def get_config(self):
108
+ config = super().get_config()
109
+ config.update({"filters": self.filters, "shuffle_num": self.shuffle_num})
110
+ return config
111
+
112
+
113
+ @keras.saving.register_keras_serializable()
114
+ class ColorConstraint(layers.Layer):
115
+ """
116
+ Calculates the color consistency error and adds it to the model's loss
117
+ without modifying the image pixels directly.
118
+ """
119
+
120
+ def __init__(self, up_ratio: int, weight: float = 0.5, **kwargs):
121
+ super().__init__(**kwargs)
122
+ self.up_ratio = up_ratio
123
+ self.weight = weight
124
+
125
+ def call(self, inputs):
126
+ # inputs must be [gen, og]
127
+ gen, og = inputs
128
+
129
+ # Downscale to compare
130
+ gen_downscaled = tf.nn.avg_pool2d(
131
+ gen, ksize=self.up_ratio, strides=self.up_ratio, padding="VALID"
132
+ )
133
+
134
+ # Get error
135
+ color_error = tf.reduce_mean(tf.abs(og - gen_downscaled))
136
+
137
+ # Add loss
138
+ self.add_loss(color_error * self.weight)
139
+
140
+ return gen
141
+
142
+ def compute_output_shape(self, input_shape):
143
+ return input_shape[0]
144
+
145
+ def get_config(self):
146
+ config = super().get_config()
147
+ config.update({"up_ratio": self.up_ratio, "weight": self.weight})
148
+ return config
149
+
150
+
151
+ @keras.saving.register_keras_serializable()
152
+ class Reswish(keras.Model):
153
+ """Defines a model using ReswishLayer architecture."""
154
+
155
+ def __init__(
156
+ self,
157
+ filters: int = 64,
158
+ blocks: int = 4,
159
+ up_ratio: int = 5,
160
+ name="Reswish",
161
+ **kwargs
162
+ ):
163
+ # Save stuff for config
164
+ self.filters = filters
165
+ self.blocks = blocks
166
+ self.up_ratio = up_ratio
167
+
168
+ # Declare input (RGB images)
169
+ inputs = layers.Input(shape=(None, None, 3))
170
+
171
+ # Expand to #filters
172
+ x = layers.Conv2D(filters, (3, 3), padding="same")(inputs)
173
+
174
+ # Keep residual
175
+ res = x
176
+
177
+ # Add ReswishLayers
178
+ for _ in range(blocks):
179
+ x = ReswishLayer(filters)(x)
180
+
181
+ # Extra conv to keep it on its toes
182
+ x = layers.Conv2D(filters, (3, 3), padding="same")(x)
183
+
184
+ # Add res back
185
+ x = layers.add([x, res])
186
+
187
+ # Upscale
188
+ x = PixelShuffleUpscale(filters=filters, shuffle_num=up_ratio)(x)
189
+
190
+ # Collapse to RGB
191
+ x = layers.Conv2D(3, (3, 3), padding="same", activation=None)(x)
192
+
193
+ # Correct
194
+ outputs = ColorConstraint(up_ratio)([x, inputs])
195
+
196
+ # Create whole model
197
+ super().__init__(inputs, outputs, name=name, **kwargs)
198
+
199
+ def get_config(self):
200
+ # This allows model.save() to work
201
+ return {
202
+ "filters": self.filters,
203
+ "blocks": self.blocks,
204
+ "up_ratio": self.up_ratio,
205
+ }
architectures/srgan.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import keras
3
+ from keras import layers
4
+
5
+ # import the generator from other modules (or create a custom one)
6
+ from .srrn import SRRN
7
+
8
+ @keras.saving.register_keras_serializable()
9
+ class DiscBlock(layers.Layer):
10
+ """Basic discriminator block: Conv -> BatchNorm -> LeakyReLu"""
11
+ def __init__(self, filters, stride=1, name="DiscBlock"):
12
+ super().__init__(name=name)
13
+ self.conv = layers.Conv2D(filters, 3, strides=stride, padding="same")
14
+ self.bn = layers.BatchNormalization()
15
+ self.act = layers.LeakyReLU(0.2)
16
+
17
+ def call(self, x, training=False):
18
+ x = self.conv(x)
19
+ x = self.bn(x, training=training)
20
+ return self.act(x)
21
+
22
+ @keras.saving.register_keras_serializable()
23
+ class Discriminator(keras.Model):
24
+ """Discriminator for SRGAN"""
25
+ def __init__(self, name="discriminator", **kwargs):
26
+ super().__init__(name=name, **kwargs)
27
+
28
+ # Discriminator blocks
29
+ self.blocks = [
30
+ DiscBlock(64, stride=1),
31
+ DiscBlock(64, stride=2),
32
+
33
+ DiscBlock(128, stride=1),
34
+ DiscBlock(128, stride=2),
35
+
36
+ DiscBlock(256, stride=1),
37
+ DiscBlock(256, stride=2),
38
+
39
+ DiscBlock(512, stride=1),
40
+ DiscBlock(512, stride=2)
41
+ ]
42
+
43
+ # Final conv
44
+ self.final_conv = layers.Conv2D(1, 3, padding="same")
45
+
46
+ def call(self, inputs, training=False):
47
+ x = inputs
48
+ for block in self.blocks:
49
+ x = block(x, training=training)
50
+ x = self.final_conv(x)
51
+ return x
52
+
53
+ def get_config(self):
54
+ config = super().get_config()
55
+ return config
56
+
57
+ @keras.saving.register_keras_serializable()
58
+ class SRGAN(keras.Model):
59
+ """Super-Resolution Generator Adversarial Network. Includes the generator + discriminator + custom train step"""
60
+ def __init__(self, up_ratio, filters=64, num_blocks=8, lambda_adv=1e-3, name="srgan", **kwargs):
61
+ super().__init__(name=name, **kwargs)
62
+ self.up_ratio = up_ratio
63
+
64
+ # Generator (SRResNet)
65
+ self.generator = SRRN(up_ratio=up_ratio, filters=filters, num_blocks=num_blocks,)
66
+
67
+ # Discriminator
68
+ self.discriminator = Discriminator()
69
+
70
+ # Loss weights
71
+ self.lambda_adv = lambda_adv
72
+
73
+ # Loss functions
74
+ self.pixel_loss_fn = tf.keras.losses.MeanAbsoluteError()
75
+
76
+ # Optimizers (they are assigned in the compile fase)
77
+ self.gen_optimizer = None
78
+ self.disc_optimizer = None
79
+
80
+ def discriminator_loss(self, real_logits, fake_logits):
81
+ # Uses Least Squares instead of Binary Cross-Entropy
82
+ real_loss = tf.reduce_mean((real_logits - 1.0) ** 2)
83
+ fake_loss = tf.reduce_mean((fake_logits - 0.0) ** 2)
84
+ return real_loss + fake_loss
85
+
86
+ def _generator_adversarial_loss(self, fake_logits):
87
+ return tf.reduce_mean((fake_logits - 1.0) ** 2)
88
+
89
+ def generator_total_loss(self, sr, hr, fake_logits):
90
+ pixel_loss = self.pixel_loss_fn(hr, sr)
91
+ adv_loss = self._generator_adversarial_loss(fake_logits)
92
+ return pixel_loss + self.lambda_adv * adv_loss
93
+
94
+ def compile(self, gen_optimizer, disc_optimizer, **kwargs):
95
+ super().compile(**kwargs)
96
+
97
+ self.gen_optimizer = gen_optimizer
98
+ self.disc_optimizer = disc_optimizer
99
+
100
+ def call(self, inputs):
101
+ return self.generator(inputs)
102
+
103
+ def get_config(self):
104
+ config = super().get_config()
105
+ config.update({"up_ratio": self.up_ratio, "lambda_adv": self.lambda_adv})
106
+ return config
107
+
108
+ @tf.function
109
+ def train_step(self, data):
110
+ lr, hr = data
111
+
112
+ with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
113
+ # Generate SR image
114
+ sr = self.generator(lr, training=True)
115
+
116
+ # Discriminator predictions
117
+ real_logits = self.discriminator(hr, training=True)
118
+ fake_logits = self.discriminator(sr, training=True)
119
+
120
+ # Losses
121
+ disc_loss = self.discriminator_loss(real_logits, fake_logits)
122
+ gen_loss = self.generator_total_loss(sr, hr, fake_logits)
123
+
124
+ # Gradients
125
+ gen_grads = gen_tape.gradient(gen_loss, self.generator.trainable_variables)
126
+ disc_grads = disc_tape.gradient(disc_loss, self.discriminator.trainable_variables)
127
+
128
+ # Apply gradients
129
+ self.gen_optimizer.apply_gradients(zip(gen_grads, self.generator.trainable_variables))
130
+ self.disc_optimizer.apply_gradients(zip(disc_grads, self.discriminator.trainable_variables))
131
+
132
+ # PSNR y SSIM metri
133
+ psnr_val = tf.image.psnr(hr, sr, max_val=1.0)
134
+ ssim_val = tf.image.ssim(hr, sr, max_val=1.0)
135
+
136
+ return {
137
+ "gen_loss": gen_loss,
138
+ "disc_loss": disc_loss,
139
+ "psnr": tf.reduce_mean(psnr_val),
140
+ "ssim": tf.reduce_mean(ssim_val)
141
+ }
architectures/srrn.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import keras
2
+ import tensorflow as tf
3
+ from keras import layers
4
+
5
+ @keras.saving.register_keras_serializable()
6
+ class ResidualBlock(layers.Layer):
7
+ """Basic residual block: Conv -> ReLU"""
8
+ def __init__(self, filters, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.conv1 = layers.Conv2D(filters, 3, padding='same')
11
+ self.relu = layers.ReLU()
12
+ self.conv2 = layers.Conv2D(filters, 3, padding='same')
13
+
14
+ def call(self, x, training=False):
15
+ residual = x
16
+ x = self.conv1(x)
17
+ x = self.relu(x)
18
+ x = self.conv2(x)
19
+ return x + residual
20
+
21
+ @keras.saving.register_keras_serializable()
22
+ class SRRN(keras.Model):
23
+ """
24
+ Super-Resolution Residual Network. A more sofisticated post-upscaler Network.
25
+ Uses residual blocks to refine features in LR space, then performs upsampling via PixelShuffle.
26
+ """
27
+
28
+ def __init__(self, up_ratio, filters=64, num_blocks=8, name="SRRN", **kwargs):
29
+ super().__init__(name=name, **kwargs)
30
+ self.up_ratio = up_ratio
31
+
32
+ # Intial conv
33
+ self.conv_in = layers.Conv2D(filters, 9, padding='same')
34
+ self.relu = layers.ReLU()
35
+
36
+ # Residual blocks
37
+ self.res_blocks = [ResidualBlock(filters) for _ in range(num_blocks)]
38
+
39
+ # Post residual
40
+ self.conv_post_res = layers.Conv2D(filters, 3, padding='same')
41
+
42
+ # Upsampling
43
+ self.upsample = layers.Conv2D(filters * (up_ratio**2), 3, padding='same')
44
+ self.pixel_shuffle = layers.Lambda(lambda x: tf.nn.depth_to_space(x, up_ratio))
45
+
46
+ # Final conv
47
+ self.conv_final = layers.Conv2D(3, 9, padding='same', activation='sigmoid')
48
+
49
+ def call(self, x, training=False):
50
+ x = self.conv_in(x)
51
+ x = self.relu(x)
52
+
53
+ # Residual blocks
54
+ res = x
55
+ for block in self.res_blocks:
56
+ x = block(x, training=training)
57
+ x = self.conv_post_res(x)
58
+ x = layers.add([res, x])
59
+
60
+ # Upsampling
61
+ x = self.upsample(x)
62
+ x = self.pixel_shuffle(x)
63
+
64
+ return self.conv_final(x)
65
+
66
+ def get_config(self):
67
+ config = super().get_config()
68
+ config.update({"up_ratio": self.up_ratio})
69
+ return config