neuralib-imaging 0.7.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.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, YT.WEI
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: neuralib-imaging
3
+ Version: 0.7.0
4
+ Summary: Tools for cellular imaging and widefield imaging
5
+ Author-email: Yu-Ting Wei <ytsimon2004@gmail.com>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/ytsimon2004/neuralib2
8
+ Project-URL: Documentation, https://neuralib2.readthedocs.io/en/latest/index.html
9
+ Project-URL: Repository, https://github.com/ytsimon2004/neuralib2
10
+ Project-URL: Issues, https://github.com/ytsimon2004/neuralib2/issues
11
+ Keywords: Parser,Segmentation,Morphology
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <3.13,>=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: neuralib-utils[all]>=0.7.0
24
+ Requires-Dist: numpy
25
+ Requires-Dist: numba
26
+ Requires-Dist: matplotlib
27
+ Provides-Extra: cascade
28
+ Requires-Dist: ruamel.yaml; extra == "cascade"
29
+ Requires-Dist: tensorflow; extra == "cascade"
30
+ Provides-Extra: widefield
31
+ Requires-Dist: napari; extra == "widefield"
32
+ Requires-Dist: bokeh; extra == "widefield"
33
+ Requires-Dist: scikit-learn; extra == "widefield"
34
+ Provides-Extra: all
35
+ Requires-Dist: neuralib-imaging[cascade]; extra == "all"
36
+ Requires-Dist: neuralib-imaging[widefield]; extra == "all"
37
+ Provides-Extra: test
38
+ Requires-Dist: neuralib-imaging[all]; extra == "test"
39
+ Requires-Dist: neuralib-parser[all]; extra == "test"
40
+ Dynamic: license-file
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "neuralib-imaging"
3
+ version = "0.7.0"
4
+ requires-python = ">=3.11,<3.13"
5
+ description = "Tools for cellular imaging and widefield imaging"
6
+ authors = [
7
+ { name = "Yu-Ting Wei", email = "ytsimon2004@gmail.com" },
8
+ ]
9
+ license = { text = "BSD-3-Clause" }
10
+ readme = "README.md"
11
+ keywords = ["Parser", "Segmentation", "Morphology"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "License :: OSI Approved :: BSD License",
16
+ "Natural Language :: English",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering",
19
+ 'Intended Audience :: Science/Research',
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ dependencies = [
24
+ "neuralib-utils[all]>=0.7.0",
25
+ "numpy",
26
+ "numba",
27
+ "matplotlib",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+
32
+ cascade = ["ruamel.yaml", "tensorflow"]
33
+ widefield = ["napari", "bokeh", "scikit-learn"]
34
+
35
+ all = ["neuralib-imaging[cascade]", "neuralib-imaging[widefield]"]
36
+
37
+ test = ["neuralib-imaging[all]", "neuralib-parser[all]"]
38
+
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/ytsimon2004/neuralib2"
42
+ Documentation = "https://neuralib2.readthedocs.io/en/latest/index.html"
43
+ Repository = "https://github.com/ytsimon2004/neuralib2"
44
+ Issues = "https://github.com/ytsimon2004/neuralib2/issues"
45
+
46
+ [build-system]
47
+ build-backend = "setuptools.build_meta"
48
+ requires = ["setuptools>=61.0"]
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from .coordinates import *
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ import attrs
4
+ import numpy as np
5
+ from matplotlib.patches import Polygon
6
+ from typing import Sequence, Literal, Self
7
+
8
+ from neuralib.util.unstable import unstable
9
+
10
+ __all__ = ['get_field_of_view',
11
+ 'get_cellular_coordinate',
12
+ 'FieldOfView',
13
+ 'CellularCoordinates']
14
+
15
+ UNIT = Literal['mm', 'um']
16
+
17
+
18
+ def get_field_of_view(am: Sequence[float],
19
+ pm: Sequence[float],
20
+ pl: Sequence[float],
21
+ al: Sequence[float],
22
+ *,
23
+ rotation_ml: float = 0,
24
+ rotation_ap: float = 0,
25
+ unit: UNIT = 'mm',
26
+ perpendicular: bool = True,
27
+ region_name: str | None = None) -> FieldOfView:
28
+ """
29
+ Construct a :class:`~FieldOfView` from four corner coordinates.
30
+
31
+ :param am: anteromedial corner coordinate [x, y] in mm or µm.
32
+ :param pm: posteromedial corner coordinate [x, y] in mm or µm.
33
+ :param pl: posterolateral corner coordinate [x, y] in mm or µm.
34
+ :param al: anterolateral corner coordinate [x, y] in mm or µm.
35
+ :param rotation_ml: in-plane rotation (CCW) around the ML axis, in degrees.
36
+ :param rotation_ap: tilt around the AP axis (foreshortening), in degrees.
37
+ :param unit: unit of the corners value
38
+ :param perpendicular: imaging objective is perpendicular to the cranial windows. if True, skip rotation and tilt transforms.
39
+ :param region_name: optional identifier for this FOV region.
40
+ :returns: FieldOfView instance with corners stacked and optionally transformed.
41
+ """
42
+ corners = np.vstack([am, pm, pl, al])
43
+ return FieldOfView(corners, rotation_ml, rotation_ap, unit,
44
+ perpendicular=perpendicular,
45
+ region_name=region_name)
46
+
47
+
48
+ def _validator_corners(instance, attribute, value: np.ndarray):
49
+ if not isinstance(value, np.ndarray):
50
+ raise TypeError(f'{attribute} should be a numpy array')
51
+ if value.shape != (4, 2):
52
+ raise ValueError('')
53
+
54
+
55
+ @unstable()
56
+ def _rotate_and_tilt(pts: np.ndarray, ml: float, ap: float) -> np.ndarray:
57
+ """telecentric / orthographic imaging transformation"""
58
+ pivot = pts.mean(axis=0)
59
+ theta_x = np.deg2rad(ml)
60
+ c, s = np.cos(theta_x), np.sin(theta_x)
61
+ R = np.array([[c, -s], [s, c]])
62
+ pts0 = (pts - pivot) @ R.T + pivot
63
+ theta_y = np.deg2rad(ap)
64
+ scale_x = np.cos(theta_y)
65
+ pts1 = (pts0 - pivot) * np.array([scale_x, 1.0]) + pivot
66
+
67
+ return pts1
68
+
69
+
70
+ @attrs.frozen
71
+ class FieldOfView:
72
+ """Field‑Of‑View defined by its four XY corners"""
73
+
74
+ corners: np.ndarray = attrs.field(validator=_validator_corners)
75
+ """corner coordinates in order [AM, PM, PL, AL]. `Array[float, [4, 2]]`"""
76
+ rotation_ml: float = attrs.field(default=0.0)
77
+ """in-plane rotation (CCW) around the ML axis, in degrees"""
78
+ rotation_ap: float = attrs.field(default=0.0)
79
+ """tilt around the AP axis (foreshortening), in degrees"""
80
+ unit: UNIT = attrs.field(default='mm', validator=attrs.validators.in_(['mm', 'um']))
81
+ """unit of the corners value"""
82
+ perpendicular: bool = attrs.field(default=True, kw_only=True)
83
+ """imaging objective is perpendicular to the cranial windows. if True, skip rotation and tilt transforms"""
84
+ region_name: str | None = attrs.field(default=None, kw_only=True)
85
+ """optional identifier for this FOV region"""
86
+
87
+ def __attrs_post_init__(self):
88
+ if not self.perpendicular:
89
+ new_corners = _rotate_and_tilt(
90
+ self.corners,
91
+ self.rotation_ml,
92
+ self.rotation_ap,
93
+ )
94
+ object.__setattr__(self, 'corners', new_corners)
95
+
96
+ @property
97
+ def am(self) -> np.ndarray:
98
+ """anteromedial corner coordinate [x, y] in mm or µm."""
99
+ return self.corners[0]
100
+
101
+ @property
102
+ def pm(self) -> np.ndarray:
103
+ """posteromedial corner coordinate [x, y] in mm or µm"""
104
+ return self.corners[1]
105
+
106
+ @property
107
+ def pl(self) -> np.ndarray:
108
+ """posterolateral corner coordinate [x, y] in mm or µm"""
109
+ return self.corners[2]
110
+
111
+ @property
112
+ def al(self) -> np.ndarray:
113
+ """anterolateral corner coordinate [x, y] in mm or µm"""
114
+ return self.corners[3]
115
+
116
+ @property
117
+ def ap_distance(self) -> float:
118
+ """span along the AP axis"""
119
+ return float(np.ptp(self.corners[:, 1]))
120
+
121
+ @property
122
+ def ml_distance(self) -> float:
123
+ """span along the ML axis"""
124
+ return float(np.ptp(self.corners[:, 0]))
125
+
126
+ def invert_axes(self, ap: bool = True, ml: bool = True) -> Self:
127
+ """Return a new FOV with specified axes inverted
128
+
129
+ :param ap: invert anterior-posterior (Y) axis if True.
130
+ :param ml: invert medial-lateral (X) axis if True.
131
+ """
132
+ factors = np.array([-1. if ml else 1., -1. if ap else 1.])
133
+ return attrs.evolve(self, corners=self.corners * factors)
134
+
135
+ def to_um(self) -> Self:
136
+ if self.unit == 'um':
137
+ raise RuntimeError('unit already in um')
138
+ return attrs.evolve(self, corners=self.corners * 1000, unit='um')
139
+
140
+ def to_polygon(self, **kwargs) -> Polygon:
141
+ """convert corners to a matplotlib Polygon patch"""
142
+ idx = [0, 1, 3, 2]
143
+ reorder = self.corners[idx]
144
+ return Polygon(reorder, closed=True, edgecolor='r', facecolor='none', **kwargs)
145
+
146
+
147
+ def get_cellular_coordinate(neuron_idx: np.ndarray,
148
+ ap: np.ndarray,
149
+ ml: np.ndarray, *,
150
+ unit: UNIT = 'mm',
151
+ plane_index: np.ndarray | None = None) -> CellularCoordinates:
152
+ """
153
+ Get cellular coordinates container for doing brain mapping / topographical analysis
154
+
155
+ :param neuron_idx: neuron index. `Array[float, N]`
156
+ :param ap: anterior posterior coordinates. `Array[float, N]`
157
+ :param ml: medial lateral coordinates. `Array[float, N]`
158
+ :param unit: unit of the ap/ml value. default in `mm`
159
+ :param plane_index: neuron's corresponding image plane. `Array[float, N]`. If None then full_zero
160
+ :return: :class:`~CellularCoordinates`
161
+ """
162
+ return CellularCoordinates(neuron_idx, ap, ml, unit, plane_index)
163
+
164
+
165
+ def _validator_shape(instance: CellularCoordinates, attribute, value: np.ndarray | None):
166
+ if value is None:
167
+ return
168
+ else:
169
+ assert instance.neuron_idx.shape == value.shape
170
+
171
+
172
+ @attrs.define
173
+ class CellularCoordinates:
174
+ """Cellular Coordinates container"""
175
+
176
+ neuron_idx: np.ndarray
177
+ """neuron index. `Array[float, N]`"""
178
+ ap: np.ndarray
179
+ """anterior posterior coordinates (default in mm). `Array[float, N]`"""
180
+ ml: np.ndarray
181
+ """medial lateral coordinates (default in mm). `Array[float, N]`"""
182
+ unit: UNIT = attrs.field(default='mm', validator=attrs.validators.in_(['mm', 'um']))
183
+ """unit of the ap/ml value"""
184
+ plane_index: np.ndarray = attrs.field(default=None, validator=_validator_shape)
185
+ """neuron's corresponding image plane. `Array[float, N]`"""
186
+ value: np.ndarray | None = attrs.field(default=None, validator=_validator_shape)
187
+ """metric (i.e., used in topographical analysis). `Array[float, N]`"""
188
+
189
+ def __attrs_post_init__(self):
190
+ if self.plane_index is None:
191
+ self.plane_index = np.full_like(self.neuron_idx, 0, dtype=int)
192
+
193
+ assert self.neuron_idx.shape == self.ap.shape == self.ml.shape == self.plane_index.shape
194
+
195
+ def relative_origin(self, fov: FieldOfView,
196
+ origin: Literal['am', 'pm', 'al', 'pl'] = 'am') -> Self:
197
+ """
198
+ coordinates relative to :class:`~FieldOfView` origin point
199
+
200
+ :param fov: :class:`~FieldOfView`
201
+ :param origin: relative origin point
202
+ :return:
203
+ """
204
+
205
+ factor = 1000 if self.unit == 'mm' else 1
206
+ ap_um = self.ap * factor
207
+ ml_um = self.ml * factor
208
+ if fov.unit != 'um':
209
+ fov = fov.to_um()
210
+
211
+ orig_pt = getattr(fov, origin)
212
+ pts = np.vstack([ml_um, ap_um]).T
213
+
214
+ if fov.perpendicular:
215
+ delta = orig_pt - pts # posterior to origin
216
+ else:
217
+ delta_pts = orig_pt - _rotate_and_tilt(
218
+ pts,
219
+ ml=fov.rotation_ml,
220
+ ap=fov.rotation_ap,
221
+ )
222
+ delta = delta_pts
223
+
224
+ ml_new, ap_new = delta[:, 0], delta[:, 1]
225
+ return attrs.evolve(self, ml=ml_new, ap=ap_new, unit='um')
226
+
227
+ def with_value(self, value: np.ndarray) -> Self:
228
+ """assign value foreach neuron"""
229
+ return attrs.evolve(self, value=value)
230
+
231
+ def with_masking(self, mask: np.ndarray) -> Self:
232
+ """do neuronal selection by bool masking
233
+ :param mask: `Array[bool, N]`
234
+ """
235
+ return attrs.evolve(
236
+ self,
237
+ neuron_idx=self.neuron_idx[mask],
238
+ ap=self.ap[mask],
239
+ ml=self.ml[mask],
240
+ plane_index=self.plane_index[mask],
241
+ value=None if self.value is None else self.value[mask]
242
+ )
@@ -0,0 +1,2 @@
1
+ from .cascade import *
2
+ from .oasis import *