napari-tmidas 0.2.2__py3-none-any.whl → 0.2.4__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.
- napari_tmidas/__init__.py +35 -5
- napari_tmidas/_crop_anything.py +1520 -609
- napari_tmidas/_env_manager.py +76 -0
- napari_tmidas/_file_conversion.py +1646 -1131
- napari_tmidas/_file_selector.py +1455 -216
- napari_tmidas/_label_inspection.py +83 -8
- napari_tmidas/_processing_worker.py +309 -0
- napari_tmidas/_reader.py +6 -10
- napari_tmidas/_registry.py +2 -2
- napari_tmidas/_roi_colocalization.py +1221 -84
- napari_tmidas/_tests/test_crop_anything.py +123 -0
- napari_tmidas/_tests/test_env_manager.py +89 -0
- napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
- napari_tmidas/_tests/test_init.py +98 -0
- napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
- napari_tmidas/_tests/test_label_inspection.py +86 -0
- napari_tmidas/_tests/test_processing_basic.py +500 -0
- napari_tmidas/_tests/test_processing_worker.py +142 -0
- napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
- napari_tmidas/_tests/test_registry.py +70 -2
- napari_tmidas/_tests/test_scipy_filters.py +168 -0
- napari_tmidas/_tests/test_skimage_filters.py +259 -0
- napari_tmidas/_tests/test_split_channels.py +217 -0
- napari_tmidas/_tests/test_spotiflow.py +87 -0
- napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
- napari_tmidas/_tests/test_ui_utils.py +68 -0
- napari_tmidas/_tests/test_widget.py +30 -0
- napari_tmidas/_tests/test_windows_basic.py +66 -0
- napari_tmidas/_ui_utils.py +57 -0
- napari_tmidas/_version.py +16 -3
- napari_tmidas/_widget.py +41 -4
- napari_tmidas/processing_functions/basic.py +557 -20
- napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
- napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
- napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
- napari_tmidas/processing_functions/colocalization.py +513 -56
- napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
- napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
- napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
- napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
- napari_tmidas/processing_functions/sam2_mp4.py +274 -195
- napari_tmidas/processing_functions/scipy_filters.py +403 -8
- napari_tmidas/processing_functions/skimage_filters.py +424 -212
- napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
- napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
- napari_tmidas/processing_functions/timepoint_merger.py +334 -86
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +70 -30
- napari_tmidas-0.2.4.dist-info/RECORD +63 -0
- napari_tmidas/_tests/__init__.py +0 -0
- napari_tmidas-0.2.2.dist-info/RECORD +0 -40
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,37 @@ def get_nonzero_labels(image):
|
|
|
19
19
|
return [int(x) for x in labels]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def convert_semantic_to_instance_labels(image, connectivity=None):
|
|
23
|
+
"""
|
|
24
|
+
Convert semantic labels (where all objects have the same value) to instance labels.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
image: Label image that may contain semantic labels
|
|
28
|
+
connectivity: Connectivity for connected component analysis (1, 2, or None for full)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Image with instance labels (each connected component gets unique label)
|
|
32
|
+
"""
|
|
33
|
+
if image is None or np.all(image == 0):
|
|
34
|
+
return image
|
|
35
|
+
|
|
36
|
+
# Get unique non-zero values
|
|
37
|
+
unique_labels = np.unique(image[image != 0])
|
|
38
|
+
|
|
39
|
+
# Quick check: if there's only one unique non-zero value, it's definitely semantic
|
|
40
|
+
# Otherwise, apply connected components to the entire mask at once (much faster)
|
|
41
|
+
if len(unique_labels) == 1:
|
|
42
|
+
# Single semantic label - just label connected components of the binary mask
|
|
43
|
+
mask = image > 0
|
|
44
|
+
return measure.label(mask, connectivity=connectivity)
|
|
45
|
+
else:
|
|
46
|
+
# Multiple labels - could be instance or semantic
|
|
47
|
+
# Apply connected components to entire non-zero region at once
|
|
48
|
+
# This is MUCH faster than iterating over each label value
|
|
49
|
+
mask = image > 0
|
|
50
|
+
return measure.label(mask, connectivity=connectivity)
|
|
51
|
+
|
|
52
|
+
|
|
22
53
|
def count_unique_nonzero(array, mask):
|
|
23
54
|
"""Count unique non-zero values in array where mask is True."""
|
|
24
55
|
unique_vals = np.unique(array[mask])
|
|
@@ -61,6 +92,160 @@ def calculate_coloc_size(
|
|
|
61
92
|
return int(size)
|
|
62
93
|
|
|
63
94
|
|
|
95
|
+
def calculate_intensity_stats(intensity_image, mask):
|
|
96
|
+
"""
|
|
97
|
+
Calculate intensity statistics for a masked region.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
intensity_image: Raw intensity image
|
|
101
|
+
mask: Boolean mask defining the region
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
dict: Dictionary with mean, median, std, max, min intensity
|
|
105
|
+
"""
|
|
106
|
+
# Get intensity values within the mask
|
|
107
|
+
intensity_values = intensity_image[mask]
|
|
108
|
+
|
|
109
|
+
if len(intensity_values) == 0:
|
|
110
|
+
return {"mean": 0.0, "median": 0.0, "std": 0.0, "max": 0.0, "min": 0.0}
|
|
111
|
+
|
|
112
|
+
stats = {
|
|
113
|
+
"mean": float(np.mean(intensity_values)),
|
|
114
|
+
"median": float(np.median(intensity_values)),
|
|
115
|
+
"std": float(np.std(intensity_values)),
|
|
116
|
+
"max": float(np.max(intensity_values)),
|
|
117
|
+
"min": float(np.min(intensity_values)),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return stats
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def count_c2_positive_for_c3_labels(image_c2, image_c3, mask_roi):
|
|
124
|
+
"""
|
|
125
|
+
Count Channel 2 objects that contain at least one Channel 3 object (label-based).
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
image_c2: Label image of Channel 2 (e.g., nuclei)
|
|
129
|
+
image_c3: Label image of Channel 3 (e.g., Ki67 spots)
|
|
130
|
+
mask_roi: Boolean mask for the ROI from Channel 1
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict: Dictionary with positive/negative counts and percentage
|
|
134
|
+
"""
|
|
135
|
+
# Get all unique Channel 2 objects in the ROI
|
|
136
|
+
c2_in_roi = image_c2 * mask_roi
|
|
137
|
+
c2_labels = np.unique(c2_in_roi)
|
|
138
|
+
c2_labels = c2_labels[c2_labels != 0] # Remove background
|
|
139
|
+
|
|
140
|
+
if len(c2_labels) == 0:
|
|
141
|
+
return {
|
|
142
|
+
"total_c2_objects": 0,
|
|
143
|
+
"c2_positive_for_c3_count": 0,
|
|
144
|
+
"c2_negative_for_c3_count": 0,
|
|
145
|
+
"c2_percent_positive_for_c3": 0.0,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Count how many C2 objects contain at least one C3 object
|
|
149
|
+
positive_count = 0
|
|
150
|
+
for c2_label in c2_labels:
|
|
151
|
+
# Get mask for this specific Channel 2 object
|
|
152
|
+
mask_c2_obj = (image_c2 == c2_label) & mask_roi
|
|
153
|
+
|
|
154
|
+
# Check if any C3 objects overlap with this C2 object
|
|
155
|
+
c3_in_c2 = image_c3[mask_c2_obj]
|
|
156
|
+
c3_labels_in_c2 = np.unique(c3_in_c2[c3_in_c2 != 0])
|
|
157
|
+
|
|
158
|
+
if len(c3_labels_in_c2) > 0:
|
|
159
|
+
positive_count += 1
|
|
160
|
+
|
|
161
|
+
total_count = int(len(c2_labels))
|
|
162
|
+
negative_count = total_count - positive_count
|
|
163
|
+
percent_positive = (
|
|
164
|
+
(positive_count / total_count * 100) if total_count > 0 else 0.0
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"total_c2_objects": total_count,
|
|
169
|
+
"c2_positive_for_c3_count": positive_count,
|
|
170
|
+
"c2_negative_for_c3_count": negative_count,
|
|
171
|
+
"c2_percent_positive_for_c3": percent_positive,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def count_positive_objects(
|
|
176
|
+
image_c2,
|
|
177
|
+
intensity_c3,
|
|
178
|
+
mask_roi,
|
|
179
|
+
threshold_method="percentile",
|
|
180
|
+
threshold_value=75.0,
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Count Channel 2 objects that are positive for Channel 3 signal (intensity-based).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
image_c2: Label image of Channel 2 (e.g., nuclei)
|
|
187
|
+
intensity_c3: Intensity image of Channel 3 (e.g., KI67)
|
|
188
|
+
mask_roi: Boolean mask for the ROI from Channel 1
|
|
189
|
+
threshold_method: 'percentile' or 'absolute'
|
|
190
|
+
threshold_value: Threshold value (0-100 for percentile, or absolute intensity)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
dict: Dictionary with counts and threshold info
|
|
194
|
+
"""
|
|
195
|
+
# Get all unique Channel 2 objects in the ROI
|
|
196
|
+
c2_in_roi = image_c2 * mask_roi
|
|
197
|
+
c2_labels = np.unique(c2_in_roi)
|
|
198
|
+
c2_labels = c2_labels[c2_labels != 0] # Remove background
|
|
199
|
+
|
|
200
|
+
if len(c2_labels) == 0:
|
|
201
|
+
return {
|
|
202
|
+
"total_c2_objects": 0,
|
|
203
|
+
"positive_c2_objects": 0,
|
|
204
|
+
"negative_c2_objects": 0,
|
|
205
|
+
"percent_positive": 0.0,
|
|
206
|
+
"threshold_used": 0.0,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Calculate threshold
|
|
210
|
+
if threshold_method == "percentile":
|
|
211
|
+
# Calculate threshold from all Channel 3 intensity values within ROI where Channel 2 exists
|
|
212
|
+
mask_c2_in_roi = c2_in_roi > 0
|
|
213
|
+
intensity_in_c2 = intensity_c3[mask_c2_in_roi]
|
|
214
|
+
if len(intensity_in_c2) > 0:
|
|
215
|
+
threshold = float(np.percentile(intensity_in_c2, threshold_value))
|
|
216
|
+
else:
|
|
217
|
+
threshold = 0.0
|
|
218
|
+
else: # absolute
|
|
219
|
+
threshold = threshold_value
|
|
220
|
+
|
|
221
|
+
# Count positive objects
|
|
222
|
+
positive_count = 0
|
|
223
|
+
for label_id in c2_labels:
|
|
224
|
+
# Get mask for this specific Channel 2 object
|
|
225
|
+
mask_c2_obj = (image_c2 == label_id) & mask_roi
|
|
226
|
+
|
|
227
|
+
# Get mean intensity of Channel 3 in this Channel 2 object
|
|
228
|
+
intensity_in_obj = intensity_c3[mask_c2_obj]
|
|
229
|
+
if len(intensity_in_obj) > 0:
|
|
230
|
+
mean_intensity = float(np.mean(intensity_in_obj))
|
|
231
|
+
if mean_intensity >= threshold:
|
|
232
|
+
positive_count += 1
|
|
233
|
+
|
|
234
|
+
total_count = int(len(c2_labels))
|
|
235
|
+
negative_count = total_count - positive_count
|
|
236
|
+
percent_positive = (
|
|
237
|
+
(positive_count / total_count * 100) if total_count > 0 else 0.0
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"total_c2_objects": total_count,
|
|
242
|
+
"positive_c2_objects": positive_count,
|
|
243
|
+
"negative_c2_objects": negative_count,
|
|
244
|
+
"percent_positive": percent_positive,
|
|
245
|
+
"threshold_used": threshold,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
64
249
|
def process_single_roi(
|
|
65
250
|
label_id,
|
|
66
251
|
image_c1,
|
|
@@ -68,19 +253,73 @@ def process_single_roi(
|
|
|
68
253
|
image_c3=None,
|
|
69
254
|
get_sizes=False,
|
|
70
255
|
roi_sizes=None,
|
|
256
|
+
channel2_is_labels=True,
|
|
257
|
+
channel3_is_labels=True,
|
|
258
|
+
image_c2_intensity=None,
|
|
259
|
+
image_c3_intensity=None,
|
|
260
|
+
count_positive=False,
|
|
261
|
+
threshold_method="percentile",
|
|
262
|
+
threshold_value=75.0,
|
|
263
|
+
convert_to_instances_c2=False,
|
|
264
|
+
convert_to_instances_c3=False,
|
|
265
|
+
count_c2_positive_for_c3=False,
|
|
71
266
|
):
|
|
72
|
-
"""
|
|
267
|
+
"""
|
|
268
|
+
Process a single ROI for colocalization analysis.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
label_id: Label ID to process
|
|
272
|
+
image_c1: First channel image (ROI labels)
|
|
273
|
+
image_c2: Second channel image (object labels or intensity)
|
|
274
|
+
image_c3: Third channel image (labels or intensity, optional)
|
|
275
|
+
get_sizes: Whether to calculate size statistics
|
|
276
|
+
roi_sizes: Pre-calculated ROI sizes dict
|
|
277
|
+
channel2_is_labels: If True, treat channel 2 as labels; if False, as intensity
|
|
278
|
+
channel3_is_labels: If True, treat channel 3 as labels; if False, as intensity
|
|
279
|
+
image_c2_intensity: Separate intensity image for channel 2 (optional)
|
|
280
|
+
image_c3_intensity: Separate intensity image for channel 3 (optional)
|
|
281
|
+
count_positive: Count positive objects (only applicable when ch2 is labels and ch3 is intensity)
|
|
282
|
+
threshold_method: 'percentile' or 'absolute' for positive counting
|
|
283
|
+
threshold_value: Threshold value for positive counting
|
|
284
|
+
convert_to_instances_c2: If True, convert semantic labels to instance labels for channel 2
|
|
285
|
+
convert_to_instances_c3: If True, convert semantic labels to instance labels for channel 3
|
|
286
|
+
count_c2_positive_for_c3: Count C2 objects containing at least one C3 object (ch2 and ch3 both labels)
|
|
287
|
+
"""
|
|
288
|
+
# Convert semantic labels to instance labels if requested
|
|
289
|
+
if convert_to_instances_c2 and channel2_is_labels and image_c2 is not None:
|
|
290
|
+
image_c2 = convert_semantic_to_instance_labels(image_c2)
|
|
291
|
+
|
|
292
|
+
if convert_to_instances_c3 and channel3_is_labels and image_c3 is not None:
|
|
293
|
+
image_c3 = convert_semantic_to_instance_labels(image_c3)
|
|
294
|
+
|
|
73
295
|
# Create masks once
|
|
74
296
|
mask_roi = image_c1 == label_id
|
|
75
|
-
mask_c2 = image_c2 != 0
|
|
76
|
-
|
|
77
|
-
# Calculate counts
|
|
78
|
-
c2_in_c1_count = count_unique_nonzero(image_c2, mask_roi & mask_c2)
|
|
79
297
|
|
|
80
298
|
# Build the result dictionary
|
|
81
|
-
result = {"label_id": int(label_id)
|
|
299
|
+
result = {"label_id": int(label_id)}
|
|
300
|
+
|
|
301
|
+
# Handle Channel 2 based on whether it's labels or intensity
|
|
302
|
+
if channel2_is_labels:
|
|
303
|
+
mask_c2 = image_c2 != 0
|
|
304
|
+
# Calculate counts
|
|
305
|
+
c2_in_c1_count = count_unique_nonzero(image_c2, mask_roi & mask_c2)
|
|
306
|
+
result["ch2_in_ch1_count"] = c2_in_c1_count
|
|
307
|
+
else:
|
|
308
|
+
# Channel 2 is intensity - calculate intensity statistics
|
|
309
|
+
intensity_img = (
|
|
310
|
+
image_c2_intensity if image_c2_intensity is not None else image_c2
|
|
311
|
+
)
|
|
312
|
+
stats_c2 = calculate_intensity_stats(intensity_img, mask_roi)
|
|
313
|
+
result.update(
|
|
314
|
+
{
|
|
315
|
+
"ch2_in_ch1_mean": stats_c2["mean"],
|
|
316
|
+
"ch2_in_ch1_median": stats_c2["median"],
|
|
317
|
+
"ch2_in_ch1_std": stats_c2["std"],
|
|
318
|
+
"ch2_in_ch1_max": stats_c2["max"],
|
|
319
|
+
}
|
|
320
|
+
)
|
|
82
321
|
|
|
83
|
-
# Add size information if requested
|
|
322
|
+
# Add size information if requested (only for label-based channels)
|
|
84
323
|
if get_sizes:
|
|
85
324
|
if roi_sizes is None:
|
|
86
325
|
roi_sizes = {}
|
|
@@ -89,44 +328,173 @@ def process_single_roi(
|
|
|
89
328
|
roi_sizes[label_id] = area
|
|
90
329
|
|
|
91
330
|
size = roi_sizes.get(int(label_id), 0)
|
|
92
|
-
|
|
331
|
+
result["ch1_size"] = size
|
|
93
332
|
|
|
94
|
-
|
|
333
|
+
if channel2_is_labels:
|
|
334
|
+
c2_in_c1_size = calculate_coloc_size(image_c1, image_c2, label_id)
|
|
335
|
+
result["ch2_in_ch1_size"] = c2_in_c1_size
|
|
95
336
|
|
|
96
337
|
# Handle third channel if present
|
|
97
338
|
if image_c3 is not None:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
339
|
+
if channel3_is_labels:
|
|
340
|
+
# Original behavior: count objects in channel 3
|
|
341
|
+
mask_c3 = image_c3 != 0
|
|
342
|
+
|
|
343
|
+
if channel2_is_labels:
|
|
344
|
+
# Both ch2 and ch3 are labels - original 3-channel label mode
|
|
345
|
+
mask_c2 = image_c2 != 0
|
|
346
|
+
|
|
347
|
+
# Calculate third channel statistics
|
|
348
|
+
c3_in_c2_in_c1_count = count_unique_nonzero(
|
|
349
|
+
image_c3, mask_roi & mask_c2 & mask_c3
|
|
350
|
+
)
|
|
351
|
+
c3_not_in_c2_but_in_c1_count = count_unique_nonzero(
|
|
352
|
+
image_c3, mask_roi & ~mask_c2 & mask_c3
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
result.update(
|
|
356
|
+
{
|
|
357
|
+
"ch3_in_ch2_in_ch1_count": c3_in_c2_in_c1_count,
|
|
358
|
+
"ch3_not_in_ch2_but_in_ch1_count": c3_not_in_c2_but_in_c1_count,
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Count C2 objects positive for C3 if requested
|
|
363
|
+
if count_c2_positive_for_c3:
|
|
364
|
+
positive_counts = count_c2_positive_for_c3_labels(
|
|
365
|
+
image_c2, image_c3, mask_roi
|
|
366
|
+
)
|
|
367
|
+
result.update(
|
|
368
|
+
{
|
|
369
|
+
"c2_in_c1_positive_for_c3_count": positive_counts[
|
|
370
|
+
"c2_positive_for_c3_count"
|
|
371
|
+
],
|
|
372
|
+
"c2_in_c1_negative_for_c3_count": positive_counts[
|
|
373
|
+
"c2_negative_for_c3_count"
|
|
374
|
+
],
|
|
375
|
+
"c2_in_c1_percent_positive_for_c3": positive_counts[
|
|
376
|
+
"c2_percent_positive_for_c3"
|
|
377
|
+
],
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Add size information for third channel if requested
|
|
382
|
+
if get_sizes:
|
|
383
|
+
c3_in_c2_in_c1_size = calculate_coloc_size(
|
|
384
|
+
image_c1,
|
|
385
|
+
image_c2,
|
|
386
|
+
label_id,
|
|
387
|
+
mask_c2=True,
|
|
388
|
+
image_c3=image_c3,
|
|
389
|
+
)
|
|
390
|
+
c3_not_in_c2_but_in_c1_size = calculate_coloc_size(
|
|
391
|
+
image_c1,
|
|
392
|
+
image_c2,
|
|
393
|
+
label_id,
|
|
394
|
+
mask_c2=False,
|
|
395
|
+
image_c3=image_c3,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
result.update(
|
|
399
|
+
{
|
|
400
|
+
"ch3_in_ch2_in_ch1_size": c3_in_c2_in_c1_size,
|
|
401
|
+
"ch3_not_in_ch2_but_in_c1_size": c3_not_in_c2_but_in_c1_size,
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
# Ch2 is intensity, Ch3 is labels - count Ch3 objects in Ch1
|
|
406
|
+
c3_in_c1_count = count_unique_nonzero(
|
|
407
|
+
image_c3, mask_roi & mask_c3
|
|
408
|
+
)
|
|
409
|
+
result["ch3_in_ch1_count"] = c3_in_c1_count
|
|
410
|
+
|
|
411
|
+
if get_sizes:
|
|
412
|
+
c3_in_c1_size = calculate_coloc_size(
|
|
413
|
+
image_c1, image_c3, label_id
|
|
414
|
+
)
|
|
415
|
+
result["ch3_in_ch1_size"] = c3_in_c1_size
|
|
416
|
+
else:
|
|
417
|
+
# Channel 3 is intensity
|
|
418
|
+
intensity_img = (
|
|
419
|
+
image_c3_intensity
|
|
420
|
+
if image_c3_intensity is not None
|
|
421
|
+
else image_c3
|
|
122
422
|
)
|
|
123
423
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
424
|
+
if channel2_is_labels:
|
|
425
|
+
# Ch2 is labels, Ch3 is intensity - original intensity mode
|
|
426
|
+
mask_c2 = image_c2 != 0
|
|
427
|
+
|
|
428
|
+
# Calculate intensity where c2 is present in c1
|
|
429
|
+
mask_c2_in_c1 = mask_roi & mask_c2
|
|
430
|
+
stats_c2_in_c1 = calculate_intensity_stats(
|
|
431
|
+
intensity_img, mask_c2_in_c1
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Calculate intensity where c2 is NOT present in c1
|
|
435
|
+
mask_not_c2_in_c1 = mask_roi & ~mask_c2
|
|
436
|
+
stats_not_c2_in_c1 = calculate_intensity_stats(
|
|
437
|
+
intensity_img, mask_not_c2_in_c1
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Add intensity statistics to result
|
|
441
|
+
result.update(
|
|
442
|
+
{
|
|
443
|
+
"ch3_in_ch2_in_ch1_mean": stats_c2_in_c1["mean"],
|
|
444
|
+
"ch3_in_ch2_in_ch1_median": stats_c2_in_c1["median"],
|
|
445
|
+
"ch3_in_ch2_in_ch1_std": stats_c2_in_c1["std"],
|
|
446
|
+
"ch3_in_ch2_in_ch1_max": stats_c2_in_c1["max"],
|
|
447
|
+
"ch3_not_in_ch2_but_in_ch1_mean": stats_not_c2_in_c1[
|
|
448
|
+
"mean"
|
|
449
|
+
],
|
|
450
|
+
"ch3_not_in_ch2_but_in_ch1_median": stats_not_c2_in_c1[
|
|
451
|
+
"median"
|
|
452
|
+
],
|
|
453
|
+
"ch3_not_in_ch2_but_in_ch1_std": stats_not_c2_in_c1[
|
|
454
|
+
"std"
|
|
455
|
+
],
|
|
456
|
+
"ch3_not_in_ch2_but_in_ch1_max": stats_not_c2_in_c1[
|
|
457
|
+
"max"
|
|
458
|
+
],
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Count positive Channel 2 objects if requested
|
|
463
|
+
if count_positive:
|
|
464
|
+
positive_counts = count_positive_objects(
|
|
465
|
+
image_c2,
|
|
466
|
+
intensity_img,
|
|
467
|
+
mask_roi,
|
|
468
|
+
threshold_method,
|
|
469
|
+
threshold_value,
|
|
470
|
+
)
|
|
471
|
+
result.update(
|
|
472
|
+
{
|
|
473
|
+
"ch2_in_ch1_positive_for_ch3_count": positive_counts[
|
|
474
|
+
"positive_c2_objects"
|
|
475
|
+
],
|
|
476
|
+
"ch2_in_ch1_negative_for_ch3_count": positive_counts[
|
|
477
|
+
"negative_c2_objects"
|
|
478
|
+
],
|
|
479
|
+
"ch2_in_ch1_percent_positive_for_ch3": positive_counts[
|
|
480
|
+
"percent_positive"
|
|
481
|
+
],
|
|
482
|
+
"ch3_threshold_used": positive_counts[
|
|
483
|
+
"threshold_used"
|
|
484
|
+
],
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
# Both Ch2 and Ch3 are intensity - just add Ch3 stats to Ch1 ROIs
|
|
489
|
+
stats_c3 = calculate_intensity_stats(intensity_img, mask_roi)
|
|
490
|
+
result.update(
|
|
491
|
+
{
|
|
492
|
+
"ch3_in_ch1_mean": stats_c3["mean"],
|
|
493
|
+
"ch3_in_ch1_median": stats_c3["median"],
|
|
494
|
+
"ch3_in_ch1_std": stats_c3["std"],
|
|
495
|
+
"ch3_in_ch1_max": stats_c3["max"],
|
|
496
|
+
}
|
|
497
|
+
)
|
|
130
498
|
|
|
131
499
|
return result
|
|
132
500
|
|
|
@@ -145,27 +513,70 @@ def process_single_roi(
|
|
|
145
513
|
# "type": str,
|
|
146
514
|
# "default": "median",
|
|
147
515
|
# "description": "Method for size calculation (median or sum)",
|
|
516
|
+
# "channel2_is_labels": {
|
|
517
|
+
# "type": bool,
|
|
518
|
+
# "default": True,
|
|
519
|
+
# "description": "Treat channel 2 as labels (True) or intensity (False)",
|
|
520
|
+
# },
|
|
521
|
+
# "channel3_is_labels": {
|
|
522
|
+
# "type": bool,
|
|
523
|
+
# "default": True,
|
|
524
|
+
# "description": "Treat channel 3 as labels (True) or intensity (False)",
|
|
525
|
+
# },
|
|
526
|
+
# "count_positive": {
|
|
527
|
+
# "type": bool,
|
|
528
|
+
# "default": False,
|
|
529
|
+
# "description": "Count positive objects (when one channel is labels and another is intensity)",
|
|
530
|
+
# },
|
|
531
|
+
# "threshold_method": {
|
|
532
|
+
# "type": str,
|
|
533
|
+
# "default": "percentile",
|
|
534
|
+
# "description": "Threshold method: 'percentile' or 'absolute'",
|
|
535
|
+
# },
|
|
536
|
+
# "threshold_value": {
|
|
537
|
+
# "type": float,
|
|
538
|
+
# "default": 75.0,
|
|
539
|
+
# "description": "Threshold value for positive counting",
|
|
148
540
|
# },
|
|
149
541
|
# },
|
|
150
542
|
# )
|
|
151
|
-
def roi_colocalization(
|
|
543
|
+
def roi_colocalization(
|
|
544
|
+
image,
|
|
545
|
+
get_sizes=False,
|
|
546
|
+
size_method="median",
|
|
547
|
+
channel2_is_labels=True,
|
|
548
|
+
channel3_is_labels=True,
|
|
549
|
+
count_positive=False,
|
|
550
|
+
threshold_method="percentile",
|
|
551
|
+
threshold_value=75.0,
|
|
552
|
+
):
|
|
152
553
|
"""
|
|
153
|
-
Calculate colocalization between channels for a multi-channel label image.
|
|
554
|
+
Calculate colocalization between channels for a multi-channel label/intensity image.
|
|
154
555
|
|
|
155
556
|
This function takes a multi-channel image where each channel contains
|
|
156
|
-
labeled objects (segmentation masks). It analyzes how
|
|
157
|
-
overlap with objects in the other channels, and
|
|
158
|
-
about their colocalization relationships.
|
|
557
|
+
labeled objects (segmentation masks) or intensity values. It analyzes how
|
|
558
|
+
objects in one channel overlap with objects in the other channels, and
|
|
559
|
+
returns detailed statistics about their colocalization relationships.
|
|
159
560
|
|
|
160
561
|
Parameters:
|
|
161
562
|
-----------
|
|
162
563
|
image : numpy.ndarray
|
|
163
564
|
Input image array, should have shape corresponding to a multichannel
|
|
164
|
-
|
|
565
|
+
image (e.g., [n_channels, height, width]).
|
|
165
566
|
get_sizes : bool, optional
|
|
166
|
-
Whether to calculate size statistics for overlapping regions.
|
|
567
|
+
Whether to calculate size statistics for overlapping regions (only for label channels).
|
|
167
568
|
size_method : str, optional
|
|
168
569
|
Method for calculating size statistics ('median' or 'sum').
|
|
570
|
+
channel2_is_labels : bool, optional
|
|
571
|
+
If True, treat channel 2 as labeled objects. If False, treat as intensity image.
|
|
572
|
+
channel3_is_labels : bool, optional
|
|
573
|
+
If True, treat channel 3 as labeled objects. If False, treat as intensity image.
|
|
574
|
+
count_positive : bool, optional
|
|
575
|
+
Count Channel 2 objects positive for Channel 3 signal (only when channel3_is_labels=False).
|
|
576
|
+
threshold_method : str, optional
|
|
577
|
+
Method for positive threshold: 'percentile' or 'absolute'.
|
|
578
|
+
threshold_value : float, optional
|
|
579
|
+
Threshold value (0-100 for percentile, or absolute intensity value).
|
|
169
580
|
|
|
170
581
|
Returns:
|
|
171
582
|
--------
|
|
@@ -191,6 +602,16 @@ def roi_colocalization(image, get_sizes=False, size_method="median"):
|
|
|
191
602
|
image_c1, image_c2 = channels[:2]
|
|
192
603
|
image_c3 = channels[2] if n_channels > 2 else None
|
|
193
604
|
|
|
605
|
+
# Handle intensity images for channel 2 and 3
|
|
606
|
+
image_c2_intensity = None
|
|
607
|
+
image_c3_intensity = None
|
|
608
|
+
|
|
609
|
+
if not channel2_is_labels:
|
|
610
|
+
image_c2_intensity = image_c2
|
|
611
|
+
|
|
612
|
+
if image_c3 is not None and not channel3_is_labels:
|
|
613
|
+
image_c3_intensity = image_c3
|
|
614
|
+
|
|
194
615
|
# Get unique label IDs in image_c1
|
|
195
616
|
label_ids = get_nonzero_labels(image_c1)
|
|
196
617
|
|
|
@@ -206,7 +627,19 @@ def roi_colocalization(image, get_sizes=False, size_method="median"):
|
|
|
206
627
|
|
|
207
628
|
for label_id in label_ids:
|
|
208
629
|
result = process_single_roi(
|
|
209
|
-
label_id,
|
|
630
|
+
label_id,
|
|
631
|
+
image_c1,
|
|
632
|
+
image_c2,
|
|
633
|
+
image_c3,
|
|
634
|
+
get_sizes,
|
|
635
|
+
roi_sizes,
|
|
636
|
+
channel2_is_labels,
|
|
637
|
+
channel3_is_labels,
|
|
638
|
+
image_c2_intensity,
|
|
639
|
+
image_c3_intensity,
|
|
640
|
+
count_positive,
|
|
641
|
+
threshold_method,
|
|
642
|
+
threshold_value,
|
|
210
643
|
)
|
|
211
644
|
results.append(result)
|
|
212
645
|
|
|
@@ -224,17 +657,41 @@ def roi_colocalization(image, get_sizes=False, size_method="median"):
|
|
|
224
657
|
# Fill first channel with original labels
|
|
225
658
|
output[0] = image_c1
|
|
226
659
|
|
|
227
|
-
# Fill second channel
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
660
|
+
# Fill second channel based on whether it's labels or intensity
|
|
661
|
+
if channel2_is_labels:
|
|
662
|
+
# Fill with ch1 labels where ch2 overlaps
|
|
663
|
+
for label_id in label_ids:
|
|
664
|
+
mask = (image_c1 == label_id) & (image_c2 != 0)
|
|
665
|
+
if np.any(mask):
|
|
666
|
+
output[1][mask] = label_id
|
|
667
|
+
else:
|
|
668
|
+
# For intensity-based channel 2, show the intensity values within ch1 ROIs
|
|
669
|
+
for label_id in label_ids:
|
|
670
|
+
mask = image_c1 == label_id
|
|
671
|
+
if np.any(mask):
|
|
672
|
+
output[1][mask] = image_c2[mask]
|
|
232
673
|
|
|
233
674
|
# Fill third channel with ch1 labels where ch3 overlaps (if applicable)
|
|
234
675
|
if image_c3 is not None and output_channels > 2:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if
|
|
238
|
-
|
|
676
|
+
if channel3_is_labels:
|
|
677
|
+
# For label-based channel 3, show overlap
|
|
678
|
+
if channel2_is_labels:
|
|
679
|
+
# Ch2 is labels - show ch3 overlap with ch2 in ch1
|
|
680
|
+
for label_id in label_ids:
|
|
681
|
+
mask = (image_c1 == label_id) & (image_c3 != 0)
|
|
682
|
+
if np.any(mask):
|
|
683
|
+
output[2][mask] = label_id
|
|
684
|
+
else:
|
|
685
|
+
# Ch2 is intensity - just show ch3 overlap with ch1
|
|
686
|
+
for label_id in label_ids:
|
|
687
|
+
mask = (image_c1 == label_id) & (image_c3 != 0)
|
|
688
|
+
if np.any(mask):
|
|
689
|
+
output[2][mask] = label_id
|
|
690
|
+
else:
|
|
691
|
+
# For intensity-based channel 3, show the intensity values
|
|
692
|
+
for label_id in label_ids:
|
|
693
|
+
mask = image_c1 == label_id
|
|
694
|
+
if np.any(mask):
|
|
695
|
+
output[2][mask] = image_c3[mask]
|
|
239
696
|
|
|
240
697
|
return output
|