voidspace 0.1.4__tar.gz → 0.1.5__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 (28) hide show
  1. {voidspace-0.1.4/src/voidspace.egg-info → voidspace-0.1.5}/PKG-INFO +1 -1
  2. {voidspace-0.1.4 → voidspace-0.1.5}/pyproject.toml +1 -1
  3. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/workflows.py +8 -2
  4. {voidspace-0.1.4 → voidspace-0.1.5/src/voidspace.egg-info}/PKG-INFO +1 -1
  5. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_workflows.py +41 -0
  6. {voidspace-0.1.4 → voidspace-0.1.5}/LICENSE +0 -0
  7. {voidspace-0.1.4 → voidspace-0.1.5}/README.md +0 -0
  8. {voidspace-0.1.4 → voidspace-0.1.5}/setup.cfg +0 -0
  9. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/__init__.py +0 -0
  10. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/change.py +0 -0
  11. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/cli.py +0 -0
  12. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/io.py +0 -0
  13. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/metrics.py +0 -0
  14. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/models.py +0 -0
  15. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/morphology.py +0 -0
  16. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace/segment.py +0 -0
  17. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace.egg-info/SOURCES.txt +0 -0
  18. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace.egg-info/dependency_links.txt +0 -0
  19. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace.egg-info/entry_points.txt +0 -0
  20. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace.egg-info/requires.txt +0 -0
  21. {voidspace-0.1.4 → voidspace-0.1.5}/src/voidspace.egg-info/top_level.txt +0 -0
  22. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_aim_io.py +0 -0
  23. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_change.py +0 -0
  24. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_cli.py +0 -0
  25. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_metrics.py +0 -0
  26. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_models.py +0 -0
  27. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_morphology.py +0 -0
  28. {voidspace-0.1.4 → voidspace-0.1.5}/tests/test_segment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidspace
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Spacing-aware voidspace analysis for HR-pQCT segmentations
5
5
  Author: Matthias Walle
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidspace"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Spacing-aware voidspace analysis for HR-pQCT segmentations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -69,8 +69,14 @@ def intersect_masks(
69
69
  intersection, spacing, reference = read_mask(paths[0])
70
70
  for path in paths[1:]:
71
71
  mask, mask_spacing, mask_reference = read_mask(path)
72
- if mask_spacing != spacing:
73
- raise ValueError("all masks must have the same spacing")
72
+ # AIM header quantization and NIfTI float storage can differ below 1 nm.
73
+ # Tolerate that rounding, but do not silently mix scan resolutions.
74
+ if not np.allclose(mask_spacing, spacing, rtol=0, atol=1e-6):
75
+ raise ValueError(
76
+ f"mask spacing mismatch (z, y, x; mm): {paths[0]} has {spacing}; "
77
+ f"{path} has {mask_spacing}. Expected the matching native-space "
78
+ "common region at the same scan resolution."
79
+ )
74
80
  mask = _array_on_reference_grid(mask, mask_reference, reference)
75
81
  if mask.shape != intersection.shape:
76
82
  raise ValueError("all masks must overlap the reference image space")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidspace
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Spacing-aware voidspace analysis for HR-pQCT segmentations
5
5
  Author: Matthias Walle
6
6
  License-Expression: MIT
@@ -116,6 +116,47 @@ def test_intersect_masks_writes_boolean_intersection(tmp_path):
116
116
  assert np.array_equal(result, full & common)
117
117
 
118
118
 
119
+ @pytest.mark.parametrize("shift", [0.0, 0.0607])
120
+ @pytest.mark.parametrize("source_spacing, common_spacing", [
121
+ ((0.0607,) * 3, (float(np.float32(0.0607)),) * 3),
122
+ ((0.06069900095462799, 0.06069900095462799, 0.06069599837064743),
123
+ (0.06069965288043022, 0.06069965288043022, 0.06069643050432205)),
124
+ ])
125
+ def test_intersect_masks_accepts_header_rounding_and_preserves_physical_placement(
126
+ tmp_path, shift, source_spacing, common_spacing,
127
+ ):
128
+ full = sitk.GetImageFromArray(np.ones((5, 5, 5), dtype=np.uint8))
129
+ full.SetSpacing(source_spacing)
130
+ sitk.WriteImage(full, str(tmp_path / "full.mha"))
131
+ common = sitk.GetImageFromArray(np.ones((3, 3, 3), dtype=np.uint8))
132
+ common.SetSpacing(common_spacing)
133
+ common.SetOrigin((shift,) * 3)
134
+ sitk.WriteImage(common, str(tmp_path / "common.nii.gz"))
135
+
136
+ output = intersect_masks(
137
+ [tmp_path / "full.mha", tmp_path / "common.nii.gz"],
138
+ output_path=tmp_path / "intersection.nii.gz",
139
+ )
140
+ image = sitk.ReadImage(str(output))
141
+ expected = np.zeros((5, 5, 5), dtype=bool)
142
+ start = 0 if shift == 0 else 1
143
+ expected[start:start + 3, start:start + 3, start:start + 3] = True
144
+ assert np.array_equal(sitk.GetArrayFromImage(image).astype(bool), expected)
145
+ assert np.allclose(image.GetSpacing(), source_spacing, rtol=0, atol=1e-8)
146
+
147
+
148
+ def test_intersect_masks_reports_genuinely_different_resolution(tmp_path):
149
+ for name, spacing in [("full", 0.0607), ("common", 0.082)]:
150
+ image = sitk.GetImageFromArray(np.ones((3, 3, 3), dtype=np.uint8))
151
+ image.SetSpacing((spacing,) * 3)
152
+ sitk.WriteImage(image, str(tmp_path / f"{name}.mha"))
153
+ with pytest.raises(ValueError, match="spacing.*full.*common"):
154
+ intersect_masks(
155
+ [tmp_path / "full.mha", tmp_path / "common.mha"],
156
+ output_path=tmp_path / "intersection.nii.gz",
157
+ )
158
+
159
+
119
160
  def test_intersect_masks_resamples_cropped_mask_to_reference_grid(tmp_path):
120
161
  full = np.ones((5, 5, 5), dtype=bool)
121
162
  common = np.ones((3, 3, 3), dtype=bool)
File without changes
File without changes
File without changes
File without changes
File without changes