segmodels-keras 0.1.0.dev3__tar.gz → 0.1.0.dev5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/PKG-INFO +1 -1
  2. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/README.rst +4 -1
  3. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/__init__.py +2 -2
  4. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/_version.py +1 -1
  5. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/backbones/backbones_factory.py +5 -0
  6. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/backbones/inception_resnet_v2.py +19 -9
  7. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/fpn.py +12 -1
  8. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/linknet.py +12 -1
  9. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/pspnet.py +12 -1
  10. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/unet.py +12 -1
  11. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/utils.py +32 -0
  12. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras.egg-info/PKG-INFO +1 -1
  13. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/tests/test_utils.py +40 -0
  14. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/LICENSE +0 -0
  15. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/MANIFEST.in +0 -0
  16. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/pyproject.toml +0 -0
  17. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/_compat.py +0 -0
  18. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/backbones/__init__.py +0 -0
  19. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/backbones/inception_v3.py +0 -0
  20. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/base/__init__.py +0 -0
  21. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/base/functional.py +0 -0
  22. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/base/objects.py +0 -0
  23. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/losses.py +0 -0
  24. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/metrics.py +0 -0
  25. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/__init__.py +0 -0
  26. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/_common_blocks.py +0 -0
  27. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras/models/_utils.py +0 -0
  28. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras.egg-info/SOURCES.txt +0 -0
  29. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras.egg-info/dependency_links.txt +0 -0
  30. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras.egg-info/requires.txt +0 -0
  31. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/segmodels_keras.egg-info/top_level.txt +0 -0
  32. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/setup.cfg +0 -0
  33. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/setup.py +0 -0
  34. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/tests/test_backbones.py +0 -0
  35. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/tests/test_metrics.py +0 -0
  36. {segmodels_keras-0.1.0.dev3 → segmodels_keras-0.1.0.dev5}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: segmodels_keras
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev5
4
4
  Summary: Image segmentation models with pre-trained backbones with Keras.
5
5
  Home-page: https://github.com/orthoseg/segmodels_keras
6
6
  Author: Pieter Roggemans
@@ -1,8 +1,11 @@
1
- |Actions Status| |PyPI version|
1
+ |Actions Status| |Code coverage| |PyPI version|
2
2
 
3
3
  .. |Actions Status| image:: https://github.com/orthoseg/segmodels_keras/actions/workflows/tests.yml/badge.svg?branch=main
4
4
  :target: https://github.com/orthoseg/segmodels_keras/actions/workflows/tests.yml?query=workflow%3ATests
5
5
 
6
+ .. |Code coverage| image:: https://codecov.io/gh/orthoseg/segmodels_keras/graph/badge.svg?token=o01OyTMDuj
7
+ :target: https://codecov.io/gh/orthoseg/segmodels_keras
8
+
6
9
  .. |PyPI version| image:: https://img.shields.io/pypi/v/segmodels-keras.svg
7
10
  :target: https://pypi.org/project/segmodels-keras
8
11
 
@@ -2,9 +2,9 @@ import functools # noqa: I001
2
2
  import os
3
3
 
4
4
  # __version__ should be defined as soon as possible
5
- from segmodels_keras._version import __version__
5
+ from ._version import __version__
6
6
 
7
- from segmodels_keras import base
7
+ from . import base
8
8
 
9
9
  _KERAS_FRAMEWORK_NAME = "keras"
10
10
  _TF_KERAS_FRAMEWORK_NAME = "tf.keras"
@@ -1,2 +1,2 @@
1
- __versioninfo__ = (0, 1, 0, "dev3")
1
+ __versioninfo__ = (0, 1, 0, "dev5")
2
2
  __version__ = ".".join(map(str, __versioninfo__))
@@ -274,5 +274,10 @@ class BackbonesFactory:
274
274
  def get_preprocessing(self, name):
275
275
  return self._models.get(name)[1]
276
276
 
277
+ def get_custom_objects(self, name):
278
+ if name == "inceptionresnetv2":
279
+ return irv2.get_custom_objects()
280
+ return {}
281
+
277
282
 
278
283
  Backbones = BackbonesFactory()
@@ -30,8 +30,6 @@ are the following:
30
30
  https://github.com/ayushdabra/dubai-satellite-imagery-segmentation/blob/9446e16134e752dda080ba9d65be18ea47fa26a9/get_activations.py#L40
31
31
  - The keras.applications implementation uses different imports as it is internal in
32
32
  keras, and here we need to use the public API of keras.
33
- - Keep using the Lambda layer instead of CustomScaleLayer to avoid having to pass it in
34
- with `custom_objects` when loading the model.
35
33
  """
36
34
 
37
35
  import warnings
@@ -39,7 +37,6 @@ from pathlib import Path
39
37
 
40
38
  from keras import backend, layers, models
41
39
  from keras.applications import imagenet_utils
42
- from keras.layers import Layer
43
40
  from keras.utils import get_file, get_source_inputs
44
41
 
45
42
  BASE_WEIGHT_URL = (
@@ -314,6 +311,20 @@ def conv2d_bn(
314
311
  return x
315
312
 
316
313
 
314
+ class CustomScaleLayer(layers.Layer):
315
+ def __init__(self, scale, **kwargs):
316
+ super().__init__(**kwargs)
317
+ self.scale = scale
318
+
319
+ def get_config(self):
320
+ config = super().get_config()
321
+ config.update({"scale": self.scale})
322
+ return config
323
+
324
+ def call(self, inputs):
325
+ return inputs[0] + inputs[1] * self.scale
326
+
327
+
317
328
  def inception_resnet_block(x, scale, block_type, block_idx, activation="relu"):
318
329
  """Adds a Inception-ResNet block.
319
330
 
@@ -390,12 +401,7 @@ def inception_resnet_block(x, scale, block_type, block_idx, activation="relu"):
390
401
  name=f"{block_name}_conv",
391
402
  )
392
403
 
393
- x = layers.Lambda(
394
- lambda inputs, scale: inputs[0] + inputs[1] * scale,
395
- output_shape=x.shape[1:],
396
- arguments={"scale": scale},
397
- name=block_name,
398
- )([x, up])
404
+ x = CustomScaleLayer(scale)([x, up])
399
405
 
400
406
  if activation is not None:
401
407
  x = layers.Activation(activation, name=f"{block_name}_ac")(x)
@@ -434,6 +440,10 @@ def decode_predictions(preds, top=5):
434
440
  return imagenet_utils.decode_predictions(preds, top=top)
435
441
 
436
442
 
443
+ def get_custom_objects():
444
+ return {"CustomScaleLayer": CustomScaleLayer}
445
+
446
+
437
447
  def _obtain_input_shape(
438
448
  input_shape,
439
449
  default_size,
@@ -107,6 +107,7 @@ def build_fpn(
107
107
  use_batchnorm=True,
108
108
  aggregation="sum",
109
109
  dropout=None,
110
+ weights_notop=None,
110
111
  ):
111
112
  input_ = backbone.input
112
113
  x = backbone.output
@@ -172,6 +173,12 @@ def build_fpn(
172
173
  size=(2, 2), interpolation="bilinear", name="final_upsampling"
173
174
  )(x)
174
175
 
176
+ # load weights without top if path provided
177
+ if weights_notop is not None:
178
+ model_notop = models.Model(input_, x)
179
+ model_notop.load_weights(weights_notop)
180
+ x = model_notop.output
181
+
175
182
  # model head (define number of output classes)
176
183
  x = layers.Conv2D(
177
184
  filters=classes,
@@ -200,6 +207,7 @@ def FPN(
200
207
  classes=21,
201
208
  activation="softmax",
202
209
  weights=None,
210
+ weights_notop=None,
203
211
  encoder_weights="imagenet",
204
212
  encoder_freeze=False,
205
213
  encoder_features="default",
@@ -219,7 +227,9 @@ def FPN(
219
227
  your model be able to process images af any size, but ``H`` and ``W`` of
220
228
  input images should be divisible by factor ``32``.
221
229
  classes: a number of classes for output (output shape - ``(h, w, classes)``).
222
- weights: optional, path to model weights.
230
+ weights: optional, path to model weights to be loaded.
231
+ weights_notop: optional, path to model weights without top (without segmentation
232
+ head) to be loaded.
223
233
  activation: name of one of ``keras.activations`` for last model layer
224
234
  (e.g. ``sigmoid``, ``softmax``, ``linear``).
225
235
  encoder_weights: one of ``None`` (random initialization), ``imagenet``
@@ -265,6 +275,7 @@ def FPN(
265
275
  activation=activation,
266
276
  classes=classes,
267
277
  aggregation=pyramid_aggregation,
278
+ weights_notop=weights_notop,
268
279
  )
269
280
 
270
281
  # lock encoder weights for fine-tuning
@@ -141,6 +141,7 @@ def build_linknet(
141
141
  classes=1,
142
142
  activation="sigmoid",
143
143
  use_batchnorm=True,
144
+ weights_notop=None,
144
145
  ):
145
146
  input_ = backbone.input
146
147
  x = backbone.output
@@ -169,6 +170,12 @@ def build_linknet(
169
170
  x, skip
170
171
  )
171
172
 
173
+ # load weights without top if path provided
174
+ if weights_notop is not None:
175
+ model_notop = models.Model(input_, x)
176
+ model_notop.load_weights(weights_notop)
177
+ x = model_notop.output
178
+
172
179
  # model head (define number of output classes)
173
180
  x = layers.Conv2D(
174
181
  filters=classes,
@@ -196,6 +203,7 @@ def Linknet(
196
203
  classes=1,
197
204
  activation="sigmoid",
198
205
  weights=None,
206
+ weights_notop=None,
199
207
  encoder_weights="imagenet",
200
208
  encoder_freeze=False,
201
209
  encoder_features="default",
@@ -219,7 +227,9 @@ def Linknet(
219
227
  classes: a number of classes for output (output shape - ``(h, w, classes)``).
220
228
  activation: name of one of ``keras.activations`` for last model layer
221
229
  (e.g. ``sigmoid``, ``softmax``, ``linear``).
222
- weights: optional, path to model weights.
230
+ weights: optional, path to model weights to be loaded.
231
+ weights_notop: optional, path to model weights without top (without segmentation
232
+ head) to be loaded.
223
233
  encoder_weights: one of ``None`` (random initialization), ``imagenet``
224
234
  (pre-training on ImageNet).
225
235
  encoder_freeze: if ``True`` set all layers of encoder (backbone model) as
@@ -274,6 +284,7 @@ def Linknet(
274
284
  activation=activation,
275
285
  n_upsample_blocks=len(decoder_filters),
276
286
  use_batchnorm=decoder_use_batchnorm,
287
+ weights_notop=weights_notop,
277
288
  )
278
289
 
279
290
  # lock encoder weights for fine-tuning
@@ -124,6 +124,7 @@ def build_psp(
124
124
  classes=21,
125
125
  activation="softmax",
126
126
  dropout=None,
127
+ weights_notop=None,
127
128
  ):
128
129
  input_ = backbone.input
129
130
  x = (
@@ -147,6 +148,12 @@ def build_psp(
147
148
  if dropout is not None:
148
149
  x = layers.SpatialDropout2D(dropout, name="spatial_dropout")(x)
149
150
 
151
+ # load weights without top if path provided
152
+ if weights_notop is not None:
153
+ model_notop = models.Model(input_, x)
154
+ model_notop.load_weights(weights_notop)
155
+ x = model_notop.output
156
+
150
157
  # model head
151
158
  x = layers.Conv2D(
152
159
  filters=classes,
@@ -177,6 +184,7 @@ def PSPNet(
177
184
  classes=21,
178
185
  activation="softmax",
179
186
  weights=None,
187
+ weights_notop=None,
180
188
  encoder_weights="imagenet",
181
189
  encoder_freeze=False,
182
190
  downsample_factor=8,
@@ -197,7 +205,9 @@ def PSPNet(
197
205
  classes: a number of classes for output (output shape - ``(h, w, classes)``).
198
206
  activation: name of one of ``keras.activations`` for last model layer
199
207
  (e.g. ``sigmoid``, ``softmax``, ``linear``).
200
- weights: optional, path to model weights.
208
+ weights: optional, path to model weights to be loaded.
209
+ weights_notop: optional, path to model weights without top (without segmentation
210
+ head) to be loaded.
201
211
  encoder_weights: one of ``None`` (random initialization), ``imagenet``
202
212
  (pre-training on ImageNet).
203
213
  encoder_freeze: if ``True`` set all layers of encoder (backbone model) as
@@ -250,6 +260,7 @@ def PSPNet(
250
260
  classes=classes,
251
261
  activation=activation,
252
262
  dropout=psp_dropout,
263
+ weights_notop=weights_notop,
253
264
  )
254
265
 
255
266
  # lock encoder weights for fine-tuning
@@ -113,6 +113,7 @@ def build_unet(
113
113
  classes=1,
114
114
  activation="sigmoid",
115
115
  use_batchnorm=True,
116
+ weights_notop=None,
116
117
  ):
117
118
  input_ = backbone.input
118
119
  x = backbone.output
@@ -141,6 +142,12 @@ def build_unet(
141
142
  x, skip
142
143
  )
143
144
 
145
+ # load weights without top if path provided
146
+ if weights_notop is not None:
147
+ model_notop = models.Model(input_, x)
148
+ model_notop.load_weights(weights_notop)
149
+ x = model_notop.output
150
+
144
151
  # model head (define number of output classes)
145
152
  x = layers.Conv2D(
146
153
  filters=classes,
@@ -169,6 +176,7 @@ def Unet(
169
176
  classes=1,
170
177
  activation="sigmoid",
171
178
  weights=None,
179
+ weights_notop=None,
172
180
  encoder_weights="imagenet",
173
181
  encoder_freeze=False,
174
182
  encoder_features="default",
@@ -190,7 +198,9 @@ def Unet(
190
198
  classes: a number of classes for output (output shape - ``(h, w, classes)``).
191
199
  activation: name of one of ``keras.activations`` for last model layer
192
200
  (e.g. ``sigmoid``, ``softmax``, ``linear``).
193
- weights: optional, path to model weights.
201
+ weights: optional, path to model weights to be loaded.
202
+ weights_notop: optional, path to model weights without top (without segmentation
203
+ head) to be loaded.
194
204
  encoder_weights: one of ``None`` (random initialization), ``imagenet``
195
205
  (pre-training on ImageNet).
196
206
  encoder_freeze: if ``True`` set all layers of encoder (backbone model) as
@@ -245,6 +255,7 @@ def Unet(
245
255
  activation=activation,
246
256
  n_upsample_blocks=len(decoder_filters),
247
257
  use_batchnorm=decoder_use_batchnorm,
258
+ weights_notop=weights_notop,
248
259
  )
249
260
 
250
261
  # lock encoder weights for fine-tuning
@@ -1,8 +1,40 @@
1
1
  """Utility functions for segmentation models"""
2
2
 
3
+ from pathlib import Path
4
+
3
5
  from keras import models
4
6
 
5
7
 
8
+ def save_model_weights_notop(
9
+ model: models.Model, decoder: str, path: str | Path, overwrite: bool = True
10
+ ):
11
+ """Save model weights without top (without segmentation head).
12
+
13
+ The weights saved like this can be used to preload a segmentation model for
14
+ fine-tuning by passing the path to these weights to ``weights_notop`` argument of
15
+ the model constructor, e.g. ``Unet(weights_notop="path/to/weights.h5")``.
16
+
17
+ Args:
18
+ model (``keras.models.Model``): instance of keras model
19
+ decoder: type of the decoder part of the model. Should be one of ``fpn``,
20
+ ``linknet``, ``unet``, ``pspnet``.
21
+ path (str | Path): path to save model weights
22
+ overwrite (bool): whether to overwrite existing file at ``path``.
23
+ Defaults to ``True``.
24
+
25
+ """
26
+ decoder_top_layers = {"fpn": 2, "linknet": 2, "unet": 2, "pspnet": 3}
27
+ if decoder not in decoder_top_layers:
28
+ raise ValueError(
29
+ f"Decoder should be one of {decoder_top_layers.keys()}, got {decoder}"
30
+ )
31
+
32
+ # Add 1 to the slice to actually remove the number of top layers specified
33
+ nb_top_layers = decoder_top_layers[decoder] + 1
34
+ model_notop = models.Model(model.input, model.layers[-nb_top_layers].output)
35
+ model_notop.save_weights(str(path), overwrite=overwrite)
36
+
37
+
6
38
  def set_trainable(model, recompile=True, **kwargs): # noqa: ARG001
7
39
  """Set all layers of model trainable and recompile it
8
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: segmodels_keras
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev5
4
4
  Summary: Image segmentation models with pre-trained backbones with Keras.
5
5
  Home-page: https://github.com/orthoseg/segmodels_keras
6
6
  Author: Pieter Roggemans
@@ -106,5 +106,45 @@ def test_activity_reg(case):
106
106
  _test_regularizer(model, reg_model, x, y)
107
107
 
108
108
 
109
+ @pytest.mark.parametrize("decoder", ["fpn", "linknet", "unet", "pspnet"])
110
+ def test_save_model_weights_notop(tmp_path, decoder):
111
+ backbone_name = "mobilenetv2"
112
+ if decoder == "pspnet":
113
+ model = sm.PSPNet(backbone_name, encoder_weights=None)
114
+ elif decoder == "fpn":
115
+ model = sm.FPN(backbone_name, encoder_weights=None)
116
+ elif decoder == "linknet":
117
+ model = sm.Linknet(backbone_name, encoder_weights=None)
118
+ elif decoder == "unet":
119
+ model = sm.Unet(backbone_name, encoder_weights=None)
120
+ else:
121
+ raise ValueError(f"Incorrect decoder {decoder}")
122
+
123
+ # Test saving the model weights without the top layers
124
+ path = tmp_path / f"{decoder}.weights.h5"
125
+ sm.utils.save_model_weights_notop(model, decoder=decoder, path=path)
126
+
127
+ # Test loading the model weights without the top layers again
128
+ assert path.exists()
129
+ if decoder == "pspnet":
130
+ model = sm.PSPNet(backbone_name, weights_notop=path)
131
+ elif decoder == "fpn":
132
+ model = sm.FPN(backbone_name, weights_notop=path)
133
+ elif decoder == "linknet":
134
+ model = sm.Linknet(backbone_name, weights_notop=path)
135
+ elif decoder == "unet":
136
+ model = sm.Unet(backbone_name, weights_notop=path)
137
+ else:
138
+ raise ValueError(f"Incorrect decoder {decoder}")
139
+
140
+
141
+ def test_save_model_weights_notop_invalid_decoder():
142
+ model = keras.models.Model()
143
+ with pytest.raises(ValueError, match="Decoder should be one of"):
144
+ sm.utils.save_model_weights_notop(
145
+ model, decoder="invalid_decoder", path="path/to/weights.h5"
146
+ )
147
+
148
+
109
149
  if __name__ == "__main__":
110
150
  pytest.main([__file__])