pydoug-core 0.1.0__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 (125) hide show
  1. pydoug_core-0.1.0/.gitignore +50 -0
  2. pydoug_core-0.1.0/LICENSE +7 -0
  3. pydoug_core-0.1.0/PKG-INFO +29 -0
  4. pydoug_core-0.1.0/README.md +1 -0
  5. pydoug_core-0.1.0/pyproject.toml +48 -0
  6. pydoug_core-0.1.0/src/pydoug_core/__init__.py +24 -0
  7. pydoug_core-0.1.0/src/pydoug_core/_manifest.py +2 -0
  8. pydoug_core-0.1.0/src/pydoug_core/analyze/__init__.py +0 -0
  9. pydoug_core-0.1.0/src/pydoug_core/analyze/compare.py +384 -0
  10. pydoug_core-0.1.0/src/pydoug_core/analyze/image_info.py +636 -0
  11. pydoug_core-0.1.0/src/pydoug_core/analyze/measure.py +1018 -0
  12. pydoug_core-0.1.0/src/pydoug_core/analyze/plot_distrib.py +456 -0
  13. pydoug_core-0.1.0/src/pydoug_core/analyze/plot_util.py +414 -0
  14. pydoug_core-0.1.0/src/pydoug_core/batch/__init__.py +0 -0
  15. pydoug_core-0.1.0/src/pydoug_core/batch/_result_save.py +61 -0
  16. pydoug_core-0.1.0/src/pydoug_core/batch/_tables.py +314 -0
  17. pydoug_core-0.1.0/src/pydoug_core/batch/recorder.py +545 -0
  18. pydoug_core-0.1.0/src/pydoug_core/batch/runner.py +114 -0
  19. pydoug_core-0.1.0/src/pydoug_core/color/__init__.py +0 -0
  20. pydoug_core-0.1.0/src/pydoug_core/color/cspace.py +72 -0
  21. pydoug_core-0.1.0/src/pydoug_core/detect/__init__.py +0 -0
  22. pydoug_core-0.1.0/src/pydoug_core/detect/shapes.py +482 -0
  23. pydoug_core-0.1.0/src/pydoug_core/detect/sharp.py +447 -0
  24. pydoug_core-0.1.0/src/pydoug_core/detect/texture.py +77 -0
  25. pydoug_core-0.1.0/src/pydoug_core/filter/__init__.py +0 -0
  26. pydoug_core-0.1.0/src/pydoug_core/filter/blur.py +169 -0
  27. pydoug_core-0.1.0/src/pydoug_core/filter/edge.py +259 -0
  28. pydoug_core-0.1.0/src/pydoug_core/filter/other.py +123 -0
  29. pydoug_core-0.1.0/src/pydoug_core/filter/pass_band.py +260 -0
  30. pydoug_core-0.1.0/src/pydoug_core/filter/ridge.py +240 -0
  31. pydoug_core-0.1.0/src/pydoug_core/gen/__init__.py +0 -0
  32. pydoug_core-0.1.0/src/pydoug_core/gen/gen.py +300 -0
  33. pydoug_core-0.1.0/src/pydoug_core/hist/__init__.py +0 -0
  34. pydoug_core-0.1.0/src/pydoug_core/hist/hist.py +500 -0
  35. pydoug_core-0.1.0/src/pydoug_core/io/__init__.py +0 -0
  36. pydoug_core-0.1.0/src/pydoug_core/io/imreader.py +284 -0
  37. pydoug_core-0.1.0/src/pydoug_core/io/imwriter.py +153 -0
  38. pydoug_core-0.1.0/src/pydoug_core/io/io_util.py +33 -0
  39. pydoug_core-0.1.0/src/pydoug_core/manipulate/__init__.py +0 -0
  40. pydoug_core-0.1.0/src/pydoug_core/manipulate/crop.py +101 -0
  41. pydoug_core-0.1.0/src/pydoug_core/manipulate/dims.py +394 -0
  42. pydoug_core-0.1.0/src/pydoug_core/math/__init__.py +0 -0
  43. pydoug_core-0.1.0/src/pydoug_core/math/arithmetic.py +104 -0
  44. pydoug_core-0.1.0/src/pydoug_core/math/logic.py +175 -0
  45. pydoug_core-0.1.0/src/pydoug_core/morph/__init__.py +0 -0
  46. pydoug_core-0.1.0/src/pydoug_core/morph/features.py +439 -0
  47. pydoug_core-0.1.0/src/pydoug_core/morph/morph.py +711 -0
  48. pydoug_core-0.1.0/src/pydoug_core/payloads.py +24 -0
  49. pydoug_core-0.1.0/src/pydoug_core/registry.py +323 -0
  50. pydoug_core-0.1.0/src/pydoug_core/restore/__init__.py +0 -0
  51. pydoug_core-0.1.0/src/pydoug_core/restore/denoise.py +490 -0
  52. pydoug_core-0.1.0/src/pydoug_core/restore/misc.py +172 -0
  53. pydoug_core-0.1.0/src/pydoug_core/results.py +151 -0
  54. pydoug_core-0.1.0/src/pydoug_core/segment/__init__.py +0 -0
  55. pydoug_core-0.1.0/src/pydoug_core/segment/intensity.py +257 -0
  56. pydoug_core-0.1.0/src/pydoug_core/segment/label.py +223 -0
  57. pydoug_core-0.1.0/src/pydoug_core/segment/local.py +311 -0
  58. pydoug_core-0.1.0/src/pydoug_core/segment/rgb.py +137 -0
  59. pydoug_core-0.1.0/src/pydoug_core/segment/segment_util.py +82 -0
  60. pydoug_core-0.1.0/src/pydoug_core/testing.py +91 -0
  61. pydoug_core-0.1.0/src/pydoug_core/trans/__init__.py +0 -0
  62. pydoug_core-0.1.0/src/pydoug_core/trans/geom.py +292 -0
  63. pydoug_core-0.1.0/src/pydoug_core/trans/space.py +224 -0
  64. pydoug_core-0.1.0/src/pydoug_core/user/__init__.py +0 -0
  65. pydoug_core-0.1.0/src/pydoug_core/user/user_functions.py +48 -0
  66. pydoug_core-0.1.0/src/pydoug_core/util.py +557 -0
  67. pydoug_core-0.1.0/tests/__init__.py +0 -0
  68. pydoug_core-0.1.0/tests/conftest.py +133 -0
  69. pydoug_core-0.1.0/tests/test_analyze/__init__.py +0 -0
  70. pydoug_core-0.1.0/tests/test_analyze/test_compare.py +147 -0
  71. pydoug_core-0.1.0/tests/test_analyze/test_image_info.py +268 -0
  72. pydoug_core-0.1.0/tests/test_analyze/test_measure.py +26 -0
  73. pydoug_core-0.1.0/tests/test_analyze/test_measure_bulk.py +1270 -0
  74. pydoug_core-0.1.0/tests/test_analyze/test_measure_surf.py +1767 -0
  75. pydoug_core-0.1.0/tests/test_analyze/test_plot_distrib.py +161 -0
  76. pydoug_core-0.1.0/tests/test_analyze/test_plot_util.py +121 -0
  77. pydoug_core-0.1.0/tests/test_batch/__init__.py +0 -0
  78. pydoug_core-0.1.0/tests/test_batch/test_recorder.py +332 -0
  79. pydoug_core-0.1.0/tests/test_batch/test_result_save.py +40 -0
  80. pydoug_core-0.1.0/tests/test_batch/test_runner.py +199 -0
  81. pydoug_core-0.1.0/tests/test_batch/test_tables.py +445 -0
  82. pydoug_core-0.1.0/tests/test_color/__init__.py +0 -0
  83. pydoug_core-0.1.0/tests/test_color/test_cspace.py +38 -0
  84. pydoug_core-0.1.0/tests/test_detect/__init__.py +0 -0
  85. pydoug_core-0.1.0/tests/test_detect/test_shapes.py +120 -0
  86. pydoug_core-0.1.0/tests/test_detect/test_sharp.py +117 -0
  87. pydoug_core-0.1.0/tests/test_detect/test_texture.py +27 -0
  88. pydoug_core-0.1.0/tests/test_filter/__init__.py +0 -0
  89. pydoug_core-0.1.0/tests/test_filter/test_blur.py +141 -0
  90. pydoug_core-0.1.0/tests/test_filter/test_edge.py +394 -0
  91. pydoug_core-0.1.0/tests/test_filter/test_other.py +45 -0
  92. pydoug_core-0.1.0/tests/test_filter/test_pass_band.py +71 -0
  93. pydoug_core-0.1.0/tests/test_filter/test_ridge.py +157 -0
  94. pydoug_core-0.1.0/tests/test_gen/__init__.py +0 -0
  95. pydoug_core-0.1.0/tests/test_gen/test_gen.py +147 -0
  96. pydoug_core-0.1.0/tests/test_hist/__init__.py +0 -0
  97. pydoug_core-0.1.0/tests/test_hist/test_hist.py +192 -0
  98. pydoug_core-0.1.0/tests/test_io/__init__.py +0 -0
  99. pydoug_core-0.1.0/tests/test_io/conftest.py +224 -0
  100. pydoug_core-0.1.0/tests/test_io/test_imreader.py +140 -0
  101. pydoug_core-0.1.0/tests/test_io/test_imwriter.py +233 -0
  102. pydoug_core-0.1.0/tests/test_manipulate/__init__.py +0 -0
  103. pydoug_core-0.1.0/tests/test_manipulate/test_crop.py +82 -0
  104. pydoug_core-0.1.0/tests/test_manipulate/test_dims.py +789 -0
  105. pydoug_core-0.1.0/tests/test_math/__init__.py +0 -0
  106. pydoug_core-0.1.0/tests/test_math/test_arithmetic.py +108 -0
  107. pydoug_core-0.1.0/tests/test_math/test_logic.py +157 -0
  108. pydoug_core-0.1.0/tests/test_morph/__init__.py +0 -0
  109. pydoug_core-0.1.0/tests/test_morph/test_features.py +177 -0
  110. pydoug_core-0.1.0/tests/test_morph/test_morph.py +330 -0
  111. pydoug_core-0.1.0/tests/test_payloads.py +16 -0
  112. pydoug_core-0.1.0/tests/test_registry.py +41 -0
  113. pydoug_core-0.1.0/tests/test_restore/__init__.py +0 -0
  114. pydoug_core-0.1.0/tests/test_restore/test_denoise.py +339 -0
  115. pydoug_core-0.1.0/tests/test_restore/test_misc.py +68 -0
  116. pydoug_core-0.1.0/tests/test_results.py +74 -0
  117. pydoug_core-0.1.0/tests/test_segment/__init__.py +0 -0
  118. pydoug_core-0.1.0/tests/test_segment/test_intensity.py +99 -0
  119. pydoug_core-0.1.0/tests/test_segment/test_label.py +89 -0
  120. pydoug_core-0.1.0/tests/test_segment/test_local.py +140 -0
  121. pydoug_core-0.1.0/tests/test_segment/test_rgb.py +21 -0
  122. pydoug_core-0.1.0/tests/test_trans/__init__.py +0 -0
  123. pydoug_core-0.1.0/tests/test_trans/test_geom.py +161 -0
  124. pydoug_core-0.1.0/tests/test_trans/test_space.py +60 -0
  125. pydoug_core-0.1.0/tests/test_util.py +338 -0
@@ -0,0 +1,50 @@
1
+ build/
2
+ dist/
3
+ .coverage
4
+
5
+ packages/pydoug-core/src/pydoug_core/_manifest.py
6
+ packages/pydoug-core/src/pydoug_core/__pycache__/
7
+ packages/pydoug-core/src/pydoug_core/analyze/__pycache__/
8
+ packages/pydoug-core/src/pydoug_core/batch/__pycache__/
9
+ packages/pydoug-core/src/pydoug_core/color/__pycache__/
10
+ packages/pydoug-core/src/pydoug_core/detect/__pycache__/
11
+ packages/pydoug-core/src/pydoug_core/filter/__pycache__/
12
+ packages/pydoug-core/src/pydoug_core/gen/__pycache__/
13
+ packages/pydoug-core/src/pydoug_core/hist/__pycache__/
14
+ packages/pydoug-core/src/pydoug_core/io/__pycache__/
15
+ packages/pydoug-core/src/pydoug_core/manipulate/__pycache__/
16
+ packages/pydoug-core/src/pydoug_core/math/__pycache__/
17
+ packages/pydoug-core/src/pydoug_core/morph/__pycache__/
18
+ packages/pydoug-core/src/pydoug_core/restore/__pycache__/
19
+ packages/pydoug-core/src/pydoug_core/segment/__pycache__/
20
+ packages/pydoug-core/src/pydoug_core/trans/__pycache__/
21
+ packages/pydoug-core/src/pydoug_core/user/__pycache__/
22
+ packages/pydoug-core/tests/__pycache__/
23
+ packages/pydoug-core/tests/test_analyze/__pycache__/
24
+ packages/pydoug-core/tests/test_batch/__pycache__/
25
+ packages/pydoug-core/tests/test_color/__pycache__/
26
+ packages/pydoug-core/tests/test_detect/__pycache__/
27
+ packages/pydoug-core/tests/test_filter/__pycache__/
28
+ packages/pydoug-core/tests/test_gen/__pycache__/
29
+ packages/pydoug-core/tests/test_hist/__pycache__/
30
+ packages/pydoug-core/tests/test_io/__pycache__/
31
+ packages/pydoug-core/tests/test_manipulate/__pycache__/
32
+ packages/pydoug-core/tests/test_math/__pycache__/
33
+ packages/pydoug-core/tests/test_morph/__pycache__/
34
+ packages/pydoug-core/tests/test_restore/__pycache__/
35
+ packages/pydoug-core/tests/test_segment/__pycache__/
36
+ packages/pydoug-core/tests/test_trans/__pycache__/
37
+ packages/pydoug-core/tests/test_user/__pycache__/
38
+
39
+ packages/pydoug-napari/src/pydoug_napari/__pycache__
40
+ packages/pydoug-napari/src/pydoug_napari/_operations/_manifest.py
41
+ packages/pydoug-napari/src/pydoug_napari/_operations/__pycache__/
42
+ packages/pydoug-napari/src/pydoug_napari/_widgets/__pycache__
43
+ packages/pydoug-napari/tests/__pycache__
44
+ packages/pydoug-napari/tests/test_operations/__pycache__/
45
+ packages/pydoug-napari/tests/test_widgets/__pycache__
46
+
47
+ packages/pydoug-standalone/src/pydoug_standalone/__pycache__/
48
+ packages/pydoug-standalone/src/pydoug_standalone/hooks/__pycache__/
49
+
50
+ scripts/__pycache__/
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026, Douglas Lars Nelson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.5
2
+ Name: pydoug-core
3
+ Version: 0.1.0
4
+ Summary: Core I/O, image processing and analysis, and batch logic for PyDoug
5
+ Project-URL: Homepage, https://github.com/LarsN-hub/PyDoug2
6
+ Project-URL: Issues, https://github.com/LarsN-hub/PyDoug2/issues
7
+ Author-email: Douglas Lars Nelson <PyDoug@referable.anonaddy.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: image processing,image segmentation,microscopy,scientific imaging,tomography
11
+ Requires-Python: >=3.13
12
+ Requires-Dist: algotom==1.7.0
13
+ Requires-Dist: cmasher==1.9.2
14
+ Requires-Dist: h5py==3.16.0
15
+ Requires-Dist: imageio==2.37.4
16
+ Requires-Dist: matplotlib==3.11.1
17
+ Requires-Dist: numba==0.67.0
18
+ Requires-Dist: numpy==2.5.2
19
+ Requires-Dist: openpnm==3.6.4
20
+ Requires-Dist: porespy==3.1.0
21
+ Requires-Dist: psygnal==0.15.1
22
+ Requires-Dist: scikit-image==0.26.0
23
+ Requires-Dist: scipy==1.18.0
24
+ Provides-Extra: testing
25
+ Requires-Dist: pytest-cov==7.1.0; extra == 'testing'
26
+ Requires-Dist: pytest==9.1.1; extra == 'testing'
27
+ Description-Content-Type: text/markdown
28
+
29
+ This is the core functionality subpackage for the full PyDoug image processing software package. Visit https://github.com/LarsN-hub/PyDoug2 for the full package README.md.
@@ -0,0 +1 @@
1
+ This is the core functionality subpackage for the full PyDoug image processing software package. Visit https://github.com/LarsN-hub/PyDoug2 for the full package README.md.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pydoug-core"
7
+ version = "0.1.0"
8
+ description = "Core I/O, image processing and analysis, and batch logic for PyDoug"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "Douglas Lars Nelson", email = "PyDoug@referable.anonaddy.com"}
14
+ ]
15
+ keywords = [
16
+ "image processing",
17
+ "image segmentation",
18
+ "microscopy",
19
+ "tomography",
20
+ "scientific imaging",
21
+ ]
22
+ dependencies = [
23
+ "algotom==1.7.0",
24
+ "cmasher==1.9.2",
25
+ "h5py==3.16.0",
26
+ "ImageIO==2.37.4",
27
+ "matplotlib==3.11.1",
28
+ "numba==0.67.0",
29
+ "numpy==2.5.2",
30
+ "openpnm==3.6.4",
31
+ "porespy==3.1.0",
32
+ "psygnal==0.15.1",
33
+ "scikit-image==0.26.0",
34
+ "scipy==1.18.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ testing = [
39
+ "pytest==9.1.1",
40
+ "pytest-cov==7.1.0",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/LarsN-hub/PyDoug2"
45
+ Issues = "https://github.com/LarsN-hub/PyDoug2/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/pydoug_core"]
@@ -0,0 +1,24 @@
1
+ """
2
+ Auto-imports every module in this package for discovery by widget registry.
3
+ """
4
+
5
+ import importlib
6
+ import sys
7
+
8
+ if getattr(sys, "frozen", False): # pragma: no cover
9
+ from pydoug_core._manifest import OPERATION_MODULES
10
+
11
+ for name in OPERATION_MODULES:
12
+ importlib.import_module(name)
13
+ else:
14
+ import pkgutil
15
+
16
+ _NOT_OPERATIONS: set[str] = {"pydoug_core.testing"}
17
+
18
+ for _, module_name, _ in pkgutil.walk_packages(__path__, prefix=f"{__name__}."):
19
+ if (
20
+ module_name.rsplit(".", 1)[-1].startswith("_")
21
+ or module_name in _NOT_OPERATIONS
22
+ ):
23
+ continue
24
+ importlib.import_module(module_name)
@@ -0,0 +1,2 @@
1
+ # Auto-generated by scripts/generate_manifests.py - DO NOT EDIT!
2
+ OPERATION_MODULES = ['pydoug_core.analyze', 'pydoug_core.analyze.compare', 'pydoug_core.analyze.image_info', 'pydoug_core.analyze.measure', 'pydoug_core.analyze.plot_distrib', 'pydoug_core.analyze.plot_util', 'pydoug_core.batch', 'pydoug_core.batch.recorder', 'pydoug_core.batch.runner', 'pydoug_core.color', 'pydoug_core.color.cspace', 'pydoug_core.detect', 'pydoug_core.detect.shapes', 'pydoug_core.detect.sharp', 'pydoug_core.detect.texture', 'pydoug_core.filter', 'pydoug_core.filter.blur', 'pydoug_core.filter.edge', 'pydoug_core.filter.other', 'pydoug_core.filter.pass_band', 'pydoug_core.filter.ridge', 'pydoug_core.gen', 'pydoug_core.gen.gen', 'pydoug_core.hist', 'pydoug_core.hist.hist', 'pydoug_core.io', 'pydoug_core.io.imreader', 'pydoug_core.io.imwriter', 'pydoug_core.io.io_util', 'pydoug_core.manipulate', 'pydoug_core.manipulate.crop', 'pydoug_core.manipulate.dims', 'pydoug_core.math', 'pydoug_core.math.arithmetic', 'pydoug_core.math.logic', 'pydoug_core.morph', 'pydoug_core.morph.features', 'pydoug_core.morph.morph', 'pydoug_core.payloads', 'pydoug_core.registry', 'pydoug_core.restore', 'pydoug_core.restore.denoise', 'pydoug_core.restore.misc', 'pydoug_core.results', 'pydoug_core.segment', 'pydoug_core.segment.intensity', 'pydoug_core.segment.label', 'pydoug_core.segment.local', 'pydoug_core.segment.rgb', 'pydoug_core.segment.segment_util', 'pydoug_core.trans', 'pydoug_core.trans.geom', 'pydoug_core.trans.space', 'pydoug_core.user', 'pydoug_core.user.user_functions', 'pydoug_core.util']
File without changes
@@ -0,0 +1,384 @@
1
+ """
2
+ Compare two image datasets.
3
+ """
4
+
5
+ from typing import Annotated, Literal
6
+
7
+ import numpy as np
8
+
9
+ from pydoug_core.registry import register_operation
10
+
11
+ __all__ = ["mse", "ssi", "psnr", "are", "voi", "nmi", "intersection", "hausdorff_dist"]
12
+
13
+
14
+ @register_operation(
15
+ category="compare",
16
+ display_name="Mean Squared Error",
17
+ array_params=("Layer2",),
18
+ output_kind="value",
19
+ recordable_toggle=True,
20
+ )
21
+ def mse(
22
+ imarray: np.ndarray,
23
+ Layer2: np.ndarray,
24
+ NRMSE: bool = False,
25
+ Normalization_Method: Literal["Euclidean", "Min-Max", "Mean"] = "Euclidean",
26
+ ) -> float:
27
+ """
28
+ Calculate the mean squared error (MSE) between two images. Images must have the same
29
+ dimensions but can be 2D/3D grayscale/RGB. Useful for judging intensity differences
30
+ between images. Ranges from 0 (perfectly identical images) upwards (higher = less
31
+ similar). Convenience wrapper around scikit-image's metrics.mean_squared_error and
32
+ metrics.normalized_root_mse:
33
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.mean_squared_error
34
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.normalized_root_mse
35
+
36
+ Parameters
37
+ ----------
38
+ imarray: np.ndarray
39
+ The ground-truth image.
40
+ Layer2: np.ndarray
41
+ The image to test against the ground-truth image.
42
+ NRMSE: bool
43
+ Whether to compute the normalized root MSE (True) or standard MSE (False).
44
+ Default is False.
45
+ Normalization_Method: Literal
46
+ The normalization method to use if "NRMSE" is "True". Ignored otherwise. Default
47
+ is "Euclidean".
48
+ - "Euclidean": Normalize by averaged Euclidean norm of ground-truth image.
49
+ - "Min-Max": Normalize by intensity range of ground-truth image.
50
+ - "Mean": Normalize by mean of ground-truth image.
51
+
52
+ Returns
53
+ -------
54
+ mse_val: float
55
+ The MSE between the two images.
56
+ """
57
+
58
+ if NRMSE:
59
+ from skimage.metrics import normalized_root_mse
60
+
61
+ return normalized_root_mse(
62
+ imarray, Layer2, normalization=Normalization_Method.lower()
63
+ )
64
+
65
+ from skimage.metrics import mean_squared_error
66
+
67
+ return mean_squared_error(imarray, Layer2)
68
+
69
+
70
+ @register_operation(
71
+ category="compare",
72
+ display_name="Structural Similarity",
73
+ array_params=("Layer2",),
74
+ output_kind="value",
75
+ recordable_toggle=True,
76
+ )
77
+ def ssi(
78
+ imarray: np.ndarray,
79
+ Layer2: np.ndarray,
80
+ ) -> float:
81
+ """
82
+ Calculate the structural similarity index (SSI) between two images. Images must have
83
+ the same dimensions but can be 2D/3D grayscale/RGB. Useful for judging intensity and
84
+ texture differences between images. Ranges from 1 (identical images) to 0 (perfectly
85
+ non-similar images). Convenience wrapper around scikit-image's
86
+ metrics.structural_similarity:
87
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.structural_similarity
88
+
89
+ Parameters
90
+ ----------
91
+ imarray: np.ndarray
92
+ The first image.
93
+ Layer2: np.ndarray
94
+ The second image.
95
+
96
+ Returns
97
+ -------
98
+ ssi_val: float
99
+ The SSI between the two images.
100
+ """
101
+
102
+ from skimage.metrics import structural_similarity
103
+
104
+ from pydoug_core.util import chan_axis
105
+
106
+ if imarray.dtype == np.bool:
107
+ imrange: float | int = 1
108
+ else:
109
+ imrange = np.max(imarray) - np.min(imarray)
110
+
111
+ return structural_similarity(
112
+ imarray,
113
+ Layer2,
114
+ data_range=imrange,
115
+ channel_axis=chan_axis(imarray),
116
+ ) # type: ignore
117
+
118
+
119
+ @register_operation(
120
+ category="compare",
121
+ display_name="Peak Signal to Noise Ratio",
122
+ array_params=("Layer2",),
123
+ output_kind="value",
124
+ recordable_toggle=True,
125
+ )
126
+ def psnr(
127
+ imarray: np.ndarray,
128
+ Layer2: np.ndarray,
129
+ ) -> float:
130
+ """
131
+ Calculate the peak signal to noise ratio (PSNR) between two images. Images must be
132
+ the same dimension but can be 2D/3D grayscale/RGB. Ratio of the peak signal of an
133
+ image to the corrupting noise present (higher = better, PSNR -> infinity if both
134
+ images are identical). Convenience wrapper around scikit-image's
135
+ metrics.peak_signal_noise_ratio:
136
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.peak_signal_noise_ratio
137
+
138
+ Parameters
139
+ ----------
140
+ imarray: np.ndarray
141
+ The ground-truth image.
142
+ Layer2: np.ndarray
143
+ The image to test against the ground-truth image.
144
+
145
+ Returns
146
+ -------
147
+ psnr_val: float
148
+ The PSNR between the two images.
149
+ """
150
+
151
+ from skimage.metrics import peak_signal_noise_ratio
152
+
153
+ if imarray.dtype == np.bool:
154
+ imrange: float | int = 1
155
+ else:
156
+ imrange = np.max(imarray) - np.min(imarray)
157
+
158
+ return peak_signal_noise_ratio(imarray, Layer2, data_range=imrange)
159
+
160
+
161
+ @register_operation(
162
+ category="compare",
163
+ display_name="Adapted Rand Error",
164
+ array_params=("Layer2",),
165
+ output_kind="value",
166
+ recordable_toggle=True,
167
+ )
168
+ def are(
169
+ imarray: np.ndarray,
170
+ Layer2: np.ndarray,
171
+ ) -> dict[str, float]:
172
+ """
173
+ Calculate the adapted Rand error between an image and a ground-truth image. Images
174
+ must have the same dimensions but can be 2D/3D grayscale/RGB. Useful for evaluating
175
+ segmentation similarity. Returns "Error", "Precision", and "Recall", where "Error"
176
+ indicates overall similarity (0 = perfectly similar). "Precision" is the number of
177
+ correctly segmented pixels divided by the number of segmented pixels in the test
178
+ image and "Recall" is the number of correctly segmented pixels divided by the number
179
+ of segmented pixels in the true image. If "Precision" is close to 1 but "Recall" is
180
+ lower, the image is undersegmented. If "Recall" is close to 1 but "Precision" is
181
+ lower, the image is oversegmented. Both are 1 for perfect segmentation. Non-integer
182
+ images are converted before calculation. Convenience wrapper around scikit-image's
183
+ metrics.adapted_rand_error:
184
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.adapted_rand_error
185
+
186
+ Parameters
187
+ ----------
188
+ imarray: np.ndarray
189
+ The ground truth image.
190
+ Layer2: np.ndarray
191
+ The image to test against the ground truth image.
192
+
193
+ Returns
194
+ -------
195
+ are_dict:
196
+ The adapted Rand error, precision, and recall between the two images.
197
+ """
198
+
199
+ from skimage.metrics import adapted_rand_error
200
+
201
+ are, prec, rec = adapted_rand_error(
202
+ np.round(imarray).astype(np.int32), np.round(Layer2).astype(np.int32)
203
+ )
204
+
205
+ return {"Error": are, "Precision": prec, "Recall": rec}
206
+
207
+
208
+ @register_operation(
209
+ category="compare",
210
+ display_name="Variation of Information",
211
+ array_params=("Layer2",),
212
+ output_kind="value",
213
+ recordable_toggle=True,
214
+ )
215
+ def voi(
216
+ imarray: np.ndarray,
217
+ Layer2: np.ndarray,
218
+ Rounding_Decimals: Annotated[int, {"min": 0}] = 5,
219
+ ) -> dict[str, float]:
220
+ """
221
+ Calculate the variation of information (VOI) between two images. Images must have
222
+ the same dimensions but can be 2D/3D grayscale/RGB. Useful for assessing over/under
223
+ segmentation with a ground-truth image. Returns "Splits" and "Merges" where "Splits"
224
+ indicates undersegmentation while "Merges" indicates oversegmentation. 0 = perfect
225
+ similarity while higher means less similar. Non-integer images are converted before
226
+ calculation. Convenience wrapper around scikit-image's
227
+ metrics.variation_of_information:
228
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.variation_of_information
229
+
230
+ Parameters
231
+ ----------
232
+ imarray: np.ndarray
233
+ The ground-truth image.
234
+ Layer2: np.ndarray
235
+ The image to compare to the ground-truth image.
236
+ Rounding_Decimals: int
237
+ The number of decimal points to round to (returns 0 if truly perfect instead of
238
+ just a very small number). Default is 5, increase for more decimals points.
239
+
240
+ Returns
241
+ -------
242
+ voi_vals: dict[str, float]
243
+ A dictionary containing the splits and merges from the VOI calculation.
244
+ """
245
+
246
+ from skimage.metrics import variation_of_information
247
+
248
+ splits, merges = variation_of_information(
249
+ np.round(imarray).astype(np.int32),
250
+ np.round(Layer2).astype(np.int32),
251
+ )
252
+
253
+ return {
254
+ "Splits": round(splits, Rounding_Decimals),
255
+ "Merges": round(merges, Rounding_Decimals),
256
+ }
257
+
258
+
259
+ @register_operation(
260
+ category="compare",
261
+ display_name="Normalized Mutual Information",
262
+ array_params=("Layer2",),
263
+ output_kind="value",
264
+ recordable_toggle=True,
265
+ )
266
+ def nmi(
267
+ imarray: np.ndarray,
268
+ Layer2: np.ndarray,
269
+ Bin_Count: Annotated[int, {"min": 1}] = 100,
270
+ Rounding_Decimals: Annotated[int, {"min": 0}] = 5,
271
+ ) -> float:
272
+ """
273
+ Calculate the normalized mutual information (NMI) between two images. Images must
274
+ have the same dimensions but can be 2D/3D grayscale/RGB. Useful for assessing image
275
+ registration quality (higher = better). Measured from 1 to 2 where 2 = perfect
276
+ correlation and 1 = perfect non-correlation. Convenience wrapper around
277
+ scikit-image's metrics.normalized_mutual_information:
278
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.normalized_mutual_information
279
+
280
+ Parameters
281
+ ----------
282
+ imarray: np.ndarray
283
+ The first image.
284
+ Layer2: np.ndarray
285
+ The second image.
286
+ Bin_Count: int
287
+ The number of bins to use to calculate the joint histogram.
288
+ Rounding_Decimals: int
289
+ The number of decimal points to round to (returns 0 if truly perfect instead of
290
+ just a very small number). Default is 5, increase for more decimals points.
291
+
292
+ Returns
293
+ -------
294
+ nmi_val: float
295
+ The NMI between the two images.
296
+ """
297
+
298
+ from skimage.metrics import normalized_mutual_information
299
+
300
+ return round(
301
+ normalized_mutual_information(imarray, Layer2, bins=Bin_Count), # type: ignore
302
+ Rounding_Decimals,
303
+ )
304
+
305
+
306
+ @register_operation(
307
+ category="compare",
308
+ display_name="Intersection Coefficient",
309
+ array_params=("Layer2",),
310
+ output_kind="value",
311
+ recordable_toggle=True,
312
+ )
313
+ def intersection(
314
+ imarray: np.ndarray,
315
+ Layer2: np.ndarray,
316
+ ) -> float:
317
+ """
318
+ Determine the fraction of the segmented pixels in the first image that lie within
319
+ the segmented pixels of the second image. Accepts 2D/3D grayscale/RGB images but is
320
+ only meaningful for binary segmented images. All nonzero pixels in non-binary images
321
+ are treated as segmented pixels. Convenience wrapper around numpy's boolean indexing
322
+ and count_nonzero:
323
+ https://numpy.org/doc/1.21/reference/arrays.indexing.html#boolean-array-indexing
324
+ https://numpy.org/doc/stable/reference/generated/numpy.count_nonzero.html
325
+
326
+ Parameters
327
+ ----------
328
+ imarray: np.ndarray
329
+ The first image.
330
+ Layer2: np.ndarray
331
+ The second image.
332
+
333
+ Returns
334
+ -------
335
+ inter_coeff: float
336
+ The intersection coefficient between the two images.
337
+ """
338
+
339
+ from pydoug_core.util import convert_dtype, rgb2gray
340
+
341
+ one_total: np.intp = np.count_nonzero(convert_dtype(rgb2gray(imarray), "Bool"))
342
+ one_count: np.intp = np.count_nonzero(
343
+ convert_dtype(rgb2gray(imarray), "Bool")[
344
+ convert_dtype(rgb2gray(Layer2), "Bool")
345
+ ]
346
+ )
347
+
348
+ return one_count / one_total
349
+
350
+
351
+ @register_operation(
352
+ category="compare",
353
+ display_name="Hausdorff Distance",
354
+ array_params=("Layer2",),
355
+ output_kind="value",
356
+ recordable_toggle=True,
357
+ )
358
+ def hausdorff_dist(
359
+ imarray: np.ndarray,
360
+ Layer2: np.ndarray,
361
+ ) -> float:
362
+ """
363
+ Calculate the Hausdorff distance between two images (the maximum distance between
364
+ a nonzero pixel in the first image and the nearest nonzero pixel in the second
365
+ image). Images must have the same dimensions but can be 2D/3D grayscale/RGB.
366
+ Convenience wrapper around scikit-image's metrics.hausdorff_distance:
367
+ https://scikit-image.org/docs/stable/api/skimage.metrics.html#skimage.metrics.hausdorff_distance
368
+
369
+ Parameters
370
+ ----------
371
+ imarray: np.ndarray
372
+ The first image.
373
+ Layer2: np.ndarray
374
+ The second image.
375
+
376
+ Returns
377
+ -------
378
+ hd_val: float
379
+ The Hausdorff distance between the two images.
380
+ """
381
+
382
+ from skimage.metrics import hausdorff_distance
383
+
384
+ return hausdorff_distance(imarray, Layer2) # type: ignore