segmodels-keras 0.1.0.dev8__tar.gz → 0.1.0.dev9__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 (41) hide show
  1. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/PKG-INFO +1 -1
  2. segmodels_keras-0.1.0.dev9/segmodels_keras/_compat.py +123 -0
  3. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/_version.py +1 -1
  4. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/_weights.py +2 -1
  5. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/inception_resnet_v2.py +4 -2
  6. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/inception_v3.py +4 -2
  7. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/resnet_18_34.py +35 -37
  8. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/fpn.py +3 -2
  9. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/linknet.py +3 -2
  10. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/pspnet.py +3 -2
  11. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/unet.py +4 -2
  12. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras.egg-info/PKG-INFO +1 -1
  13. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/tests/test_utils.py +84 -0
  14. segmodels_keras-0.1.0.dev8/segmodels_keras/_compat.py +0 -4
  15. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/LICENSE +0 -0
  16. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/MANIFEST.in +0 -0
  17. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/README.rst +0 -0
  18. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/pyproject.toml +0 -0
  19. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/__init__.py +0 -0
  20. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/__init__.py +0 -0
  21. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/_common_blocks.py +0 -0
  22. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/backbones_factory.py +0 -0
  23. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/backbones/resnet_common.py +0 -0
  24. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/base/__init__.py +0 -0
  25. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/base/functional.py +0 -0
  26. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/base/objects.py +0 -0
  27. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/losses.py +0 -0
  28. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/metrics.py +0 -0
  29. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/__init__.py +0 -0
  30. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/_common_blocks.py +0 -0
  31. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/models/_utils.py +0 -0
  32. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras/utils.py +0 -0
  33. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras.egg-info/SOURCES.txt +0 -0
  34. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras.egg-info/dependency_links.txt +0 -0
  35. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras.egg-info/requires.txt +0 -0
  36. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/segmodels_keras.egg-info/top_level.txt +0 -0
  37. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/setup.cfg +0 -0
  38. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/setup.py +0 -0
  39. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/tests/test_backbones.py +0 -0
  40. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/tests/test_metrics.py +0 -0
  41. {segmodels_keras-0.1.0.dev8 → segmodels_keras-0.1.0.dev9}/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.dev8
3
+ Version: 0.1.0.dev9
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
@@ -0,0 +1,123 @@
1
+ from collections import defaultdict
2
+ from pathlib import Path
3
+
4
+ import keras
5
+ from packaging.version import parse as parse_version
6
+
7
+ KERAS_GTE_3 = parse_version(keras.__version__) >= parse_version("3.0.0")
8
+
9
+
10
+ def _is_keras_v3_weights_hdf5(filepath):
11
+ try:
12
+ import h5py # noqa: PLC0415
13
+ except ImportError:
14
+ return False
15
+
16
+ try:
17
+ with h5py.File(filepath, "r") as handle:
18
+ return "layers" in handle and "vars" in handle
19
+ except OSError:
20
+ return False
21
+
22
+
23
+ def _get_sorted_hdf5_var_keys(vars_group):
24
+ return sorted(vars_group.keys(), key=int)
25
+
26
+
27
+ def _get_keras_v3_weighted_layers(handle):
28
+ import h5py # noqa: PLC0415
29
+
30
+ weighted_layers = []
31
+ for layer_name in handle["layers"].keys():
32
+ layer_group = handle["layers"][layer_name]
33
+ vars_group = layer_group.get("vars")
34
+ if vars_group is None or not vars_group.keys():
35
+ continue
36
+
37
+ signature = tuple(
38
+ tuple(vars_group[key].shape)
39
+ for key in _get_sorted_hdf5_var_keys(vars_group)
40
+ )
41
+ weighted_layers.append(
42
+ {
43
+ "name": layer_name,
44
+ "addr": h5py.h5o.get_info(layer_group.id).addr,
45
+ "signature": signature,
46
+ "vars_group": vars_group,
47
+ }
48
+ )
49
+
50
+ weighted_layers.sort(key=lambda layer: layer["addr"])
51
+ return weighted_layers
52
+
53
+
54
+ def _load_keras_v3_weights_hdf5(model, filepath):
55
+ import h5py # noqa: PLC0415
56
+
57
+ with h5py.File(filepath, "r") as handle:
58
+ saved_layers = _get_keras_v3_weighted_layers(handle)
59
+ saved_layers_by_signature = defaultdict(list)
60
+ for saved_layer in saved_layers:
61
+ saved_layers_by_signature[saved_layer["signature"]].append(saved_layer)
62
+
63
+ loaded_layers = 0
64
+
65
+ for layer in model.layers:
66
+ if not layer.weights:
67
+ continue
68
+
69
+ signature = tuple(tuple(variable.shape) for variable in layer.weights)
70
+ if not saved_layers_by_signature[signature]:
71
+ raise ValueError(
72
+ "Unable to locate matching Keras v3 HDF5 weights for layer "
73
+ f"'{layer.name}' with shapes {list(signature)}."
74
+ )
75
+
76
+ vars_group = saved_layers_by_signature[signature].pop(0)["vars_group"]
77
+ weight_values = [
78
+ vars_group[key][()] for key in _get_sorted_hdf5_var_keys(vars_group)
79
+ ]
80
+ layer.set_weights(weight_values)
81
+ loaded_layers += 1
82
+
83
+ if loaded_layers == 0:
84
+ raise ValueError(
85
+ "No matching weighted layers found in Keras v3 HDF5 weights file: "
86
+ f"{filepath}"
87
+ )
88
+
89
+
90
+ def load_weights(model, filepath):
91
+ """Load weights from an HDF5 file into a Keras model.
92
+
93
+ TensorFlow/Keras 2.10 and 2.11 can only read the legacy HDF5 format
94
+ (``layer_names`` attribute at the root). Keras 3 switched to a newer
95
+ layout where weights are stored under ``layers/<name>/vars/<index>``.
96
+ When a file saved with Keras 3 is opened by the TF 2.10/2.11 loader the
97
+ error "found 0 saved layers" is raised because the legacy reader finds no
98
+ ``layer_names`` attribute.
99
+
100
+ This wrapper calls ``model.load_weights`` first. If that raises the
101
+ "found 0 saved layers" ValueError *and* the file is detected as a Keras 3
102
+ weights HDF5 file, it falls back to a manual reader that matches each
103
+ model layer to its saved counterpart by weight-shape signature and file
104
+ creation order.
105
+
106
+ Args:
107
+ model: Keras model whose weights should be restored.
108
+ filepath (str | Path): Path to a ``.h5`` or ``.hdf5`` weights file.
109
+ """
110
+ try:
111
+ model.load_weights(filepath)
112
+ except ValueError as ex:
113
+ filepath = str(filepath)
114
+ message = str(ex)
115
+ is_hdf5 = Path(filepath).suffix in {".h5", ".hdf5"}
116
+ if (
117
+ not is_hdf5
118
+ or "found 0 saved layers" not in message
119
+ or not _is_keras_v3_weights_hdf5(filepath)
120
+ ):
121
+ raise
122
+
123
+ _load_keras_v3_weights_hdf5(model, filepath)
@@ -1,2 +1,2 @@
1
- __versioninfo__ = (0, 1, 0, "dev8")
1
+ __versioninfo__ = (0, 1, 0, "dev9")
2
2
  __version__ = ".".join(map(str, __versioninfo__))
@@ -3,6 +3,7 @@
3
3
  Source: https://github.com/qubvel/classification_models
4
4
  """
5
5
 
6
+ from segmodels_keras._compat import load_weights
6
7
  from keras.utils import get_file
7
8
 
8
9
  __all__ = ["load_model_weights"]
@@ -35,7 +36,7 @@ def load_model_weights(model, model_name, dataset, classes, include_top):
35
36
  md5_hash=weights["md5"],
36
37
  )
37
38
 
38
- model.load_weights(weights_path)
39
+ load_weights(model, weights_path)
39
40
 
40
41
  else:
41
42
  raise ValueError(
@@ -38,6 +38,8 @@ from keras import backend, layers, models
38
38
  from keras.applications import imagenet_utils
39
39
  from keras.utils import get_file, get_source_inputs
40
40
 
41
+ from segmodels_keras._compat import load_weights
42
+
41
43
  from .resnet_common import _obtain_input_shape
42
44
 
43
45
  BASE_WEIGHT_URL = (
@@ -261,9 +263,9 @@ def InceptionResNetV2(
261
263
  cache_subdir="models",
262
264
  file_hash="d19885ff4a710c122648d3b5c3b684e4",
263
265
  )
264
- model.load_weights(weights_path)
266
+ load_weights(model, weights_path)
265
267
  elif weights is not None:
266
- model.load_weights(weights)
268
+ load_weights(model, weights)
267
269
 
268
270
  return model
269
271
 
@@ -14,6 +14,8 @@ from keras import backend, layers, models
14
14
  from keras import utils as keras_utils
15
15
  from keras.applications import imagenet_utils
16
16
 
17
+ from segmodels_keras._compat import load_weights
18
+
17
19
  WEIGHTS_PATH = (
18
20
  "https://github.com/fchollet/deep-learning-models/"
19
21
  "releases/download/v0.5/"
@@ -361,9 +363,9 @@ def InceptionV3(
361
363
  cache_subdir="models",
362
364
  file_hash="bcbd6486424b2319ff4ef7d526e38f63",
363
365
  )
364
- model.load_weights(weights_path)
366
+ load_weights(model, weights_path)
365
367
  elif weights is not None:
366
- model.load_weights(weights)
368
+ load_weights(model, weights)
367
369
 
368
370
  return model
369
371
 
@@ -14,27 +14,22 @@ closely so that the two can be maintained together:
14
14
  Layer naming follows the same ``conv{N}_block{M}_*`` convention used by
15
15
  keras.applications, so models built here share the same weight-loading patterns.
16
16
 
17
- Note that the resulting resnet18 and resnet34 are not exactly the same as the version
18
- in https://github.com/qubvel/classification_models and hence is also different than
19
- the segmentation models in https://github.com/qubvel/segmentation_models.
20
- There are some differences in the bottom and top layers of the resnet, e.g. because
21
- the classification_models version applies also post-activation after the last residual
22
- block.
23
-
24
- This implementation is designed to be compatible with torchvision ResNet18/34 pretrained
25
- weights. Because the resnet implementation in keras.applications.resnet.ResNet (which
26
- only supports ResNet50+) already uses the same bottom and top layers this implementation
27
- is based on it, but in addition the following tweaks have been applied:
28
- - BatchNormalization uses ``momentum=0.9`` and ``epsilon=1e-5`` (torchvision defaults)
29
- instead of the keras.applications defaults of ``momentum=0.99`` and
30
- ``epsilon=1.001e-5``.
31
- Note that momentum is only used during training. In addition, the value in pytorch
32
- is inverted compared to the tf.keras implementation, so the default in pytorch
33
- is actually ``momentum=0.1``, but after inversion we use ``momentum=0.9`` here.
34
- - The single bottom convolutional layer gets ``use_bias=False`` (like all convolutional
35
- layers in the model just like the version in torchvision and in segmentation_models.
36
- In the keras.applications ResNet(50+) implementation ``use_bias=True`` is used for the
37
- single bottom convolutional layer.
17
+ Notes:
18
+ - The resulting resnet18 and resnet34 are not exactly the same as the version in
19
+ https://github.com/qubvel/classification_models and hence is also different than
20
+ the segmentation models in https://github.com/qubvel/segmentation_models.
21
+ There are some differences in the bottom and top layers of the resnet, e.g. because
22
+ the classification_models version applies also post-activation after the last
23
+ residual block.
24
+ - The implementation is also ~ compatible with torchvision ResNet18/34 pretrained
25
+ weights based on a (rough) test. There are some small differences, but they don't
26
+ seem to influance the results significantly:
27
+ - BatchNormalization in torchvision uses ``momentum=0.9`` and ``epsilon=1e-5``
28
+ instead of the keras.applications defaults of ``momentum=0.99`` and
29
+ ``epsilon=1.001e-5`` used here.
30
+ Note that momentum is only used during training. In addition, the value in
31
+ pytorch is inverted compared to the tf.keras implementation, so the default in
32
+ pytorch is actually ``momentum=0.1``, but inverted this gives ``momentum=0.9``.
38
33
  """
39
34
 
40
35
  import os
@@ -43,8 +38,13 @@ from keras import backend, layers, models
43
38
  from keras.applications import imagenet_utils
44
39
  from keras.utils import get_source_inputs
45
40
 
41
+ from segmodels_keras._compat import load_weights
42
+
46
43
  from .resnet_common import _obtain_input_shape
47
44
 
45
+ momentum = 0.99
46
+ epsilon = 1.001e-5
47
+
48
48
 
49
49
  def ResNet(
50
50
  stack_fn,
@@ -141,7 +141,7 @@ def ResNet(
141
141
  # The batchnormalization parameters have been changed to be compatible with the
142
142
  # torchvision ResNet18/34 pretrained weights.
143
143
  x = layers.BatchNormalization(
144
- axis=bn_axis, momentum=0.1, epsilon=1e-5, name="conv1_bn"
144
+ axis=bn_axis, momentum=momentum, epsilon=epsilon, name="conv1_bn"
145
145
  )(x)
146
146
  x = layers.Activation("relu", name="conv1_relu")(x)
147
147
 
@@ -154,7 +154,7 @@ def ResNet(
154
154
  # The batchnormalization parameters have been changed to be compatible with the
155
155
  # torchvision ResNet18/34 pretrained weights.
156
156
  x = layers.BatchNormalization(
157
- axis=bn_axis, momentum=0.1, epsilon=1e-5, name="post_bn"
157
+ axis=bn_axis, momentum=momentum, epsilon=epsilon, name="post_bn"
158
158
  )(x)
159
159
  x = layers.Activation("relu", name="post_relu")(x)
160
160
 
@@ -184,7 +184,7 @@ def ResNet(
184
184
  model = models.Model(inputs, x, name=name)
185
185
 
186
186
  if weights is not None and os.path.exists(weights):
187
- model.load_weights(weights)
187
+ load_weights(model, weights)
188
188
 
189
189
  return model
190
190
 
@@ -226,7 +226,7 @@ def residual_basicblock(
226
226
  filters, 1, strides=stride, use_bias=False, name=f"{name}_0_conv"
227
227
  )(x)
228
228
  shortcut = layers.BatchNormalization(
229
- axis=bn_axis, momentum=0.1, epsilon=1e-5, name=f"{name}_0_bn"
229
+ axis=bn_axis, momentum=momentum, epsilon=epsilon, name=f"{name}_0_bn"
230
230
  )(shortcut)
231
231
  else:
232
232
  shortcut = x
@@ -236,14 +236,14 @@ def residual_basicblock(
236
236
  filters, kernel_size, strides=stride, use_bias=False, name=f"{name}_1_conv"
237
237
  )(x)
238
238
  x = layers.BatchNormalization(
239
- axis=bn_axis, momentum=0.1, epsilon=1e-5, name=f"{name}_1_bn"
239
+ axis=bn_axis, momentum=momentum, epsilon=epsilon, name=f"{name}_1_bn"
240
240
  )(x)
241
241
  x = layers.Activation("relu", name=f"{name}_1_relu")(x)
242
242
 
243
243
  x = layers.ZeroPadding2D(padding=((1, 1), (1, 1)), name=f"{name}_2_pad")(x)
244
244
  x = layers.Conv2D(filters, kernel_size, use_bias=False, name=f"{name}_2_conv")(x)
245
245
  x = layers.BatchNormalization(
246
- axis=bn_axis, momentum=0.1, epsilon=1e-5, name=f"{name}_2_bn"
246
+ axis=bn_axis, momentum=momentum, epsilon=epsilon, name=f"{name}_2_bn"
247
247
  )(x)
248
248
 
249
249
  x = layers.Add(name=f"{name}_add")([shortcut, x])
@@ -354,15 +354,13 @@ def ResNet34(
354
354
  )
355
355
 
356
356
 
357
- def preprocess_input(x, **kwargs):
358
- """Torchvision-compatible ImageNet normalization (input in [0, 1])."""
359
- import numpy as np
357
+ def preprocess_input(x, data_format=None):
358
+ """Preprocesses a numpy array encoding a batch of images.
360
359
 
361
- # FLAIR-compatible normalization (input in [0, 255]).
362
- mean = np.array([105.08, 110.87, 101.82, 106.38, 53.26], dtype="float32")
363
- std = np.array([52.17, 45.38, 44, 39.69, 79.3], dtype="float32")
360
+ Args:
361
+ x: a 4D numpy array consists of RGB values within [0, 255].
364
362
 
365
- # Torchvision-compatible ImageNet normalization (input in [0, 1]).
366
- # mean = np.array([0.485, 0.456, 0.406], dtype="float32")
367
- # std = np.array([0.229, 0.224, 0.225], dtype="float32")
368
- return (x - mean) / std
363
+ Returns:
364
+ Preprocessed array.
365
+ """
366
+ return imagenet_utils.preprocess_input(x, data_format=data_format, mode="tf")
@@ -1,6 +1,7 @@
1
1
  from keras import backend, layers, models
2
2
  from keras import utils as keras_utils
3
3
 
4
+ from .._compat import load_weights
4
5
  from ..backbones.backbones_factory import Backbones
5
6
  from ._common_blocks import Conv2dBn
6
7
  from ._utils import freeze_model
@@ -177,7 +178,7 @@ def build_fpn(
177
178
  # load weights without top if path provided
178
179
  if weights_notop is not None:
179
180
  model_notop = models.Model(input_, x)
180
- model_notop.load_weights(weights_notop)
181
+ load_weights(model_notop, weights_notop)
181
182
  if freeze_notop:
182
183
  freeze_model(model_notop)
183
184
  x = model_notop.output
@@ -291,6 +292,6 @@ def FPN(
291
292
 
292
293
  # loading model weights
293
294
  if weights is not None:
294
- model.load_weights(weights)
295
+ load_weights(model, weights)
295
296
 
296
297
  return model
@@ -1,6 +1,7 @@
1
1
  from keras import backend, layers, models
2
2
  from keras import utils as keras_utils
3
3
 
4
+ from .._compat import load_weights
4
5
  from ..backbones.backbones_factory import Backbones
5
6
  from ._common_blocks import Conv2dBn
6
7
  from ._utils import freeze_model
@@ -174,7 +175,7 @@ def build_linknet(
174
175
  # load weights without top if path provided
175
176
  if weights_notop is not None:
176
177
  model_notop = models.Model(input_, x)
177
- model_notop.load_weights(weights_notop)
178
+ load_weights(model_notop, weights_notop)
178
179
  if freeze_notop:
179
180
  freeze_model(model_notop)
180
181
  x = model_notop.output
@@ -300,6 +301,6 @@ def Linknet(
300
301
 
301
302
  # loading model weights
302
303
  if weights is not None:
303
- model.load_weights(weights)
304
+ load_weights(model, weights)
304
305
 
305
306
  return model
@@ -1,6 +1,7 @@
1
1
  from keras import backend, layers, models
2
2
  from keras import utils as keras_utils
3
3
 
4
+ from .._compat import load_weights
4
5
  from ..backbones.backbones_factory import Backbones
5
6
  from ._common_blocks import Conv2dBn
6
7
  from ._utils import freeze_model
@@ -152,7 +153,7 @@ def build_psp(
152
153
  # load weights without top if path provided
153
154
  if weights_notop is not None:
154
155
  model_notop = models.Model(input_, x)
155
- model_notop.load_weights(weights_notop)
156
+ load_weights(model_notop, weights_notop)
156
157
  if freeze_notop:
157
158
  freeze_model(model_notop)
158
159
  x = model_notop.output
@@ -276,6 +277,6 @@ def PSPNet(
276
277
 
277
278
  # loading model weights
278
279
  if weights is not None:
279
- model.load_weights(weights)
280
+ load_weights(model, weights)
280
281
 
281
282
  return model
@@ -1,6 +1,7 @@
1
1
  from keras import backend, layers, models
2
2
  from keras import utils as keras_utils
3
3
 
4
+ from .._compat import load_weights
4
5
  from ..backbones.backbones_factory import Backbones
5
6
  from ._common_blocks import Conv2dBn
6
7
  from ._utils import freeze_model
@@ -146,7 +147,8 @@ def build_unet(
146
147
  # load weights without top if path provided
147
148
  if weights_notop is not None:
148
149
  model_notop = models.Model(input_, x)
149
- model_notop.load_weights(weights_notop)
150
+ load_weights(model_notop, weights_notop)
151
+
150
152
  if freeze_notop:
151
153
  freeze_model(model_notop)
152
154
  x = model_notop.output
@@ -271,6 +273,6 @@ def Unet(
271
273
 
272
274
  # loading model weights
273
275
  if weights is not None:
274
- model.load_weights(weights)
276
+ load_weights(model, weights)
275
277
 
276
278
  return model
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: segmodels_keras
3
- Version: 0.1.0.dev8
3
+ Version: 0.1.0.dev9
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,10 @@
1
+ import h5py
1
2
  import numpy as np
2
3
  import pytest
3
4
 
4
5
  import segmodels_keras as sm
5
6
  from segmodels_keras import Unet
7
+ from segmodels_keras._compat import _load_keras_v3_weights_hdf5
6
8
  from segmodels_keras.utils import set_regularization
7
9
 
8
10
  if sm.framework() == sm._TF_KERAS_FRAMEWORK_NAME:
@@ -146,5 +148,87 @@ def test_save_model_weights_notop_invalid_decoder():
146
148
  )
147
149
 
148
150
 
151
+ def test_load_keras_v3_style_weights_hdf5(tmp_path):
152
+ """Normal case: distinct weight-shape signatures per layer."""
153
+ inputs = keras.layers.Input(shape=(4,), name="input")
154
+ x = keras.layers.Dense(3, use_bias=True, name="dense")(inputs)
155
+ x = keras.layers.BatchNormalization(scale=False, name="batch_norm")(x)
156
+ outputs = keras.layers.Dense(2, use_bias=False, name="head")(x)
157
+ model = keras.models.Model(inputs, outputs, name="functional")
158
+
159
+ expected_weights = [weight.copy() for weight in model.get_weights()]
160
+ path = tmp_path / "keras_v3_style.weights.h5"
161
+
162
+ with h5py.File(path, "w") as handle:
163
+ layers_group = handle.create_group("layers")
164
+ handle.create_group("vars").attrs["name"] = model.name
165
+
166
+ for layer in model.layers:
167
+ layer_group = layers_group.create_group(layer.name)
168
+ vars_group = layer_group.create_group("vars")
169
+ for index, value in enumerate(layer.get_weights()):
170
+ vars_group.create_dataset(str(index), data=value)
171
+
172
+ zero_weights = [np.zeros_like(weight) for weight in expected_weights]
173
+ model.set_weights(zero_weights)
174
+
175
+ _load_keras_v3_weights_hdf5(model, path)
176
+
177
+ for actual, expected in zip(model.get_weights(), expected_weights, strict=True):
178
+ np.testing.assert_allclose(actual, expected)
179
+
180
+
181
+ def test_load_keras_v3_style_weights_hdf5_duplicate_signatures(tmp_path):
182
+ """Layers with identical weight-shape signatures are matched in save order.
183
+
184
+ This gave an error for Keras v3 HDF5 weight files of an inceptionresnetv2 backbone.
185
+ """
186
+ # All three Dense layers share the same shape signature: [(4, 3)].
187
+ # The fallback must pop each candidate in file-write order so that
188
+ # branch_a, branch_b, and branch_c each receive their own distinct values.
189
+ inputs = keras.layers.Input(shape=(4,), name="input")
190
+ branch_a = keras.layers.Dense(3, use_bias=False, name="branch_a")(inputs)
191
+ branch_b = keras.layers.Dense(3, use_bias=False, name="branch_b")(inputs)
192
+ branch_c = keras.layers.Dense(3, use_bias=False, name="branch_c")(inputs)
193
+ concat_axis = 1
194
+ outputs = keras.layers.Concatenate(axis=concat_axis, name="concat")(
195
+ [branch_a, branch_b, branch_c]
196
+ )
197
+ model = keras.models.Model(inputs, outputs, name="functional")
198
+
199
+ # Assign distinct sentinel values to each dense layer so we can verify
200
+ # that the right values end up in the right layer after loading.
201
+ dense_layers = [model.get_layer(n) for n in ("branch_a", "branch_b", "branch_c")]
202
+ original_values = []
203
+ for i, layer in enumerate(dense_layers):
204
+ sentinel = np.full((4, 3), fill_value=float(i + 1), dtype=np.float32)
205
+ layer.set_weights([sentinel])
206
+ original_values.append(sentinel.copy())
207
+
208
+ path = tmp_path / "dup_signatures.weights.h5"
209
+ with h5py.File(path, "w") as handle:
210
+ layers_group = handle.create_group("layers")
211
+ handle.create_group("vars").attrs["name"] = model.name
212
+ for layer in model.layers:
213
+ layer_group = layers_group.create_group(layer.name)
214
+ vars_group = layer_group.create_group("vars")
215
+ for index, value in enumerate(layer.get_weights()):
216
+ vars_group.create_dataset(str(index), data=value)
217
+
218
+ # Zero out all weights before loading.
219
+ for layer in dense_layers:
220
+ layer.set_weights([np.zeros((4, 3), dtype=np.float32)])
221
+
222
+ _load_keras_v3_weights_hdf5(model, path)
223
+
224
+ for layer, expected in zip(dense_layers, original_values):
225
+ actual = layer.get_weights()[0]
226
+ np.testing.assert_allclose(
227
+ actual,
228
+ expected,
229
+ err_msg=f"Layer '{layer.name}' got wrong weights after loading.",
230
+ )
231
+
232
+
149
233
  if __name__ == "__main__":
150
234
  pytest.main([__file__])
@@ -1,4 +0,0 @@
1
- import keras
2
- from packaging.version import parse as parse_version
3
-
4
- KERAS_GTE_3 = parse_version(keras.__version__) >= parse_version("3.0.0")