swcgeom 0.15.0__py3-none-any.whl → 0.16.0__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.

Potentially problematic release.


This version of swcgeom might be problematic. Click here for more details.

@@ -0,0 +1,107 @@
1
+ """The contrast of an image.
2
+
3
+ Notes
4
+ -----
5
+ This is expremental code, and the API is subject to change.
6
+ """
7
+
8
+ from typing import Optional, overload
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ __all__ = ["contrast_std", "contrast_michelson", "contrast_rms", "contrast_weber"]
14
+
15
+ Array3D = npt.NDArray[np.float32]
16
+
17
+
18
+ @overload
19
+ def contrast_std(image: Array3D) -> float:
20
+ """Get the std contrast of an image stack.
21
+
22
+ Parameters
23
+ ----------
24
+ imgs : ndarray
25
+
26
+ Returns
27
+ -------
28
+ contrast : float
29
+ """
30
+ ...
31
+
32
+
33
+ @overload
34
+ def contrast_std(image: Array3D, contrast: float) -> Array3D:
35
+ """Adjust the contrast of an image stack.
36
+
37
+ Parameters
38
+ ----------
39
+ imgs : ndarray
40
+ constrast : float
41
+ The contrast adjustment factor. 1.0 leaves the image unchanged.
42
+
43
+ Returns
44
+ -------
45
+ imgs : ndarray
46
+ The adjusted image.
47
+ """
48
+ ...
49
+
50
+
51
+ def contrast_std(image: Array3D, contrast: Optional[float] = None):
52
+ if contrast is None:
53
+ return np.std(image).item()
54
+ else:
55
+ return np.clip(contrast * image, 0, 1)
56
+
57
+
58
+ def contrast_michelson(image: Array3D) -> float:
59
+ """Get the Michelson contrast of an image stack.
60
+
61
+ Parameters
62
+ ----------
63
+ imgs : ndarray
64
+
65
+ Returns
66
+ -------
67
+ contrast : float
68
+ """
69
+
70
+ vmax = np.max(image)
71
+ vmin = np.min(image)
72
+ return ((vmax - vmin) / (vmax + vmin)).item()
73
+
74
+
75
+ def contrast_rms(imgs: npt.NDArray[np.float32]) -> float:
76
+ """Get the RMS contrast of an image stack.
77
+
78
+ Parameters
79
+ ----------
80
+ imgs : ndarray
81
+
82
+ Returns
83
+ -------
84
+ contrast : float
85
+ """
86
+
87
+ return np.sqrt(np.mean(imgs**2)).item()
88
+
89
+
90
+ def contrast_weber(imgs: Array3D, mask: npt.NDArray[np.bool_]) -> float:
91
+ """Get the Weber contrast of an image stack.
92
+
93
+ Parameters
94
+ ----------
95
+ imgs : ndarray
96
+ mask : ndarray of bool
97
+ The mask to segment the foreground and background. 1 for
98
+ foreground, 0 for background.
99
+
100
+ Returns
101
+ -------
102
+ contrast : float
103
+ """
104
+
105
+ l_foreground = np.mean(imgs, where=mask)
106
+ l_background = np.mean(imgs, where=np.logical_not(mask))
107
+ return ((l_foreground - l_background) / l_background).item()
swcgeom/images/folder.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """Image stack folder."""
2
2
 
3
+ import math
3
4
  import os
4
5
  import re
5
6
  import warnings
6
- from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
7
8
  from typing import (
8
9
  Callable,
9
10
  Generic,
@@ -18,21 +19,18 @@ from typing import (
18
19
 
19
20
  import numpy as np
20
21
  import numpy.typing as npt
22
+ from tqdm import tqdm
21
23
  from typing_extensions import Self
22
24
 
23
25
  from swcgeom.images.io import ScalarType, read_imgs
24
26
  from swcgeom.transforms import Identity, Transform
25
27
 
26
- __all__ = [
27
- "ImageStackFolder",
28
- "LabeledImageStackFolder",
29
- "PathImageStackFolder",
30
- ]
28
+ __all__ = ["ImageStackFolder", "LabeledImageStackFolder", "PathImageStackFolder"]
31
29
 
32
30
  T = TypeVar("T")
33
31
 
34
32
 
35
- class ImageStackFolderBase(Generic[ScalarType, T], ABC):
33
+ class ImageStackFolderBase(Generic[ScalarType, T]):
36
34
  """Image stack folder base."""
37
35
 
38
36
  files: List[str]
@@ -51,10 +49,6 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
51
49
  self.dtype = dtype or np.float32
52
50
  self.transform = transform or Identity() # type: ignore
53
51
 
54
- @abstractmethod
55
- def __getitem__(self, key: str, /) -> T:
56
- raise NotImplementedError()
57
-
58
52
  def __len__(self) -> int:
59
53
  return len(self.files)
60
54
 
@@ -78,6 +72,12 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
78
72
 
79
73
  @staticmethod
80
74
  def read_imgs(fname: str) -> npt.NDArray[np.float32]:
75
+ """Read images.
76
+
77
+ .. deprecated:: 0.16.0
78
+ Use :meth:`~swcgeom.images.io.read_imgs(fname).get_full()` instead.
79
+ """
80
+
81
81
  warnings.warn(
82
82
  "`ImageStackFolderBase.read_imgs` serves as a "
83
83
  "straightforward wrapper for `~swcgeom.images.io.read_imgs(fname).get_full()`. "
@@ -89,12 +89,67 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
89
89
  return read_imgs(fname).get_full()
90
90
 
91
91
 
92
+ @dataclass(frozen=True)
93
+ class Statistics:
94
+ count: int = 0
95
+ minimum: float = math.nan
96
+ maximum: float = math.nan
97
+ mean: float = 0
98
+ variance: float = 0
99
+
100
+
92
101
  class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
93
102
  """Image stack folder."""
94
103
 
95
104
  def __getitem__(self, idx: int, /) -> T:
96
105
  return self._get(self.files[idx])
97
106
 
107
+ def stat(self, *, transform: bool = False, verbose: bool = False) -> Statistics:
108
+ """Statistics of folder.
109
+
110
+ Parameters
111
+ ----------
112
+ transform : bool, default to False
113
+ Apply transform to the images. If True, you need to make
114
+ sure the transformed data is a ndarray.
115
+ verbose : bool, optional
116
+
117
+ Notes
118
+ -----
119
+ We are asserting that the images are of the same shape.
120
+ """
121
+
122
+ vmin, vmax = math.inf, -math.inf
123
+ n, mean, M2 = 0, None, None
124
+
125
+ for idx in tqdm(range(len(self))) if verbose else range(len(self)):
126
+ imgs = self[idx] if transform else self._read(self.files[idx])
127
+
128
+ vmin = min(vmin, np.min(imgs)) # type: ignore
129
+ vmax = max(vmax, np.max(imgs)) # type: ignore
130
+ # Welford algorithm to calculate mean and variance
131
+ if mean is None:
132
+ mean = np.zeros_like(imgs)
133
+ M2 = np.zeros_like(imgs)
134
+
135
+ n += 1
136
+ delta = imgs - mean # type: ignore
137
+ mean += delta / n
138
+ delta2 = imgs - mean
139
+ M2 += delta * delta2
140
+
141
+ if mean is None or M2 is None: # n = 0
142
+ raise ValueError("empty folder")
143
+
144
+ variance = M2 / (n - 1) if n > 1 else np.zeros_like(mean)
145
+ return Statistics(
146
+ count=len(self),
147
+ maximum=vmax,
148
+ minimum=vmin,
149
+ mean=np.mean(mean).item(),
150
+ variance=np.mean(variance).item(),
151
+ )
152
+
98
153
  @classmethod
99
154
  def from_dir(cls, root: str, *, pattern: Optional[str] = None, **kwargs) -> Self:
100
155
  """
@@ -106,6 +161,7 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
106
161
  **kwargs
107
162
  Pass to `cls.__init__`
108
163
  """
164
+
109
165
  return cls(cls.scan(root, pattern=pattern), **kwargs)
110
166
 
111
167
 
@@ -118,8 +174,8 @@ class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
118
174
  super().__init__(files, **kwargs)
119
175
  self.labels = list(labels)
120
176
 
121
- def __getitem__(self, idx: int) -> Tuple[npt.NDArray[np.float32], int]:
122
- return self.read_imgs(self.files[idx]), self.labels[idx]
177
+ def __getitem__(self, idx: int) -> Tuple[T, int]:
178
+ return self._get(self.files[idx]), self.labels[idx]
123
179
 
124
180
  @classmethod
125
181
  def from_dir(
@@ -140,7 +196,7 @@ class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
140
196
  return cls(files, labels, **kwargs)
141
197
 
142
198
 
143
- class PathImageStackFolder(ImageStackFolder[ScalarType, T]):
199
+ class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
144
200
  """Image stack folder with relpath."""
145
201
 
146
202
  root: str
@@ -164,6 +220,7 @@ class PathImageStackFolder(ImageStackFolder[ScalarType, T]):
164
220
  **kwargs
165
221
  Pass to `cls.__init__`
166
222
  """
223
+
167
224
  return cls(cls.scan(root, pattern=pattern), root=root, **kwargs)
168
225
 
169
226
 
swcgeom/images/io.py CHANGED
@@ -333,12 +333,16 @@ class V3dpbdImageStack(V3dImageStack[ScalarType]):
333
333
  class TeraflyImageStack(ImageStack[ScalarType]):
334
334
  """TeraFly image stack.
335
335
 
336
+ TeraFly is a terabytes of multidimensional volumetric images file
337
+ format as described in [1]_.
338
+
336
339
  References
337
340
  ----------
338
- [1] Bria, Alessandro, Giulio Iannello, Leonardo Onofri, and
339
- Hanchuan Peng. “TeraFly: Real-Time Three-Dimensional Visualization
340
- and Annotation of Terabytes of Multidimensional Volumetric Images.”
341
- Nature Methods 13, no. 3 (March 2016): 192-94. https://doi.org/10.1038/nmeth.3767.
341
+ .. [1] Bria, Alessandro, Giulio Iannello, Leonardo Onofri, and
342
+ Hanchuan Peng. “TeraFly: Real-Time Three-Dimensional
343
+ Visualization and Annotation of Terabytes of Multidimensional
344
+ Volumetric Images.” Nature Methods 13,
345
+ no. 3 (March 2016): 192-94. https://doi.org/10.1038/nmeth.3767.
342
346
 
343
347
  Notes
344
348
  -----
@@ -633,6 +637,12 @@ class GrayImageStack:
633
637
 
634
638
 
635
639
  def read_images(*args, **kwargs) -> GrayImageStack:
640
+ """Read images.
641
+
642
+ .. deprecated:: 0.16.0
643
+ Use :meth:`read_imgs` instead.
644
+ """
645
+
636
646
  warnings.warn(
637
647
  "`read_images` has been replaced by `read_imgs` because it"
638
648
  "provide rgb support, and this will be removed in next version",
@@ -3,9 +3,11 @@
3
3
  from swcgeom.transforms.base import *
4
4
  from swcgeom.transforms.branch import *
5
5
  from swcgeom.transforms.geometry import *
6
+ from swcgeom.transforms.image_preprocess import *
6
7
  from swcgeom.transforms.image_stack import *
7
8
  from swcgeom.transforms.images import *
8
9
  from swcgeom.transforms.mst import *
10
+ from swcgeom.transforms.neurolucida_asc import *
9
11
  from swcgeom.transforms.path import *
10
12
  from swcgeom.transforms.population import *
11
13
  from swcgeom.transforms.tree import *
@@ -0,0 +1,100 @@
1
+ """Image stack pre-processing."""
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from scipy.fftpack import fftn, fftshift, ifftn
6
+ from scipy.ndimage import gaussian_filter, minimum_filter
7
+
8
+ from swcgeom.transforms.base import Transform
9
+
10
+ __all__ = ["SGuoImPreProcess"]
11
+
12
+
13
+ class SGuoImPreProcess(Transform[npt.NDArray[np.uint8], npt.NDArray[np.uint8]]):
14
+ """Single-Neuron Image Enhancement.
15
+
16
+ Implementation of the image enhancement method described in the paper:
17
+
18
+ Shuxia Guo, Xuan Zhao, Shengdian Jiang, Liya Ding, Hanchuan Peng,
19
+ Image enhancement to leverage the 3D morphological reconstruction
20
+ of single-cell neurons, Bioinformatics, Volume 38, Issue 2,
21
+ January 2022, Pages 503–512, https://doi.org/10.1093/bioinformatics/btab638
22
+ """
23
+
24
+ def __call__(self, x: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
25
+ # TODO: support np.float32
26
+ assert x.dtype == np.uint8, "Image must be in uint8 format"
27
+ x = self.sigmoid_adjustment(x)
28
+ x = self.subtract_min_along_z(x)
29
+ x = self.bilateral_filter_3d(x)
30
+ x = self.high_pass_fft(x)
31
+ return x
32
+
33
+ @staticmethod
34
+ def sigmoid_adjustment(
35
+ image: npt.NDArray[np.uint8], sigma: float = 3, percentile: float = 25
36
+ ) -> npt.NDArray[np.uint8]:
37
+ image_normalized = image / 255.0
38
+ u = np.percentile(image_normalized, percentile)
39
+ adjusted = 1 / (1 + np.exp(-sigma * (image_normalized - u)))
40
+ adjusted_rescaled = (adjusted * 255).astype(np.uint8)
41
+ return adjusted_rescaled
42
+
43
+ @staticmethod
44
+ def subtract_min_along_z(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
45
+ min_along_z = minimum_filter(
46
+ image,
47
+ size=(1, 1, image.shape[2], 1),
48
+ mode="constant",
49
+ cval=np.max(image).item(),
50
+ )
51
+ subtracted = image - min_along_z
52
+ return subtracted
53
+
54
+ @staticmethod
55
+ def bilateral_filter_3d(
56
+ image: npt.NDArray[np.uint8], spatial_sigma=(1, 1, 0.33), range_sigma=35
57
+ ) -> npt.NDArray[np.uint8]:
58
+ # initialize the output image
59
+ filtered_image = np.zeros_like(image)
60
+
61
+ spatial_gaussian = gaussian_filter(image, spatial_sigma)
62
+
63
+ # traverse each pixel to perform bilateral filtering
64
+ # TODO: optimization is needed
65
+ for z in range(image.shape[2]):
66
+ for y in range(image.shape[1]):
67
+ for x in range(image.shape[0]):
68
+ value = image[x, y, z]
69
+ range_weight = np.exp(
70
+ -((image - value) ** 2) / (2 * range_sigma**2)
71
+ )
72
+ weights = spatial_gaussian * range_weight
73
+ filtered_image[x, y, z] = np.sum(image * weights) / np.sum(weights)
74
+
75
+ return filtered_image
76
+
77
+ @staticmethod
78
+ def high_pass_fft(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
79
+ # fft
80
+ fft_image = fftn(image)
81
+ fft_shifted = fftshift(fft_image)
82
+
83
+ # create a high-pass filter
84
+ h, w, d, _ = image.shape
85
+ x, y, z = np.ogrid[:h, :w, :d]
86
+ center = (h / 2, w / 2, d / 2)
87
+ distance = np.sqrt(
88
+ (x - center[0]) ** 2 + (y - center[1]) ** 2 + (z - center[2]) ** 2
89
+ )
90
+ # adjust this threshold to control the filtering strength
91
+ high_pass_mask = distance > (d // 4)
92
+ # apply the high-pass filter
93
+ fft_shifted *= high_pass_mask
94
+
95
+ # inverse fft
96
+ fft_unshifted = np.fft.ifftshift(fft_shifted)
97
+ filtered_image = np.real(ifftn(fft_unshifted))
98
+
99
+ filtered_rescaled = np.clip(filtered_image, 0, 255).astype(np.uint8)
100
+ return filtered_rescaled
@@ -25,6 +25,7 @@ from sdflit import (
25
25
  Scene,
26
26
  SDFObject,
27
27
  )
28
+ from tqdm import tqdm
28
29
 
29
30
  from swcgeom.core import Population, Tree
30
31
  from swcgeom.transforms.base import Transform
@@ -89,8 +90,6 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
89
90
  samplers = self._get_samplers(coord_min, coord_max)
90
91
 
91
92
  if verbose:
92
- from tqdm import tqdm
93
-
94
93
  total = (coord_max[2] - coord_min[2]) / self.resolution[2]
95
94
  samplers = tqdm(samplers, total=total.astype(np.int64).item())
96
95
 
@@ -117,8 +116,6 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
117
116
  )
118
117
 
119
118
  if verbose:
120
- from tqdm import tqdm
121
-
122
119
  trees = tqdm(trees)
123
120
 
124
121
  # TODO: multiprocess
@@ -1,6 +1,6 @@
1
1
  """Image stack related transform."""
2
2
 
3
-
3
+ import warnings
4
4
  from typing import Tuple
5
5
 
6
6
  import numpy as np
@@ -8,10 +8,20 @@ import numpy.typing as npt
8
8
 
9
9
  from swcgeom.transforms.base import Transform
10
10
 
11
- __all__ = ["Center"]
11
+ __all__ = [
12
+ "ImagesCenterCrop",
13
+ "ImagesScale",
14
+ "ImagesClip",
15
+ "ImagesNormalizer",
16
+ "ImagesMeanVarianceAdjustment",
17
+ "Center", # legacy
18
+ ]
19
+
12
20
 
21
+ NDArrayf32 = npt.NDArray[np.float32]
13
22
 
14
- class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
23
+
24
+ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
15
25
  """Get image stack center."""
16
26
 
17
27
  def __init__(self, shape_out: int | Tuple[int, int, int]):
@@ -22,7 +32,7 @@ class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
22
32
  else (shape_out, shape_out, shape_out)
23
33
  )
24
34
 
25
- def __call__(self, x: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
35
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
26
36
  diff = np.subtract(x.shape[:3], self.shape_out)
27
37
  s = diff // 2
28
38
  e = np.add(s, self.shape_out)
@@ -30,3 +40,63 @@ class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
30
40
 
31
41
  def extra_repr(self) -> str:
32
42
  return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
43
+
44
+
45
+ class Center(ImagesCenterCrop):
46
+ """Get image stack center.
47
+
48
+ .. deprecated:: 0.5.0
49
+ Use :class:`ImagesCenterCrop` instead.
50
+ """
51
+
52
+ def __init__(self, shape_out: int | Tuple[int, int, int]):
53
+ warnings.warn(
54
+ "`Center` is deprecated, use `ImagesCenterCrop` instead",
55
+ DeprecationWarning,
56
+ stacklevel=2,
57
+ )
58
+ super().__init__(shape_out)
59
+
60
+
61
+ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
62
+ def __init__(self, scaler: float) -> None:
63
+ super().__init__()
64
+ self.scaler = scaler
65
+
66
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
67
+ return self.scaler * x
68
+
69
+
70
+ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
71
+ def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
72
+ super().__init__()
73
+ self.vmin, self.vmax = vmin, vmax
74
+
75
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
76
+ return np.clip(x, self.vmin, self.vmax)
77
+
78
+
79
+ class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
80
+ """Normalize image stack."""
81
+
82
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
83
+ mean = np.mean(x)
84
+ variance = np.var(x)
85
+ return (x - mean) / variance
86
+
87
+
88
+ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
89
+ """Adjust image stack mean and variance.
90
+
91
+ See Also
92
+ --------
93
+ ~swcgeom.images.ImageStackFolder.stat
94
+ """
95
+
96
+ def __init__(self, mean: float, variance: float) -> None:
97
+ super().__init__()
98
+ self.mean = mean
99
+ self.variance = variance
100
+
101
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
102
+ return (x - self.mean) / self.variance
swcgeom/transforms/mst.py CHANGED
@@ -23,11 +23,11 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
23
23
 
24
24
  References
25
25
  ----------
26
- [1] Cuntz, H., Forstner, F., Borst, A. & Häusser, M. One Rule to
27
- Grow Them Al: A General Theory of Neuronal Branching and Its
28
- Practical Application. PLOS Comput Biol 6, e1000877 (2010).
29
- [2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
30
- dendritic structure. Theor Biol Med Model 4, 21 (2007).
26
+ .. [1] Cuntz, H., Forstner, F., Borst, A. & Häusser, M. One Rule to
27
+ Grow Them Al: A General Theory of Neuronal Branching and Its
28
+ Practical Application. PLOS Comput Biol 6, e1000877 (2010).
29
+ .. [2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
30
+ dendritic structure. Theor Biol Med Model 4, 21 (2007).
31
31
  """
32
32
 
33
33
  def __init__(