microssim 0.0.1__py3-none-any.whl

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.
microssim/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """MicroSSIM and MicroMS3IM metrics to compare images."""
2
+
3
+ from .micro_ms3im import MicroMS3IM, micro_multiscale_structural_similarity
4
+ from .micro_ssim import MicroSSIM, micro_structural_similarity
5
+
6
+ __all__ = [
7
+ "MicroSSIM",
8
+ "micro_structural_similarity",
9
+ "MicroMS3IM",
10
+ "micro_multiscale_structural_similarity",
11
+ ]
microssim/conftest.py ADDED
@@ -0,0 +1,39 @@
1
+ """File used to discover python modules and run doctest.
2
+
3
+ See https://sybil.readthedocs.io/en/latest/use.html#pytest
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ from pytest import TempPathFactory
10
+ from sybil import Sybil
11
+ from sybil.parsers.codeblock import PythonCodeBlockParser
12
+ from sybil.parsers.doctest import DocTestParser
13
+
14
+
15
+ @pytest.fixture(scope="module")
16
+ def my_path(tmpdir_factory: TempPathFactory) -> Path:
17
+ """Fixture used in doctest to create a temporary directory.
18
+
19
+ Parameters
20
+ ----------
21
+ tmpdir_factory : TempPathFactory
22
+ Temporary path factory from pytest.
23
+
24
+ Returns
25
+ -------
26
+ Path
27
+ Temporary directory path.
28
+ """
29
+ return tmpdir_factory.mktemp("my_path")
30
+
31
+
32
+ pytest_collect_file = Sybil(
33
+ parsers=[
34
+ DocTestParser(),
35
+ PythonCodeBlockParser(future_imports=["print_function"]),
36
+ ],
37
+ pattern="*.py",
38
+ fixtures=["my_path"],
39
+ ).pytest()
@@ -0,0 +1,13 @@
1
+ """Image processing utilities."""
2
+
3
+ __all__ = [
4
+ "get_background",
5
+ "remove_background",
6
+ "linearize_list",
7
+ "normalize_min_max",
8
+ "compute_norm_parameters",
9
+ ]
10
+
11
+ from .background import get_background, remove_background
12
+ from .linearize import linearize_list
13
+ from .micro_ssim_normalization import compute_norm_parameters, normalize_min_max
@@ -0,0 +1,52 @@
1
+ """Background calculation and removal functions."""
2
+
3
+ import numpy as np
4
+ from numpy.typing import NDArray
5
+
6
+
7
+ def get_background(x: NDArray, percentage: float = 3) -> float:
8
+ """
9
+ Compute the value of the background based on the percentile.
10
+
11
+ This method simply run `np.percentile` on the input image.
12
+
13
+ Parameters
14
+ ----------
15
+ x : numpy.ndarray
16
+ The input image.
17
+ percentage : float, default=3
18
+ Percentage for the percentile.
19
+
20
+ Returns
21
+ -------
22
+ float
23
+ The value of the background.
24
+ """
25
+ return np.percentile(x, percentage, keepdims=True)
26
+
27
+
28
+ def remove_background(
29
+ x: NDArray, percentage: float = 3, dtype: type = np.float32
30
+ ) -> NDArray:
31
+ """
32
+ Remove the background from an image.
33
+
34
+ Parameters
35
+ ----------
36
+ x : NDArray
37
+ The input image.
38
+ percentage : float, default=3
39
+ Percentage for the percentile.
40
+ dtype : type, default=np.float32
41
+ Data type for the output image.
42
+
43
+ Returns
44
+ -------
45
+ NDArray
46
+ The image with the background removed.
47
+ """
48
+
49
+ # get percentile
50
+ perc_val = get_background(x, percentage).astype(dtype, copy=False)
51
+
52
+ return x.astype(dtype, copy=False) - perc_val
@@ -0,0 +1,124 @@
1
+ """Normalization as done in the CSBDEEP library and CARE approach.
2
+
3
+ Code adapted from https://github.com/CSBDeep/CSBDeep under BSD-3-Clause license.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ from numpy.typing import NDArray
10
+
11
+
12
+ def normalize_care(
13
+ x: NDArray,
14
+ pmin: float = 3,
15
+ pmax: float = 99.8,
16
+ axis: Optional[int] = None,
17
+ eps: float = 1e-20,
18
+ dtype: type = np.float32,
19
+ ) -> NDArray:
20
+ """
21
+ Normalize input image using min and max percentiles.
22
+
23
+ This approach was used in the CARE method.
24
+
25
+ Parameters
26
+ ----------
27
+ x : NDArray
28
+ Input image.
29
+ pmin : float, default=3
30
+ Lower percentile.
31
+ pmax : float, default=99.8
32
+ Upper percentile.
33
+ axis : int or None, default=None
34
+ Axis or axes along which to compute the percentiles. The default `None` is to
35
+ compute the percentile(s) over a flattened version of the array.
36
+ eps : float, default=1e-20
37
+ Small value to avoid division by zero.
38
+ dtype : type, default=np.float32
39
+ Data type of the output.
40
+
41
+ Returns
42
+ -------
43
+ NDArray
44
+ Normalized image.
45
+ """
46
+ # Compute percentiles
47
+ mi = np.percentile(x, pmin, axis=axis, keepdims=True)
48
+ ma = np.percentile(x, pmax, axis=axis, keepdims=True)
49
+
50
+ return normalize_min_max(x, mi, ma, eps=eps, dtype=dtype)
51
+
52
+
53
+ def normalize_min_max(
54
+ x: NDArray,
55
+ min_val: float,
56
+ max_val: float,
57
+ eps: float = 1e-20,
58
+ dtype: type = np.float32,
59
+ ) -> NDArray:
60
+ """
61
+ Normalize input image using absolute min and max values.
62
+
63
+ This approach was used in the CARE method.
64
+
65
+ Parameters
66
+ ----------
67
+ x : NDArray
68
+ Input image.
69
+ min_val : float
70
+ Lower value.
71
+ max_val : float
72
+ Upper value.
73
+ eps : float, default=1e-20
74
+ Small value to avoid division by zero.
75
+ dtype : type, default=np.float32
76
+ Data type of the output.
77
+
78
+ Returns
79
+ -------
80
+ NDArray
81
+ Normalized image.
82
+ """
83
+ # change types
84
+ x = x.astype(dtype, copy=False)
85
+ min_val = (
86
+ dtype(min_val) if np.isscalar(min_val) else min_val.astype(dtype, copy=False)
87
+ )
88
+ max_val = (
89
+ dtype(max_val) if np.isscalar(max_val) else max_val.astype(dtype, copy=False)
90
+ )
91
+ eps = dtype(eps)
92
+
93
+ return (x - min_val) / (max_val - min_val + eps)
94
+
95
+
96
+ def normalize_min_mse(x: NDArray, target: NDArray) -> NDArray:
97
+ """
98
+ Normalize input image to a target image using minimum mean square error.
99
+
100
+ This method performs an affine rescaling of x, such that the mean squared error to
101
+ target is minimal.
102
+
103
+ Parameters
104
+ ----------
105
+ x : NDArray
106
+ Input image.
107
+ target : NDArray
108
+ Target image.
109
+
110
+ Returns
111
+ -------
112
+ NDArray
113
+ Normalized input image.
114
+ """
115
+ # compute covariance
116
+ cov = np.cov(x.flatten(), target.flatten())
117
+
118
+ # scaling factor
119
+ alpha = cov[0, 1] / (cov[0, 0] + 1e-10)
120
+
121
+ # offset
122
+ beta = target.mean() - alpha * x.mean()
123
+
124
+ return alpha * x + beta
@@ -0,0 +1,21 @@
1
+ """Linearize list of images."""
2
+
3
+ import numpy as np
4
+ from numpy.typing import NDArray
5
+
6
+
7
+ def linearize_list(images: list[NDArray]) -> NDArray:
8
+ """
9
+ Linearize and concatenate a list of images, avoiding copying the arrays.
10
+
11
+ Parameters
12
+ ----------
13
+ images : list of numpy.ndarray
14
+ Input images.
15
+
16
+ Returns
17
+ -------
18
+ np.ndarray
19
+ Lineatized and concatenated images.
20
+ """
21
+ return np.concatenate([np.ravel(x) for x in images])
@@ -0,0 +1,84 @@
1
+ """MicroSSIM normalization functions."""
2
+
3
+ from typing import Optional, Union
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+
9
+ # TODO add tests
10
+ def normalize_min_max(
11
+ images: Union[list[NDArray], NDArray], min_val: float, max_val: float
12
+ ) -> Union[list[NDArray], NDArray]:
13
+ """
14
+ Normalize the images using provided minimum and maximum values.
15
+
16
+ Parameters
17
+ ----------
18
+ images : numpy.ndarray or list of numpy.ndarray
19
+ Image or list of images to normalize.
20
+ min_val : float
21
+ Minimum value used in normalization.
22
+ max_val : float
23
+ Maximum value used in normalization.
24
+
25
+ Returns
26
+ -------
27
+ numpy.ndarray or list of numpy.ndarray
28
+ Normalized image or list of normalized images.
29
+ """
30
+ if isinstance(images, list):
31
+ return [normalize_min_max(x, min_val, max_val) for x in images]
32
+
33
+ return (images - min_val) / max_val
34
+
35
+
36
+ def compute_norm_parameters(
37
+ gt: NDArray,
38
+ pred: NDArray,
39
+ bg_percentile: float = 3.0,
40
+ offset_gt: Optional[float] = None,
41
+ offset_pred: Optional[float] = None,
42
+ max_val: Optional[float] = None,
43
+ ) -> tuple[float, float, float]:
44
+ """
45
+ Compute the parameters used to normalize the images for MicroSSIM.
46
+
47
+ If the offsets are provided, they are simply returned. Otherwise, they are
48
+ estimated from the images using the background percentile value.
49
+
50
+ If the maximum value is provided, it is simply returned. Otherwise, it is
51
+ estimated from the ground truth image by checking the maximum value after
52
+ background subtraction.
53
+
54
+ Parameters
55
+ ----------
56
+ gt : NDArray
57
+ Reference image.
58
+ pred : NDArray
59
+ Image being compared to the reference.
60
+ bg_percentile : float, default=3
61
+ Percentile of the image considered as background.
62
+ offset_gt : float or None, default=None
63
+ Estimate of background pixel intensity in the reference image.
64
+ offset_pred : float or None, default=None
65
+ Estimate of background pixel intensity in the second image.
66
+ max_val : float or None, default=None
67
+ Maximum value used in normalization.
68
+
69
+ Returns
70
+ -------
71
+ (float, float, float)
72
+ A tuple containing the ground-truth offset, the prediction offset and the
73
+ maximum value.
74
+ """
75
+ if offset_gt is None:
76
+ offset_gt = np.percentile(gt, bg_percentile, keepdims=False)
77
+
78
+ if offset_pred is None:
79
+ offset_pred = np.percentile(pred, bg_percentile, keepdims=False)
80
+
81
+ if max_val is None:
82
+ max_val = (gt - offset_gt).max()
83
+
84
+ return offset_gt, offset_pred, max_val
@@ -0,0 +1,200 @@
1
+ """MicroMS3IM metrics."""
2
+
3
+ import warnings
4
+ from typing import Optional, Union
5
+
6
+ import torch
7
+ from numpy.typing import NDArray
8
+ from torch import Tensor
9
+ from torchmetrics.image import MultiScaleStructuralSimilarityIndexMeasure
10
+
11
+ from microssim.image_processing import normalize_min_max
12
+ from microssim.micro_ssim import MicroSSIM
13
+
14
+ # TODO implement it in numpy?
15
+ # TODO return types are torch.Tensor
16
+
17
+
18
+ def micro_multiscale_structural_similarity(
19
+ gt: Union[NDArray, list[NDArray]],
20
+ pred: Union[NDArray, list[NDArray]],
21
+ *,
22
+ bg_percentile: int = 3,
23
+ offset_gt: Optional[float] = None,
24
+ offset_pred: Optional[float] = None,
25
+ max_val: Optional[float] = None,
26
+ ri_factor: Optional[float] = None,
27
+ ) -> Union[float, list[float]]:
28
+ """
29
+ Compute the mean MicroMS3IM metric between two images.
30
+
31
+ MicroSSIM computes a scaled version of the multiscale structural similarity index
32
+ (MS3IM), in which images are first normalized using an offset and maximum value. A
33
+ range-invariant factor is then estimated by maximizing a scaled MS3IM metrics
34
+ between the normalized images.
35
+
36
+ If the offsets are not provided, they are estimated from the images using the
37
+ background percentile value.
38
+
39
+ If the maximum value is not provided, it is estimated from the ground truth image
40
+ by checking the maximum value after background subtraction.
41
+
42
+ If the range-invariant factor is not provided, it is estimated from the normalized
43
+ images.
44
+
45
+ Parameters
46
+ ----------
47
+ gt : numpy.ndarray or list of numpy.ndarray
48
+ Reference image.
49
+ pred : numpy.ndarray or list of numpy.ndarray
50
+ Image being compared to the reference.
51
+ bg_percentile : int, default=3
52
+ Percentile of the image considered as background.
53
+ offset_gt : float or None, default=None
54
+ Estimate of background pixel intensity in the reference image.
55
+ offset_pred : float or None, default=None
56
+ Estimate of background pixel intensity in the second image.
57
+ max_val : float or None, default=None
58
+ Maximum value used in normalization.
59
+ ri_factor : float or None, default=None
60
+ Range-invariant factor.
61
+
62
+ Returns
63
+ -------
64
+ float or list of float
65
+ Mean MicroSSIM metric between the images, either as a list if the input are
66
+ lists or array with more than 2 dimensions.
67
+
68
+ Examples
69
+ --------
70
+ >>> import numpy as np
71
+ >>> from microssim import micro_multiscale_structural_similarity
72
+ >>> rng = np.random.default_rng(42)
73
+ >>> gt = 150 + rng.integers(0, data_range, (100, 100))
74
+ >>> pred = rng.poisson(gt) / 10. - 100
75
+ >>> micro_multiscale_structural_similarity(gt, pred)
76
+ """
77
+ # generate parameters for the metrics computation
78
+ micro_ssim = MicroMS3IM(
79
+ bg_percentile=bg_percentile,
80
+ offset_gt=offset_gt,
81
+ offset_pred=offset_pred,
82
+ max_val=max_val,
83
+ ri_factor=ri_factor,
84
+ )
85
+ micro_ssim.fit(gt, pred)
86
+
87
+ # compute the MicroMS3IM metric
88
+ if isinstance(gt, list) or gt.ndim > 2:
89
+ return [
90
+ micro_ssim.score(
91
+ gt_i,
92
+ pred_i,
93
+ )
94
+ for gt_i, pred_i in zip(gt, pred)
95
+ ]
96
+ else:
97
+ return micro_ssim.score(
98
+ gt,
99
+ pred,
100
+ )
101
+
102
+
103
+ class MicroMS3IM(MicroSSIM):
104
+ """
105
+ A class computing the MicroMS3IM metric between images.
106
+
107
+ In addition to computing the metrics, this class allows to inspect the parameters
108
+ estimated along the way, such as the offsets, the max value and the range-invariant
109
+ factor.
110
+
111
+ Attributes
112
+ ----------
113
+ _bg_percentile : int
114
+ Percentile of the image considered as background.
115
+ _offset_pred : float
116
+ Estimate of background pixel intensity in the prediction image.
117
+ _offset_gt : float
118
+ Estimate of background pixel intensity in the ground truth image.
119
+ _max_val : float
120
+ Maximum value used in normalization.
121
+ _ri_factor : float
122
+ MicroSSIM scaling factor.
123
+ _initialized : bool
124
+ Whether the class has been initialized and can compute scores between images.
125
+ """
126
+
127
+ def score(
128
+ self,
129
+ gt: NDArray,
130
+ pred: NDArray,
131
+ return_individual_components: bool = False,
132
+ **ms_ssim_kwargs,
133
+ ) -> float:
134
+ """Compute the metrics between two arrays.
135
+
136
+ Parameters
137
+ ----------
138
+ gt : numpy.ndarray
139
+ Reference array.
140
+ pred : NDArray
141
+ Array to be compared to the reference array.
142
+ return_individual_components : bool, default=False
143
+ Unused argument.
144
+ **ms_ssim_kwargs : dict
145
+ Additional keyword arguments to be passed to the
146
+ `torchmetrics.image.MultiScaleStructuralSimilarityIndexMeasure` class.
147
+
148
+ Returns
149
+ -------
150
+ float or ScaledSSIM
151
+ MicroSSIM metric between the arrays.
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ If the `fit` method has not been called before calling this method.
157
+ ValueError
158
+ If the groundtruth and prediction arrays have different shapes.
159
+ ValueError
160
+ If the arrays are not 2D.
161
+ """
162
+ if return_individual_components:
163
+ warnings.warn(
164
+ "The `return_individual_components` argument is not supported for "
165
+ "the MS-SSIM metric. Ignoring it."
166
+ )
167
+
168
+ if not self._initialized:
169
+ raise ValueError(
170
+ "MicroSSIM was not initialized, call the `fit` method first. It is "
171
+ "advised to run the `fit` method on entire datasets rather than on "
172
+ "pairs of images."
173
+ )
174
+
175
+ if gt.shape != pred.shape:
176
+ raise ValueError("Groundtruth and prediction must have the same shape.")
177
+
178
+ if gt.ndim != 2:
179
+ raise ValueError("Only 2D images are supported.")
180
+
181
+ if ms_ssim_kwargs is None:
182
+ ms_ssim_kwargs = {}
183
+
184
+ # normalize the images
185
+ gt_norm = normalize_min_max(gt, self._offset_gt, self._max_val)
186
+ pred_norm = normalize_min_max(pred, self._offset_pred, self._max_val)
187
+
188
+ # convert to torch tensors
189
+ gt_torch = torch.Tensor(gt_norm[None, None])
190
+ pred_torch = torch.Tensor(pred_norm[None, None])
191
+
192
+ # rescale
193
+ pred_torch = pred_torch * self._ri_factor
194
+
195
+ # compute the MS-SSIM
196
+ ms_ssim = MultiScaleStructuralSimilarityIndexMeasure(
197
+ data_range=gt_norm.max() - gt_norm.min(), **ms_ssim_kwargs
198
+ )
199
+
200
+ return ms_ssim(pred_torch, gt_torch)