neuralib-parser 0.7.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.
@@ -0,0 +1 @@
1
+ from .core import *
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ from typing import Self
7
+
8
+ from neuralib.typing import PathLike
9
+
10
+ __all__ = [
11
+ 'read_cellpose',
12
+ 'cellpose_point_roi_helper',
13
+ 'CellposeSegmentation'
14
+ ]
15
+
16
+
17
+ def read_cellpose(file: PathLike) -> CellposeSegmentation:
18
+ """
19
+ Read a cellpose segmentation result file
20
+
21
+ :param file: cellpose segmentation result ``.npy`` file
22
+ :return: :class:`CellposeSegmentation`
23
+ """
24
+ return CellposeSegmentation.load(file)
25
+
26
+
27
+ def cellpose_point_roi_helper(file: PathLike, output: PathLike) -> None:
28
+ """
29
+ Read a cellpose segmentation result and convert the segmentation result to point coordinates
30
+
31
+ :param file: cellpose segmentation result ``.npy`` file
32
+ :param output: ``*.roi`` output file path
33
+ """
34
+ CellposeSegmentation.load(file).to_roi(output)
35
+
36
+
37
+ class CellposeSegmentation:
38
+ """`Cellpose <https://github.com/MouseLand/cellpose>`_ segmentation results
39
+
40
+ `Dimension parameters`:
41
+
42
+ N = Number of segmented cells
43
+
44
+ W = Image width
45
+
46
+ H = Image height
47
+
48
+ .. seealso::
49
+
50
+ `Cellpose Native Doc <https://cellpose.readthedocs.io/en/latest/outputs.html#seg-npy-output>`_
51
+
52
+ """
53
+
54
+ def __init__(self, outlines, masks, chan_choose, ismanual, flows, diameter, filename):
55
+ self._outlines = outlines
56
+ self._masks = masks
57
+ self._chan_choose = chan_choose
58
+ self._is_manual = ismanual
59
+ self._flows = flows
60
+ self._diameter = diameter
61
+
62
+ self._filename = filename
63
+
64
+ @classmethod
65
+ def load(cls, file: PathLike) -> Self:
66
+ """
67
+ Load a cellpose segmentation result
68
+
69
+ :param file: cellpose segmentation result ``.npy`` file
70
+ :return: :class:`CellposeSegmentation`
71
+ """
72
+ dat = np.load(file, allow_pickle=True).item()
73
+ return cls(**dat)
74
+
75
+ @property
76
+ def n_segmentation(self) -> int:
77
+ """number of segmented cells"""
78
+ return len(self._is_manual)
79
+
80
+ @property
81
+ def width(self):
82
+ """image width"""
83
+ return self._outlines.shape[1]
84
+
85
+ @property
86
+ def height(self):
87
+ """image height"""
88
+ return self._outlines.shape[0]
89
+
90
+ @property
91
+ def filename(self) -> Path:
92
+ """filepath of image"""
93
+ return Path(self._filename)
94
+
95
+ @property
96
+ def outlines(self) -> np.ndarray:
97
+ """outlines of ROIs (0 = NO outline; 1,2,… = outline labels). `Array[uint16, [H, W]]`"""
98
+ return self._outlines
99
+
100
+ @property
101
+ def masks(self) -> np.ndarray:
102
+ """each pixel in the image is assigned to an ROI (0 = NO ROI; 1,2,… = ROI labels). `Array[uint16, [H, W]]` """
103
+ return self._masks
104
+
105
+ @property
106
+ def chan_choose(self) -> list[int]:
107
+ """channels that you chose in GUI (0=gray/none, 1=red, 2=green, 3=blue)"""
108
+ return self._chan_choose
109
+
110
+ @property
111
+ def flows(self) -> list[np.ndarray]:
112
+ """
113
+ flows[0] is XY flow in RGB
114
+
115
+ flows[1] is the cell probability in range 0-255 instead of -10.0 to 10.0
116
+
117
+ flows[2] is Z flow in range 0-255 (if it exists, otherwise zeros),
118
+
119
+ flows[3] is [dY, dX, cellprob] (or [dZ, dY, dX, cellprob] for 3D), flows[4] is pixel destinations (for internal use)
120
+ """
121
+ return self._flows
122
+
123
+ @property
124
+ def diameter(self) -> float:
125
+ """cell body diameter"""
126
+ return self._diameter
127
+
128
+ @property
129
+ def is_manual(self) -> np.ndarray:
130
+ """whether or not mask k was manually drawn or computed by the cellpose algorithm. `Array[bool, N]`"""
131
+ return self._is_manual
132
+
133
+ @property
134
+ def nan_masks(self) -> np.ndarray:
135
+ """value 0 in :attr:`CellposeSegmentation.masks` to nan"""
136
+ masks = self.masks.copy().astype(np.float_)
137
+ masks[masks == 0] = np.nan
138
+
139
+ return masks
140
+
141
+ @property
142
+ def nan_outlines(self) -> np.ndarray:
143
+ """value 0 in :attr:`CellposeSegmentation.outlines` to nan"""
144
+ outlines = self.outlines.copy().astype(np.float_)
145
+ outlines[outlines == 0] = np.nan
146
+
147
+ return outlines
148
+
149
+ @property
150
+ def points(self) -> np.ndarray:
151
+ """Calculate center of each segmented area in XY pixel. `Array[int, [N, 2]]`"""
152
+ centers = self._calculate_centers()
153
+ return np.round(centers).astype(int)
154
+
155
+ # noinspection PyTypeChecker
156
+ def to_roi(self, output_file: PathLike):
157
+ """
158
+ Covert segmented roi to point roi, and save it as ``.roi`` for imageJ.
159
+
160
+ :param output_file: ``*.roi`` output file path
161
+ """
162
+ from roifile import ImagejRoi, ROI_TYPE, ROI_OPTIONS
163
+
164
+ if Path(output_file).suffix != '.roi':
165
+ raise ValueError('output file must have .roi extension')
166
+
167
+ points = np.fliplr(self.points) # XY rotate in .roi format
168
+ roi = ImagejRoi(
169
+ roitype=ROI_TYPE.POINT,
170
+ options=ROI_OPTIONS.PROMPT_BEFORE_DELETING | ROI_OPTIONS.SUB_PIXEL_RESOLUTION,
171
+ n_coordinates=self.points.shape[0],
172
+ integer_coordinates=points,
173
+ subpixel_coordinates=points
174
+ )
175
+
176
+ roi.tofile(output_file)
177
+
178
+ def _calculate_centers(self):
179
+ """calculate center of each segmented area in XY pixel"""
180
+ labels = np.unique(self.masks)
181
+ labels = labels[labels != 0] # remove background
182
+
183
+ n_neurons = len(labels)
184
+ centers = np.zeros((n_neurons, 2))
185
+ for i, label in enumerate(labels):
186
+ segment_coords = np.argwhere(self.masks == label)
187
+ center = segment_coords.mean(axis=0)
188
+ centers[i] = center
189
+
190
+ return centers
@@ -0,0 +1 @@
1
+ from .core import *
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pickle
6
+ import polars as pl
7
+ from typing import TypedDict
8
+
9
+ from neuralib.typing import PathLike
10
+ from neuralib.util.dataframe import DataFrameWrapper
11
+
12
+ __all__ = [
13
+ 'Joint',
14
+ 'read_dlc',
15
+ 'DeepLabCutDataFrame',
16
+ 'JointDataFrame',
17
+ 'DeepLabCutMeta',
18
+ 'DeepLabCutModelConfig'
19
+ ]
20
+
21
+ Joint = str
22
+ """Joint name"""
23
+
24
+
25
+ def read_dlc(file: PathLike, meta_file: PathLike | None = None) -> DeepLabCutDataFrame:
26
+ """
27
+ load DeepLabCut result from file
28
+
29
+ :param file: DeepLabCut result filepath. supports both ``.h5`` and ``.csv``
30
+ :param meta_file: Optional DeepLabCut meta filepath. should be the ``.pickle``
31
+ :return:
32
+ """
33
+ file = Path(file)
34
+ meta_file = Path(meta_file)
35
+ meta = _load_meta(meta_file)
36
+
37
+ match file.suffix:
38
+ case '.h5' | '.hdf5':
39
+ df = _load_dlc_h5_table(file)
40
+ case '.csv':
41
+ df = _load_dlc_csv(file)
42
+ case _:
43
+ raise ValueError(f'file: {file} is not supported')
44
+
45
+ return DeepLabCutDataFrame(df, meta=meta, filtered=('filtered' in file.name))
46
+
47
+
48
+ class DeepLabCutDataFrame(DataFrameWrapper):
49
+ """
50
+ DeepLabCut DataFrame ::
51
+
52
+ ┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐
53
+ │ Nose_x ┆ Nose_y ┆ Nose_like ┆ EarL_x ┆ … ┆ TailMid_l ┆ TailEnd_x ┆ TailEnd_y ┆ TailEnd_ │
54
+ │ --- ┆ --- ┆ lihood ┆ --- ┆ ┆ ikelihood ┆ --- ┆ --- ┆ likeliho │
55
+ │ f64 ┆ f64 ┆ --- ┆ f64 ┆ ┆ --- ┆ f64 ┆ f64 ┆ od │
56
+ │ ┆ ┆ f64 ┆ ┆ ┆ f64 ┆ ┆ ┆ --- │
57
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ f64 │
58
+ ╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡
59
+ │ 57.907318 ┆ 512.54742 ┆ 0.999679 ┆ 77.701355 ┆ … ┆ 0.999904 ┆ 257.71426 ┆ 561.89660 ┆ 0.999961 │
60
+ │ ┆ 4 ┆ ┆ ┆ ┆ ┆ 4 ┆ 6 ┆ │
61
+ │ 57.907318 ┆ 516.79528 ┆ 0.999688 ┆ 77.701355 ┆ … ┆ 0.999923 ┆ 257.71426 ┆ 562.05725 ┆ 0.999954 │
62
+ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
63
+ │ 94.259621 ┆ 43.849434 ┆ 0.973851 ┆ 106.33532 ┆ … ┆ 0.998977 ┆ 87.477776 ┆ 257.11996 ┆ 0.999937 │
64
+ │ 94.294357 ┆ 44.340511 ┆ 0.965436 ┆ 106.45220 ┆ … ┆ 0.999604 ┆ 87.223534 ┆ 258.46600 ┆ 0.999912 │
65
+ └───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘
66
+
67
+ """
68
+
69
+ def __init__(self, df: pl.DataFrame,
70
+ meta: DeepLabCutMeta | None, *,
71
+ filtered: bool):
72
+ """
73
+ :param df: DeepLabCut result dataframe
74
+ :param meta: :attr:`~DeepLabCutMeta`
75
+ :param filtered: whether the results has already been filtered
76
+ """
77
+ self._df = df
78
+ self._meta = meta
79
+ self._filtered = filtered
80
+
81
+ def __repr__(self):
82
+ raise repr(self.dataframe())
83
+
84
+ def dataframe(self, dataframe: pl.DataFrame = None, may_inplace=True):
85
+ if dataframe is None:
86
+ return self._df
87
+ else:
88
+ return DeepLabCutDataFrame(dataframe, meta=self._meta, filtered=self._filtered)
89
+
90
+ @property
91
+ def default_filtered(self) -> bool:
92
+ """whether default filtered when running the deeplabcut"""
93
+ return self._filtered
94
+
95
+ @property
96
+ def meta(self) -> DeepLabCutMeta:
97
+ """:attr:`~neuralib.tracking.deeplabcut.DeepLabCutMeta`"""
98
+ return self._meta
99
+
100
+ @property
101
+ def model_config(self) -> DeepLabCutModelConfig:
102
+ """:attr:`~neuralib.tracking.deeplabcut.DeepLabCutModelConfig`"""
103
+ return self.meta['model_config']
104
+
105
+ @property
106
+ def fps(self) -> float:
107
+ """frame per second, meta data required"""
108
+ return self.meta['fps']
109
+
110
+ @property
111
+ def nframes(self) -> int:
112
+ """number of frames"""
113
+ return self.meta['nframes']
114
+
115
+ @property
116
+ def joints(self) -> list[Joint]:
117
+ """list of labelled joints"""
118
+ return self.meta['model_config']['all_joints_names']
119
+
120
+ def get_joint(self, joint: Joint) -> JointDataFrame:
121
+ """get specific joint"""
122
+ cols = ('x', 'y', 'likelihood')
123
+ df = self.select([f'{joint}_{col}' for col in cols]).dataframe()
124
+ return JointDataFrame(df)
125
+
126
+
127
+ class JointDataFrame(DataFrameWrapper):
128
+ """
129
+ Dataframe from a specific joint ::
130
+
131
+ ┌───────────┬────────────┬─────────────────┐
132
+ │ Nose_x ┆ Nose_y ┆ Nose_likelihood │
133
+ │ --- ┆ --- ┆ --- │
134
+ │ f64 ┆ f64 ┆ f64 │
135
+ ╞═══════════╪════════════╪═════════════════╡
136
+ │ 57.907318 ┆ 512.547424 ┆ 0.999679 │
137
+ │ 57.907318 ┆ 516.795288 ┆ 0.999688 │
138
+ │ 57.907318 ┆ 519.56311 ┆ 0.999449 │
139
+ │ 56.733799 ┆ 522.204224 ┆ 0.999161 │
140
+ │ 53.546089 ┆ 525.24939 ┆ 0.999518 │
141
+ │ … ┆ … ┆ … │
142
+ │ 94.259621 ┆ 43.849434 ┆ 0.973851 │
143
+ │ 94.294357 ┆ 44.111595 ┆ 0.980125 │
144
+ │ 94.8013 ┆ 44.340511 ┆ 0.963981 │
145
+ │ 94.294357 ┆ 44.340511 ┆ 0.947905 │
146
+ │ 94.294357 ┆ 44.340511 ┆ 0.965436 │
147
+ └───────────┴────────────┴─────────────────┘
148
+ """
149
+
150
+ def __init__(self, df: pl.DataFrame):
151
+ self._df = df
152
+
153
+ def __repr__(self):
154
+ return repr(self.dataframe())
155
+
156
+ def dataframe(self, dataframe: pl.DataFrame = None, may_inplace=True):
157
+ if dataframe is None:
158
+ return self._df
159
+ else:
160
+ return JointDataFrame(dataframe)
161
+
162
+
163
+ class DeepLabCutMeta(TypedDict):
164
+ """DeepLabCut model metadata"""
165
+ start: float
166
+ stop: float
167
+ run_duration: float
168
+ Scorer: str
169
+ model_config: DeepLabCutModelConfig
170
+ fps: float
171
+ batch_size: int
172
+ frame_dimensions: tuple[int, int]
173
+ nframes: int
174
+ iteration: int
175
+ training_set_fraction: float
176
+ cropping: bool
177
+ cropping_parameters: list[tuple[float, float, float, float]]
178
+
179
+
180
+ class DeepLabCutModelConfig(TypedDict):
181
+ """DeepLabCut model configuration"""
182
+ stride: float
183
+ weigh_part_predictions: bool
184
+ weigh_negatives: bool
185
+ fg_fraction: float
186
+ mean_pixel: list[float]
187
+ shuffle: bool
188
+ snapshot_prefix: str
189
+ log_dir: str
190
+ global_scale: float
191
+ location_refinement: bool
192
+ locref_stdev: float
193
+ locref_loss_weight: float
194
+ locref_huber_loss: bool
195
+ optimizer: str
196
+ intermediate_supervision: bool
197
+ intermediate_supervision_layer: int
198
+ regularize: bool
199
+ weight_decay: float
200
+ crop_pad: int
201
+ scoremap_dir: str
202
+ batch_size: int
203
+ dataset_type: str
204
+ deterministic: bool
205
+ mirror: bool
206
+ pairwise_huber_loss: bool
207
+ weigh_only_present_joints: bool
208
+ partaffinityfield_predict: bool
209
+ pairwise_predict: bool
210
+ all_joints: list[list[int]]
211
+ all_joints_names: list[Joint]
212
+ dataset: str
213
+ init_weights: str
214
+ net_type: str
215
+ num_joints: int
216
+ num_outputs: int
217
+
218
+
219
+ def _load_dlc_h5_table(file) -> pl.DataFrame:
220
+ import pandas as pd
221
+
222
+ df = pd.read_hdf(file)
223
+
224
+ scorers = list(df.columns.levels[0])
225
+ bodyparts = list(df.columns.levels[1])
226
+ coords = list(df.columns.levels[2])
227
+
228
+ assert len(scorers) == 1
229
+ scorer = scorers[0]
230
+
231
+ data = {
232
+ f'{b}_{c}': df[(scorer, b, c)]
233
+ for b in bodyparts
234
+ for c in coords
235
+ }
236
+
237
+ ret = pl.DataFrame(data)
238
+
239
+ return ret
240
+
241
+
242
+ def _load_dlc_csv(file) -> pl.DataFrame:
243
+ cols = ['']
244
+ with file.open() as f:
245
+ f.readline() # skip first line
246
+ parts = f.readline().strip().split(',')[1::3]
247
+ cols.extend([f'{p}_{it}' for p in parts for it in ('x', 'y', 'likelihood')])
248
+
249
+ df = pl.read_csv(file, skip_rows=3, has_header=False, new_columns=cols)[:, 1:]
250
+
251
+ return df
252
+
253
+
254
+ def _load_meta(meta_file) -> DeepLabCutMeta:
255
+ if meta_file.suffix not in ('.pkl', '.pickle'):
256
+ raise ValueError(f'{meta_file} is not a pickle file')
257
+
258
+ # meta
259
+ with meta_file.open('rb') as f:
260
+ meta = pickle.load(f)['data']
261
+
262
+ # copy to typeddict
263
+ meta['model_config'] = meta['DLC-model-config file']
264
+ meta['iteration'] = meta['iteration (active-learning)']
265
+ meta['training_set_fraction'] = meta['training set fraction']
266
+
267
+ return meta
@@ -0,0 +1 @@
1
+ from .core import *