cindra 1.0.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.
- cindra/__init__.py +44 -0
- cindra/classification/__init__.py +8 -0
- cindra/classification/classifier.npz +0 -0
- cindra/classification/classify.py +363 -0
- cindra/dataclasses/__init__.py +76 -0
- cindra/dataclasses/multi_recording_configuration.py +207 -0
- cindra/dataclasses/multi_recording_data.py +561 -0
- cindra/dataclasses/runtime_contexts.py +534 -0
- cindra/dataclasses/single_recording_configuration.py +563 -0
- cindra/dataclasses/single_recording_data.py +1619 -0
- cindra/dataclasses/version.py +22 -0
- cindra/detection/__init__.py +29 -0
- cindra/detection/denoise.py +125 -0
- cindra/detection/detect.py +537 -0
- cindra/detection/detect_rois.py +775 -0
- cindra/detection/roi_statistics.py +564 -0
- cindra/detection/tracking.py +583 -0
- cindra/detection/utils.py +326 -0
- cindra/extraction/__init__.py +14 -0
- cindra/extraction/colocalization.py +389 -0
- cindra/extraction/deconvolve.py +237 -0
- cindra/extraction/extract.py +1007 -0
- cindra/extraction/masks.py +277 -0
- cindra/gui/__init__.py +15 -0
- cindra/gui/app.py +220 -0
- cindra/gui/binary_viewer.py +562 -0
- cindra/gui/constants.py +336 -0
- cindra/gui/data_models.py +99 -0
- cindra/gui/overlays.py +986 -0
- cindra/gui/pc_viewer.py +527 -0
- cindra/gui/roi_viewer.py +1930 -0
- cindra/gui/styles.py +190 -0
- cindra/gui/tracking_viewer.py +902 -0
- cindra/gui/viewer_context.py +1234 -0
- cindra/gui/viewer_state.py +110 -0
- cindra/gui/widgets.py +580 -0
- cindra/interface/__init__.py +3 -0
- cindra/interface/acquisition_tools.py +472 -0
- cindra/interface/cli.py +312 -0
- cindra/interface/configuration_tools.py +833 -0
- cindra/interface/gui_cli.py +114 -0
- cindra/interface/gui_mcp_server.py +245 -0
- cindra/interface/mcp_instance.py +13 -0
- cindra/interface/mcp_server.py +21 -0
- cindra/interface/processing_tools.py +2205 -0
- cindra/interface/results_tools.py +1696 -0
- cindra/io/__init__.py +30 -0
- cindra/io/binary.py +547 -0
- cindra/io/combine.py +498 -0
- cindra/io/context.py +603 -0
- cindra/io/select.py +262 -0
- cindra/io/tiff.py +465 -0
- cindra/pipelines/__init__.py +19 -0
- cindra/pipelines/multi_recording.py +112 -0
- cindra/pipelines/pipeline.py +560 -0
- cindra/pipelines/single_recording.py +231 -0
- cindra/py.typed +0 -0
- cindra/registration/__init__.py +16 -0
- cindra/registration/bidiphase_correction.py +80 -0
- cindra/registration/deformation.py +817 -0
- cindra/registration/diffeomorphic.py +497 -0
- cindra/registration/metrics.py +412 -0
- cindra/registration/nonrigid.py +576 -0
- cindra/registration/pyramid.py +132 -0
- cindra/registration/register.py +1117 -0
- cindra/registration/register_recordings.py +523 -0
- cindra/registration/rigid.py +184 -0
- cindra/registration/spline_grid.py +408 -0
- cindra/registration/utils.py +351 -0
- cindra-1.0.0.dist-info/METADATA +994 -0
- cindra-1.0.0.dist-info/RECORD +74 -0
- cindra-1.0.0.dist-info/WHEEL +4 -0
- cindra-1.0.0.dist-info/entry_points.txt +3 -0
- cindra-1.0.0.dist-info/licenses/LICENSE +621 -0
cindra/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Provides pipelines for processing neural imaging data and tracking Regions of Interest across multiple recordings.
|
|
2
|
+
|
|
3
|
+
See the `documentation <https://cindra-api-docs.netlify.app/>`_ for the description of available assets. See the
|
|
4
|
+
`source code repository <https://github.com/Sun-Lab-NBB/cindra>`_ for more details.
|
|
5
|
+
|
|
6
|
+
Authors: Ivan Kondratyev, Natalie Yeung
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Configures numba threading layer for parallel execution across all modules. This must be set before any numba
|
|
10
|
+
# functions are compiled, hence it appears before other imports. macOS uses OpenMP (libomp via llvm-openmp) because
|
|
11
|
+
# tbb4py publishes no Apple Silicon wheel; all other platforms use TBB for lower overhead on flat prange loops.
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from numba import config # type: ignore[import-untyped]
|
|
15
|
+
|
|
16
|
+
config.THREADING_LAYER = "omp" if sys.platform == "darwin" else "tbb"
|
|
17
|
+
|
|
18
|
+
from ataraxis_base_utilities import console # noqa: E402
|
|
19
|
+
|
|
20
|
+
from .pipelines import ( # noqa: E402
|
|
21
|
+
MultiRecordingJobNames,
|
|
22
|
+
SingleRecordingJobNames,
|
|
23
|
+
run_multi_recording_pipeline,
|
|
24
|
+
run_single_recording_pipeline,
|
|
25
|
+
)
|
|
26
|
+
from .dataclasses import ( # noqa: E402
|
|
27
|
+
MultiRecordingConfiguration,
|
|
28
|
+
SingleRecordingConfiguration,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Ensures console output is enabled whenever the cindra library is imported. The 'Console' class is
|
|
32
|
+
# used over 'print' for all terminal outputs. With minimal configuration, this class can be extended to log terminal
|
|
33
|
+
# outputs instead of or in addition to sending them to the terminal.
|
|
34
|
+
if not console.enabled:
|
|
35
|
+
console.enable()
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"MultiRecordingConfiguration",
|
|
39
|
+
"MultiRecordingJobNames",
|
|
40
|
+
"SingleRecordingConfiguration",
|
|
41
|
+
"SingleRecordingJobNames",
|
|
42
|
+
"run_multi_recording_pipeline",
|
|
43
|
+
"run_single_recording_pipeline",
|
|
44
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Provides ROI classification functionality for distinguishing cells from artifacts."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from operator import attrgetter
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.ndimage import gaussian_filter
|
|
9
|
+
from sklearn.linear_model import LogisticRegression # type: ignore[import-untyped]
|
|
10
|
+
from ataraxis_base_utilities import console
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
|
|
15
|
+
from ..dataclasses import ROIStatistics
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_BUILTIN_CLASSIFIER_PATH: Path = Path(__file__).parent / "classifier.npz"
|
|
19
|
+
"""The path to the built-in classifier bundled with cindra."""
|
|
20
|
+
|
|
21
|
+
_CLASSIFICATION_FEATURES: tuple[str, ...] = ("normalized_pixel_count", "compactness", "skewness")
|
|
22
|
+
"""The names of the ROI features used for full classification (after signal extraction), in the order they appear in
|
|
23
|
+
the feature matrix."""
|
|
24
|
+
|
|
25
|
+
_PRECLASSIFICATION_FEATURES: tuple[str, ...] = ("normalized_pixel_count", "compactness")
|
|
26
|
+
"""The names of the ROI features used for preclassification (during detection, before signal extraction). This subset
|
|
27
|
+
excludes skewness which requires extracted fluorescence traces to compute."""
|
|
28
|
+
|
|
29
|
+
_GRID_NODE_COUNT: int = 100
|
|
30
|
+
"""The number of grid nodes used for probability estimation during model fitting."""
|
|
31
|
+
|
|
32
|
+
_LOG_EPSILON: float = 1e-6
|
|
33
|
+
"""The small epsilon value added to probabilities to prevent log(0) errors."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Classifier:
|
|
37
|
+
"""Provides logistic regression-based classification for identifying cell ROIs.
|
|
38
|
+
|
|
39
|
+
Loads classifier training data from the specified .npz file, fits a logistic regression model, and uses
|
|
40
|
+
it to predict whether detected ROIs represent real cells or artifacts based on their morphological features.
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
The classifier file format uses pickle-free npz serialization containing training_labels and feature arrays
|
|
44
|
+
(normalized_pixel_count, compactness, skewness). The model is fitted on load, which takes approximately 10ms
|
|
45
|
+
for the default training set.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
classifier_path: The path to a classifier .npz file containing training_labels and feature arrays.
|
|
49
|
+
feature_names: The tuple of feature names to use for classification. Only these features will be loaded from
|
|
50
|
+
the classifier file and used for model fitting. If None, all available features in the classifier file
|
|
51
|
+
are used.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
_classifier_path: The path to the loaded classifier file.
|
|
55
|
+
_available_features: The list of feature names used by the classifier.
|
|
56
|
+
_training_features: A dictionary mapping feature names to their training value arrays.
|
|
57
|
+
_training_labels: The boolean training labels array with shape (n_samples,).
|
|
58
|
+
_probability_grid: The grid boundaries computed from sorted training statistics with shape
|
|
59
|
+
(n_nodes, n_features). Used to map input feature values to grid intervals for probability lookup
|
|
60
|
+
during classification.
|
|
61
|
+
_grid_cell_probabilities: The Gaussian-smoothed probability that an ROI is a cell for each grid interval
|
|
62
|
+
with shape (n_nodes - 1, n_features). Used to compute log probability ratios that serve as input
|
|
63
|
+
features for the logistic regression model.
|
|
64
|
+
_model: The fitted LogisticRegression model.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, classifier_path: Path, feature_names: tuple[str, ...] | None = None) -> None:
|
|
68
|
+
if not classifier_path.exists():
|
|
69
|
+
message = (
|
|
70
|
+
f"Unable to load the classification training data. The classifier file does not exist at the "
|
|
71
|
+
f"specified path: {classifier_path}."
|
|
72
|
+
)
|
|
73
|
+
console.error(message=message, error=FileNotFoundError)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Loads the training data.
|
|
77
|
+
data = np.load(classifier_path, allow_pickle=False)
|
|
78
|
+
|
|
79
|
+
if "training_labels" not in data:
|
|
80
|
+
message = (
|
|
81
|
+
f"Unable to load the classification training data. The classifier file at {classifier_path} is "
|
|
82
|
+
f"missing the 'training_labels' column."
|
|
83
|
+
)
|
|
84
|
+
console.error(message=message, error=ValueError)
|
|
85
|
+
|
|
86
|
+
# Resolves the labels and the training dataset size.
|
|
87
|
+
training_labels = data["training_labels"].astype(np.bool_)
|
|
88
|
+
n_samples = len(training_labels)
|
|
89
|
+
|
|
90
|
+
training_features: dict[str, NDArray[np.float32]] = {}
|
|
91
|
+
available_features: list[str] = []
|
|
92
|
+
|
|
93
|
+
# Determines which features to load. If feature_names is specified, only those features are used.
|
|
94
|
+
# Otherwise, all available features in the classifier file are used.
|
|
95
|
+
target_features = feature_names if feature_names is not None else _CLASSIFICATION_FEATURES
|
|
96
|
+
|
|
97
|
+
# Loads the requested features from the classifier file. As long as the dataset contains at least one
|
|
98
|
+
# valid feature, the class can train the model. This allows flexibly working with incomplete datasets
|
|
99
|
+
# and extending the feature set in the future.
|
|
100
|
+
for feature_name in target_features:
|
|
101
|
+
if feature_name in data:
|
|
102
|
+
feature_array = data[feature_name].astype(np.float32)
|
|
103
|
+
if len(feature_array) == n_samples and not np.all(np.isnan(feature_array)):
|
|
104
|
+
training_features[feature_name] = feature_array
|
|
105
|
+
available_features.append(feature_name)
|
|
106
|
+
|
|
107
|
+
if not available_features:
|
|
108
|
+
message = (
|
|
109
|
+
f"Unable to load the classification training data. The classifier file at {classifier_path} "
|
|
110
|
+
f"does not contain any of the expected feature columns: {', '.join(target_features)}."
|
|
111
|
+
)
|
|
112
|
+
console.error(message=message, error=ValueError)
|
|
113
|
+
|
|
114
|
+
# Sets instance attributes after all validation passes.
|
|
115
|
+
self._classifier_path: Path = classifier_path
|
|
116
|
+
self._available_features: list[str] = available_features
|
|
117
|
+
self._training_features: dict[str, NDArray[np.float32]] = training_features
|
|
118
|
+
self._training_labels: NDArray[np.bool_] = training_labels
|
|
119
|
+
|
|
120
|
+
# Fits the logistic regression model using the validated training data.
|
|
121
|
+
self._fit_model()
|
|
122
|
+
|
|
123
|
+
except (ValueError, KeyError, TypeError) as exception:
|
|
124
|
+
message = (
|
|
125
|
+
f"Unable to load the classification training data. The classifier file at {classifier_path} is "
|
|
126
|
+
f"corrupted or has an invalid format. Original loader error: {exception}."
|
|
127
|
+
)
|
|
128
|
+
console.error(message=message, error=ValueError)
|
|
129
|
+
|
|
130
|
+
def _extract_features(self, roi_statistics: list[ROIStatistics]) -> NDArray[np.float32]:
|
|
131
|
+
"""Extracts classification features supported by the model from ROIStatistics instances.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
roi_statistics: The list of ROIStatistics instances to extract features from.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
An array of shape (n_rois, n_features) containing the extracted features.
|
|
138
|
+
"""
|
|
139
|
+
n_rois = len(roi_statistics)
|
|
140
|
+
n_features = len(self._available_features)
|
|
141
|
+
features = np.zeros((n_rois, n_features), dtype=np.float32)
|
|
142
|
+
|
|
143
|
+
# Pre-creates attribute accessors to avoid repeated string lookups.
|
|
144
|
+
getters = [attrgetter(name) for name in self._available_features]
|
|
145
|
+
|
|
146
|
+
# Extracts feature values, using NaN for missing values.
|
|
147
|
+
for roi_index, roi in enumerate(roi_statistics):
|
|
148
|
+
for feature_index, getter in enumerate(getters):
|
|
149
|
+
value = getter(roi)
|
|
150
|
+
features[roi_index, feature_index] = np.nan if value is None else value
|
|
151
|
+
|
|
152
|
+
return features
|
|
153
|
+
|
|
154
|
+
def _get_training_features(self) -> NDArray[np.float32]:
|
|
155
|
+
"""Assembles the training feature matrix from individual feature arrays.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
An array of shape (n_samples, n_features) containing the training features.
|
|
159
|
+
"""
|
|
160
|
+
feature_arrays = [self._training_features[name] for name in self._available_features]
|
|
161
|
+
return np.column_stack(feature_arrays)
|
|
162
|
+
|
|
163
|
+
def _compute_log_probabilities(self, features: NDArray[np.float32]) -> NDArray[np.float32]:
|
|
164
|
+
"""Computes log probability ratios for the given features.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
features: An array of shape (n_samples, n_features) containing the feature values.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
An array of shape (n_samples, n_features) containing the log probability ratios.
|
|
171
|
+
"""
|
|
172
|
+
log_probabilities = np.zeros(features.shape, dtype=np.float32)
|
|
173
|
+
|
|
174
|
+
for feature_index in range(features.shape[1]):
|
|
175
|
+
feature_values = features[:, feature_index].copy()
|
|
176
|
+
|
|
177
|
+
# Clips feature values to the grid bounds and replaces NaN with the minimum grid value.
|
|
178
|
+
grid_min = self._probability_grid[0, feature_index]
|
|
179
|
+
grid_max = self._probability_grid[-1, feature_index]
|
|
180
|
+
feature_values = np.clip(feature_values, a_min=grid_min, a_max=grid_max)
|
|
181
|
+
feature_values[np.isnan(feature_values)] = grid_min
|
|
182
|
+
|
|
183
|
+
# Maps each feature value to its corresponding grid bin index.
|
|
184
|
+
bin_indices = np.digitize(feature_values, bins=self._probability_grid[:, feature_index], right=True) - 1
|
|
185
|
+
bin_indices = np.clip(bin_indices, a_min=0, a_max=self._grid_cell_probabilities.shape[0] - 1)
|
|
186
|
+
|
|
187
|
+
# Looks up the pre-computed cell probability for each bin and converts to log-odds.
|
|
188
|
+
probabilities = self._grid_cell_probabilities[bin_indices, feature_index]
|
|
189
|
+
log_probabilities[:, feature_index] = np.log(probabilities + _LOG_EPSILON) - np.log(
|
|
190
|
+
1 - probabilities + _LOG_EPSILON
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return log_probabilities
|
|
194
|
+
|
|
195
|
+
def _predict_probabilities(self, roi_statistics: list[ROIStatistics]) -> NDArray[np.float32]:
|
|
196
|
+
"""Predicts the probability that each ROI in the input list is a cell.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
roi_statistics: The list of ROIStatistics instances that define the ROIs to predict probabilities for.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
An array of shape (n_rois,) containing the probability that each ROI is a cell.
|
|
203
|
+
"""
|
|
204
|
+
features = self._extract_features(roi_statistics=roi_statistics)
|
|
205
|
+
log_probabilities = self._compute_log_probabilities(features=features)
|
|
206
|
+
predictions = self._model.predict_proba(log_probabilities)[:, 1]
|
|
207
|
+
|
|
208
|
+
return predictions.astype(np.float32)
|
|
209
|
+
|
|
210
|
+
def _fit_model(self) -> None:
|
|
211
|
+
"""Fits the logistic regression model using the loaded training data."""
|
|
212
|
+
training_features = self._get_training_features()
|
|
213
|
+
n_samples, n_features = training_features.shape
|
|
214
|
+
|
|
215
|
+
# Sorts features and creates evenly-spaced grid boundaries for probability estimation.
|
|
216
|
+
sorted_features = np.sort(training_features, axis=0)
|
|
217
|
+
sort_indices = np.argsort(training_features, axis=0)
|
|
218
|
+
grid_indices = np.linspace(start=0, stop=n_samples - 1, num=_GRID_NODE_COUNT).astype(np.intp)
|
|
219
|
+
self._probability_grid = sorted_features[grid_indices, :]
|
|
220
|
+
|
|
221
|
+
# Computes the fraction of cells (vs artifacts) in each grid bin for each feature.
|
|
222
|
+
self._grid_cell_probabilities = np.zeros((_GRID_NODE_COUNT - 1, n_features), dtype=np.float32)
|
|
223
|
+
bin_sizes = grid_indices[1:] - grid_indices[:-1]
|
|
224
|
+
|
|
225
|
+
for feature_index in range(n_features):
|
|
226
|
+
# Reorders labels by sorted feature values and computes cumulative sum.
|
|
227
|
+
sorted_labels = self._training_labels[sort_indices[:, feature_index]].astype(np.float32)
|
|
228
|
+
cumulative_sum = np.concatenate([[0], np.cumsum(sorted_labels)])
|
|
229
|
+
|
|
230
|
+
# Computes bin sums using cumulative sum differences, then converts to means.
|
|
231
|
+
bin_sums = cumulative_sum[grid_indices[1:]] - cumulative_sum[grid_indices[:-1]]
|
|
232
|
+
self._grid_cell_probabilities[:, feature_index] = bin_sums / bin_sizes
|
|
233
|
+
|
|
234
|
+
# Smooths the probability estimates across bins to reduce noise.
|
|
235
|
+
self._grid_cell_probabilities = gaussian_filter(self._grid_cell_probabilities, sigma=(2.0, 0)).astype(
|
|
236
|
+
np.float32
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Fits the logistic regression model using log-odds transformed features.
|
|
240
|
+
log_probabilities = self._compute_log_probabilities(features=training_features)
|
|
241
|
+
self._model = LogisticRegression(C=100.0, solver="liblinear")
|
|
242
|
+
self._model.fit(X=log_probabilities, y=self._training_labels)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def create_training_dataset(
|
|
246
|
+
file_path: Path,
|
|
247
|
+
training_labels: NDArray[np.bool_],
|
|
248
|
+
normalized_pixel_count: NDArray[np.float32],
|
|
249
|
+
compactness: NDArray[np.float32],
|
|
250
|
+
skewness: NDArray[np.float32],
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Creates a new classifier training dataset file from the provided labels and features.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
file_path: The path where the classifier file will be saved. Should have .npz extension.
|
|
256
|
+
training_labels: An array of binary labels (False for artifact, True for cell) with shape (n_samples,).
|
|
257
|
+
normalized_pixel_count: An array of normalized pixel count values with shape (n_samples,).
|
|
258
|
+
compactness: An array of compactness values with shape (n_samples,).
|
|
259
|
+
skewness: An array of skewness values with shape (n_samples,).
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ValueError: If feature arrays have mismatched lengths.
|
|
263
|
+
"""
|
|
264
|
+
n_samples = len(training_labels)
|
|
265
|
+
|
|
266
|
+
# Validates feature array lengths.
|
|
267
|
+
features = {
|
|
268
|
+
"normalized_pixel_count": normalized_pixel_count,
|
|
269
|
+
"compactness": compactness,
|
|
270
|
+
"skewness": skewness,
|
|
271
|
+
}
|
|
272
|
+
for feature_name, feature_array in features.items():
|
|
273
|
+
if len(feature_array) != n_samples:
|
|
274
|
+
message = (
|
|
275
|
+
f"Unable to create the classifier training dataset file. The feature '{feature_name}' has "
|
|
276
|
+
f"{len(feature_array)} samples, but training_labels has {n_samples} samples."
|
|
277
|
+
)
|
|
278
|
+
console.error(message=message, error=ValueError)
|
|
279
|
+
|
|
280
|
+
# Saves the training dataset.
|
|
281
|
+
np.savez(
|
|
282
|
+
file_path,
|
|
283
|
+
training_labels=training_labels,
|
|
284
|
+
normalized_pixel_count=normalized_pixel_count,
|
|
285
|
+
compactness=compactness,
|
|
286
|
+
skewness=skewness,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def classify(
|
|
290
|
+
self,
|
|
291
|
+
roi_statistics: list[ROIStatistics],
|
|
292
|
+
probability_threshold: float = 0.5,
|
|
293
|
+
) -> NDArray[np.float32]:
|
|
294
|
+
"""Classifies the ROIs as cells or non-cells based on their morphological features.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
roi_statistics: The list of ROIStatistics instances that store the features of the ROIs to classify.
|
|
298
|
+
probability_threshold: The probability threshold above which an ROI is classified as a cell.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
An array of shape (n_rois, 2) where each row contains [is_cell, probability]. The is_cell value is 1.0
|
|
302
|
+
if the ROI is classified as a cell (probability > threshold) and 0.0 otherwise.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
ValueError: If the input roi_statistics list is empty.
|
|
306
|
+
"""
|
|
307
|
+
if not roi_statistics:
|
|
308
|
+
message = "Unable to classify ROIs. The input roi_statistics list is empty."
|
|
309
|
+
console.error(message=message, error=ValueError)
|
|
310
|
+
|
|
311
|
+
probabilities = self._predict_probabilities(roi_statistics=roi_statistics)
|
|
312
|
+
is_cell = (probabilities > probability_threshold).astype(np.float32)
|
|
313
|
+
|
|
314
|
+
return np.stack([is_cell, probabilities], axis=1)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def classify(
|
|
318
|
+
roi_statistics: list[ROIStatistics],
|
|
319
|
+
classification_threshold: float = 0.5,
|
|
320
|
+
custom_classifier_path: Path | None = None,
|
|
321
|
+
preclassification: bool = False,
|
|
322
|
+
) -> NDArray[np.float32]:
|
|
323
|
+
"""Classifies detected ROIs as cells or non-cells using a logistic regression model.
|
|
324
|
+
|
|
325
|
+
Loads classifier training data from the specified file (or the built-in classifier if no custom path
|
|
326
|
+
is provided), fits a logistic regression model, and uses it to classify the input ROIs based on their morphological
|
|
327
|
+
features.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
roi_statistics: The list of ROIStatistics instances containing the morphological features of the ROIs to
|
|
331
|
+
classify. Must contain at least one ROI.
|
|
332
|
+
classification_threshold: The probability threshold above which an ROI is classified as a cell. ROIs with
|
|
333
|
+
probabilities above this threshold are labeled as cells (1.0), others as non-cells (0.0).
|
|
334
|
+
custom_classifier_path: An optional path to a custom classifier .npz file. If None, the built-in classifier
|
|
335
|
+
bundled with cindra is used.
|
|
336
|
+
preclassification: Determines whether to use a 2-feature model (normalized_pixel_count, compactness) suitable
|
|
337
|
+
for early filtering during detection before signal extraction. When False, uses the full 3-feature model
|
|
338
|
+
that includes skewness computed from extracted fluorescence traces.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
An array of shape (n_rois, 2) where each row contains [is_cell, probability]. The is_cell value is 1.0 if the
|
|
342
|
+
ROI is classified as a cell (probability > threshold) and 0.0 otherwise.
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
ValueError: If the input roi_statistics list is empty.
|
|
346
|
+
"""
|
|
347
|
+
if not roi_statistics:
|
|
348
|
+
message = (
|
|
349
|
+
"Unable to classify ROIs. No ROIs appear to have been detected. Classification requires detection to "
|
|
350
|
+
"discover at least one valid ROI candidate."
|
|
351
|
+
)
|
|
352
|
+
console.error(message=message, error=ValueError)
|
|
353
|
+
|
|
354
|
+
# Resolves the classifier dataset to use for training the model.
|
|
355
|
+
classifier_path = custom_classifier_path if custom_classifier_path is not None else _BUILTIN_CLASSIFIER_PATH
|
|
356
|
+
|
|
357
|
+
# Selects the feature set based on the classification mode. Preclassification uses only morphological features
|
|
358
|
+
# available during detection, while full classification includes skewness from extracted fluorescence.
|
|
359
|
+
feature_names = _PRECLASSIFICATION_FEATURES if preclassification else _CLASSIFICATION_FEATURES
|
|
360
|
+
|
|
361
|
+
# Trains the logistic regression model (~10 ms) and uses it to classify the detected ROIs.
|
|
362
|
+
classifier = Classifier(classifier_path=classifier_path, feature_names=feature_names)
|
|
363
|
+
return classifier.classify(roi_statistics=roi_statistics, probability_threshold=classification_threshold)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Provides configuration and runtime data classes for the single-recording and multi-recording cindra pipelines."""
|
|
2
|
+
|
|
3
|
+
from .version import version, python_version
|
|
4
|
+
from .runtime_contexts import RuntimeContext, MultiRecordingRuntimeContext
|
|
5
|
+
from .multi_recording_data import (
|
|
6
|
+
MultiRecordingIOData,
|
|
7
|
+
MultiRecordingTimingData,
|
|
8
|
+
MultiRecordingRuntimeData,
|
|
9
|
+
MultiRecordingTrackingData,
|
|
10
|
+
MultiRecordingRegistrationData,
|
|
11
|
+
)
|
|
12
|
+
from .single_recording_data import (
|
|
13
|
+
IOData,
|
|
14
|
+
ROIMask,
|
|
15
|
+
TimingData,
|
|
16
|
+
CombinedData,
|
|
17
|
+
DetectionData,
|
|
18
|
+
ROIStatistics,
|
|
19
|
+
ExtractionData,
|
|
20
|
+
RegistrationData,
|
|
21
|
+
SingleRecordingRuntimeData,
|
|
22
|
+
)
|
|
23
|
+
from .multi_recording_configuration import ReferenceImageType, MultiRecordingConfiguration
|
|
24
|
+
from .single_recording_configuration import (
|
|
25
|
+
Main,
|
|
26
|
+
FileIO,
|
|
27
|
+
PipelineType,
|
|
28
|
+
ROIDetection,
|
|
29
|
+
Registration,
|
|
30
|
+
BaselineMethod,
|
|
31
|
+
RuntimeSettings,
|
|
32
|
+
SignalExtraction,
|
|
33
|
+
SpikeDeconvolution,
|
|
34
|
+
NonrigidRegistration,
|
|
35
|
+
AcquisitionParameters,
|
|
36
|
+
OnePhotonRegistration,
|
|
37
|
+
SingleRecordingConfiguration,
|
|
38
|
+
detect_pipeline_type,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AcquisitionParameters",
|
|
43
|
+
"BaselineMethod",
|
|
44
|
+
"CombinedData",
|
|
45
|
+
"DetectionData",
|
|
46
|
+
"ExtractionData",
|
|
47
|
+
"FileIO",
|
|
48
|
+
"IOData",
|
|
49
|
+
"Main",
|
|
50
|
+
"MultiRecordingConfiguration",
|
|
51
|
+
"MultiRecordingIOData",
|
|
52
|
+
"MultiRecordingRegistrationData",
|
|
53
|
+
"MultiRecordingRuntimeContext",
|
|
54
|
+
"MultiRecordingRuntimeData",
|
|
55
|
+
"MultiRecordingTimingData",
|
|
56
|
+
"MultiRecordingTrackingData",
|
|
57
|
+
"NonrigidRegistration",
|
|
58
|
+
"OnePhotonRegistration",
|
|
59
|
+
"PipelineType",
|
|
60
|
+
"ROIDetection",
|
|
61
|
+
"ROIMask",
|
|
62
|
+
"ROIStatistics",
|
|
63
|
+
"ReferenceImageType",
|
|
64
|
+
"Registration",
|
|
65
|
+
"RegistrationData",
|
|
66
|
+
"RuntimeContext",
|
|
67
|
+
"RuntimeSettings",
|
|
68
|
+
"SignalExtraction",
|
|
69
|
+
"SingleRecordingConfiguration",
|
|
70
|
+
"SingleRecordingRuntimeData",
|
|
71
|
+
"SpikeDeconvolution",
|
|
72
|
+
"TimingData",
|
|
73
|
+
"detect_pipeline_type",
|
|
74
|
+
"python_version",
|
|
75
|
+
"version",
|
|
76
|
+
]
|