-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin_record_intensities_using_preexisting_segmentation.py
More file actions
595 lines (486 loc) · 30.1 KB
/
Copy pathplugin_record_intensities_using_preexisting_segmentation.py
File metadata and controls
595 lines (486 loc) · 30.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
import math
import random
from abc import ABC, abstractmethod
from enum import Enum
from functools import partial
from typing import Any, Callable
import matplotlib.cm
import numpy
import scipy
import skimage.measure
from matplotlib.colors import Colormap, ListedColormap
from numpy import ndarray
from organoid_tracker.core import UserError, TimePoint, Name
from organoid_tracker.core.experiment import Experiment
from organoid_tracker.core.image_loader import ImageChannel
from organoid_tracker.core.position import Position
from organoid_tracker.core.position_collection import PositionCollection
from organoid_tracker.core.resolution import ImageResolution
from organoid_tracker.gui import dialog, worker_job, option_choose_dialog
from organoid_tracker.gui.gui_experiment import SingleGuiTab
from organoid_tracker.gui.window import Window
from organoid_tracker.gui.worker_job import WorkerJob
from organoid_tracker.position_analysis import intensity_calculator
from organoid_tracker.visualizer import activate
from organoid_tracker.visualizer.exitable_image_visualizer import ExitableImageVisualizer
class _ExcludeBorderMode(Enum):
OFF = 0
EXCLUDE_BORDER_XY_ONLY = 1
EXCLUDE_BORDER_ALL = 2
def get_display_name(self) -> str:
if self == _ExcludeBorderMode.OFF:
return "No exclusion"
elif self == _ExcludeBorderMode.EXCLUDE_BORDER_XY_ONLY:
return "Exclude objects touching XY borders"
elif self == _ExcludeBorderMode.EXCLUDE_BORDER_ALL:
return "Exclude objects touching any border"
else:
raise ValueError("Unknown exclusion mode")
def exclude_border_objects(self, array: ndarray) -> ndarray:
"""Returns an array where the objects touching the borders have been removed (depending on the mode). The input
array is not modified."""
if self == _ExcludeBorderMode.OFF:
return array # Nothing to do
elif self == _ExcludeBorderMode.EXCLUDE_BORDER_ALL:
if array.shape[0] == 1:
# Work in 2D, otherwise all labels will be removed
return skimage.segmentation.clear_border(array[0:])[numpy.newaxis, :, :]
return skimage.segmentation.clear_border(array)
elif self == _ExcludeBorderMode.EXCLUDE_BORDER_XY_ONLY:
# Create a mask along the XY borders only
mask = numpy.full_like(array, fill_value=True, dtype=bool)
mask[:, 0, :] = False
mask[:, -1, :] = False
mask[:, :, 0] = False
mask[:, :, -1] = False
return skimage.segmentation.clear_border(array, mask=mask)
else:
raise ValueError("Unknown exclusion mode")
def _get_ellipsoid_structure(resolution: ImageResolution, labeled_image_shape_zyx: tuple[int, int, int], size_um: float) -> ndarray:
"""Creates a 3D ellipsoid structuring element with the given size in micrometers, taking into account that the
Z-resolution may be different than the XY resolution."""
size_px_z = int(round(size_um / resolution.pixel_size_z_um)) if resolution.pixel_size_z_um > 0 else 10
size_px_y = int(round(size_um / resolution.pixel_size_y_um))
size_px_x = int(round(size_um / resolution.pixel_size_x_um))
structuring_element = numpy.ones((2 * size_px_z + 1, 2 * size_px_y + 1, 2 * size_px_x + 1), dtype=bool)
zz, yy, xx = numpy.ogrid[-size_px_z:size_px_z + 1, -size_px_y:size_px_y + 1, -size_px_x:size_px_x + 1]
ellipsoid = (zz / (size_px_z + 0.5)) ** 2 + (yy / (size_px_y + 0.5)) ** 2 + (xx / (size_px_x + 0.5)) ** 2 <= 1
structuring_element &= ellipsoid
if labeled_image_shape_zyx[0] == 1:
# 2D image, make sure structuring element is also 2D
structuring_element = structuring_element[size_px_z, :, :][numpy.newaxis, :, :]
return structuring_element
def _expand_slice(original_slice: tuple[slice, slice, slice], image_shape: tuple[int, int, int],
structuring_element: ndarray) -> tuple[slice, slice, slice]:
"""Expands the given slice by the size of half the structuring element, ensuring we don't go out of bounds.
Will not expand more than the image shape."""
z_start, z_stop = original_slice[0].start, original_slice[0].stop
y_start, y_stop = original_slice[1].start, original_slice[1].stop
x_start, x_stop = original_slice[2].start, original_slice[2].stop
expand_z = structuring_element.shape[0] // 2
expand_y = structuring_element.shape[1] // 2
expand_x = structuring_element.shape[2] // 2
new_z_start = max(0, z_start - expand_z)
new_z_stop = min(image_shape[0], z_stop + expand_z)
new_y_start = max(0, y_start - expand_y)
new_y_stop = min(image_shape[1], y_stop + expand_y)
new_x_start = max(0, x_start - expand_x)
new_x_stop = min(image_shape[2], x_stop + expand_x)
return slice(new_z_start, new_z_stop), slice(new_y_start, new_y_stop), slice(new_x_start, new_x_stop)
class _MaskProcessingMode(ABC):
@abstractmethod
def get_name(self) -> str:
"""Gets the name of this measurement mode."""
raise NotImplementedError()
@abstractmethod
def get_size_question(self) -> str | None:
"""The process_mask method requires a size parameter. This method returns the question that we should ask the
user so that the user knows what value to choose for that parameter. Like "By how many micrometers should we
enlarge the mask?". Returns None if a size parameter is not required (in which case you can pass 0 to
process_mask)."""
raise NotImplementedError()
@abstractmethod
def process_mask_3d(self, resolution: ImageResolution, labeled_image_3d: ndarray, size_um: float) -> ndarray:
"""Processes a 3D labeled image according to this mode. The labeled_image_3d array must not be modified."""
raise NotImplementedError()
def process_mask_2d(self, resolution: ImageResolution, labeled_image_getter: Callable[[int], ndarray | None],
size_um: float, image_z: int) -> ndarray | None:
"""Processes a 2D labeled image at the given Z index. Depending on the mode, this may involve processing
nearby Z slices as well. labeled_image_getter is a function that will be called with an image Z (so without
image offsets) to get the labeled image for that Z."""
# In this default implementation, we assume we need to process nearby slices as well, as a slice may get
# dilated/erased because it's near the bottom or top of a 3D object
z_size_extra = int(math.ceil(size_um / resolution.pixel_size_z_um)) if resolution.pixel_size_z_um > 0 else 0
slice_of_interest = z_size_extra
image_2d_stack = [labeled_image_getter(image_z + dz) for dz in range(-z_size_extra, z_size_extra + 1)]
# Remove None slices at the start and end (in case we went out of bounds)
while len(image_2d_stack) > 0 and image_2d_stack[0] is None:
image_2d_stack.pop(0)
slice_of_interest -= 1
while len(image_2d_stack) > 0 and image_2d_stack[-1] is None:
image_2d_stack.pop()
if slice_of_interest < 0 or slice_of_interest >= len(image_2d_stack):
return None # No image here
# Process our little 3D stack, return the appropriate slice
labeled_image_3d = numpy.stack(image_2d_stack, axis=0)
processed_3d = self.process_mask_3d(resolution, labeled_image_3d, size_um)
return processed_3d[slice_of_interest]
class _ModeInside(_MaskProcessingMode):
def get_name(self) -> str:
return "Inside masks (default)"
def get_size_question(self) -> str | None:
return None
def process_mask_3d(self, resolution: ImageResolution, labeled_image_3d: ndarray, size_um: float) -> ndarray:
return labeled_image_3d # Don't change
def process_mask_2d(self, resolution: ImageResolution, labeled_image_getter: Callable[[int], ndarray | None],
size_um: float, image_z: int) -> ndarray | None:
# No need to process nearby slices
return labeled_image_getter(image_z)
class _ModeShrunken(_MaskProcessingMode):
def get_name(self) -> str:
return "In shrunken masks"
def get_size_question(self) -> str | None:
return "By how many micrometers should we shrink the mask (in 3D)?"
def process_mask_3d(self, resolution: ImageResolution, labeled_image_3d: ndarray, size_um: float) -> ndarray:
structuring_element = _get_ellipsoid_structure(resolution, labeled_image_3d.shape, size_um)
output_array = numpy.zeros_like(labeled_image_3d)
for region in skimage.measure.regionprops(labeled_image_3d):
mask = region.image
eroded_mask = scipy.ndimage.binary_erosion(mask, structure=structuring_element)
output_array[region.slice][eroded_mask] = region.label # Set eroded
return output_array
class _ModeEnlarged(_MaskProcessingMode):
def get_name(self) -> str:
return "In enlarged masks"
def get_size_question(self) -> str | None:
return "By how many micrometers should we enlarge each mask (in 3D)?"
def process_mask_3d(self, resolution: ImageResolution, labeled_image_3d: ndarray, size_um: float) -> ndarray:
structuring_element = _get_ellipsoid_structure(resolution, labeled_image_3d.shape, size_um)
output_array = numpy.zeros_like(labeled_image_3d)
for region in skimage.measure.regionprops(labeled_image_3d):
expanded_slice = _expand_slice(region.slice, labeled_image_3d.shape, structuring_element)
mask = labeled_image_3d[expanded_slice] == region.label
dilated_mask = scipy.ndimage.binary_dilation(mask, structure=structuring_element)
output_array[expanded_slice][dilated_mask] = region.label # Set dilated
# Ensure the original labels remain, and are not overwritten by neighboring dilations
output_array[labeled_image_3d != 0] = labeled_image_3d[labeled_image_3d != 0]
return output_array
class _ModeOutside(_MaskProcessingMode):
def get_name(self) -> str:
return "At the borders, on the outside"
def get_size_question(self) -> str | None:
return "How many micrometers should we measure outside the mask?"
def process_mask_3d(self, resolution: ImageResolution, labeled_image_3d: ndarray, size_um: float) -> ndarray:
structuring_element = _get_ellipsoid_structure(resolution, labeled_image_3d.shape, size_um)
# Fill the output array with dilated masks
output_array = numpy.zeros_like(labeled_image_3d)
for region in skimage.measure.regionprops(labeled_image_3d):
expanded_slice = _expand_slice(region.slice, labeled_image_3d.shape, structuring_element)
mask = labeled_image_3d[expanded_slice] == region.label
dilated_mask = scipy.ndimage.binary_dilation(mask, structure=structuring_element)
output_array[expanded_slice][dilated_mask] = region.label # Set dilated
# Remove original masks, so that only the dilated part remains
output_array[labeled_image_3d != 0] = 0
return output_array
_PROCESSING_MODES = [_ModeInside(), _ModeOutside(), _ModeShrunken(), _ModeEnlarged()]
_DEFAULT_PROCESSING_MODE = _ModeInside()
def get_menu_items(window: Window) -> dict[str, Any]:
return {
"Intensity//Record-Record intensities//Record-Record using pre-existing segmentation...": lambda: _view_intensities(window)
}
def _view_intensities(window: Window):
activate(_PreexistingSegmentationVisualizer(window))
def _by_label(region_props: list["skimage.measure._regionprops.RegionProperties"]
) -> dict[int, "skimage.measure._regionprops.RegionProperties"]:
return_value = dict()
for region in region_props:
return_value[region.label] = region
return return_value
class _AddMissingPositionsTask(WorkerJob):
_segmentation_channel: ImageChannel
def __init__(self, segmentation_channel: ImageChannel):
self._segmentation_channel = segmentation_channel
def copy_experiment(self, experiment: Experiment) -> Experiment:
return experiment.copy_selected(images=True, positions=True)
def gather_data(self, experiment_copy: Experiment) -> PositionCollection:
if experiment_copy.positions.has_positions():
return experiment_copy.positions # Nothing to do
positions = PositionCollection()
for time_point in self.reporting_progress(experiment_copy.images.time_points()):
segmentation_image = experiment_copy.images.get_image(time_point, self._segmentation_channel)
if segmentation_image is None:
continue # No image here
offset = experiment_copy.images.offsets.of_time_point(time_point)
for region in skimage.measure.regionprops(segmentation_image.array):
z_center, y_center, x_center = region.centroid
position = Position(x=x_center + offset.x, y=y_center + offset.y, z=z_center + offset.z,
time_point=time_point)
positions.add(position)
return positions
def use_data(self, tab: SingleGuiTab, data: PositionCollection):
tab.experiment.positions = data
tab.undo_redo.mark_unsaved_changes()
def on_finished(self, results: Any):
dialog.popup_message("Positions created", "Positions have been created from the segmentation."
"\n\nYou can now proceed to record intensities.")
class _RecordIntensitiesJob(WorkerJob):
"""Records the intensities of all positions."""
_segmentation_channel: ImageChannel
_measurement_channel_1: ImageChannel
_mask_processing_mode: _MaskProcessingMode
_mask_processing_size_um: float
_intensity_key: str
_border_exclusion_mode: _ExcludeBorderMode
def __init__(self, segmentation_channel: ImageChannel, measurement_channel_1: ImageChannel,
*, mask_processing_mode: _MaskProcessingMode,
mask_processing_size_um: float, intensity_key: str, border_exclusion_mode: _ExcludeBorderMode):
self._segmentation_channel = segmentation_channel
self._measurement_channel_1 = measurement_channel_1
self._mask_processing_mode = mask_processing_mode
self._mask_processing_size_um = mask_processing_size_um
self._intensity_key = intensity_key
self._border_exclusion_mode = border_exclusion_mode
def copy_experiment(self, experiment: Experiment) -> Experiment:
return experiment.copy_selected(images=True, positions=True)
def gather_data(self, experiment_copy: Experiment) -> tuple[dict[Position, float], dict[Position, int]]:
intensities = dict()
volumes_px3 = dict()
for time_point in self.reporting_progress(experiment_copy.positions.time_points()):
print(f"Working on time point {time_point.time_point_number()}...")
positions = list(experiment_copy.positions.of_time_point(time_point))
if len(positions) == 0:
continue # Skip this time point
# Load images
label_image = experiment_copy.images.get_image(time_point, self._segmentation_channel)
measurement_image = experiment_copy.images.get_image(time_point, self._measurement_channel_1)
if label_image is None or measurement_image is None:
continue # Skip this time point, an image is missing
# Exclude border-touching objects if needed
processed_labels = self._border_exclusion_mode.exclude_border_objects(label_image.array)
# Calculate intensities
resolution = experiment_copy.images.resolution()
processed_labels = self._mask_processing_mode.process_mask_3d(resolution, processed_labels, self._mask_processing_size_um)
props_by_label = _by_label(skimage.measure.regionprops(processed_labels))
for position in positions:
index = label_image.value_at(position)
if index == 0:
continue
props = props_by_label.get(index)
if props is None:
continue
intensity = numpy.sum(measurement_image.array[props.slice] * props.image)
intensities[position] = float(intensity)
volumes_px3[position] = props.area
return intensities, volumes_px3
def use_data(self, tab: SingleGuiTab, data: tuple[dict[Position, float], dict[Position, int]]):
intensities, volume_px3 = data
intensity_calculator.set_raw_intensities(tab.experiment, intensities, volume_px3,
intensity_key=self._intensity_key)
tab.undo_redo.mark_unsaved_changes()
def on_finished(self, results: Any):
dialog.popup_message("Intensities recorded", "All intensities have been recorded.\n\n"
"Your next step is likely to set a normalization. This can be\n"
"done from the Intensity menu in the main screen of the program.")
class _PreexistingSegmentationVisualizer(ExitableImageVisualizer):
"""First, specify the segmentation channel (containing a pre-segmented image) and the measurement channel in the
Parameters menu. Then, use Edit -> Record intensities to record the average intensities.
If you don't have pre-segmented images loaded yet, exit this view and use Edit -> Append image channel.
"""
_segmented_channel: ImageChannel | None = None
_measurement_channel: ImageChannel | None = None
_intensity_key: str = intensity_calculator.DEFAULT_INTENSITY_KEY
_label_colormap: Colormap
_mask_processing_mode: _MaskProcessingMode = _DEFAULT_PROCESSING_MODE
_mask_processing_size_um: int = 1.0
_exclude_border_mode: _ExcludeBorderMode = _ExcludeBorderMode.OFF
def __init__(self, window: Window):
super().__init__(window)
self._display_settings.max_intensity_projection = False
# Initialize or random colormap
source_colormap: Colormap = matplotlib.cm.jet
samples = [source_colormap(sample_pos / 1000) for sample_pos in range(1000)]
random.Random("fixed seed to ensure same colors").shuffle(samples)
samples[0] = (0, 0, 0, 0) # Force background to black
samples[1] = (0, 0, 0, 0) # Force first label to black too, this is also background
self._label_colormap = ListedColormap(samples)
def get_extra_menu_options(self) -> dict[str, Any]:
options = {
**super().get_extra_menu_options(),
"Edit//Channels-Record intensities...": self._record_intensities,
"Parameters//Channel-Set measurement channel...": self._set_measurement_channel_one,
"Parameters//Channel-Set segmented channel...": self._set_segmented_channel,
"Parameters//Other-Set storage key...": self._set_intensity_key,
"Parameters//Other-Set border exclusion mode...": self._set_border_exclusion_mode
}
for mode in _PROCESSING_MODES:
options["Parameters//Other-Set measurement location//" + mode.get_name()] =\
partial(self._set_processing_mode, mode)
if self._find_missing_positions_experiment() is not None:
options["Edit//Create positions from segmentation..."] = self._add_positions_from_segmentation
return options
def _add_positions_from_segmentation(self):
if self._segmented_channel is None:
dialog.popup_message("Segmentation channel not set",
"Please set a segmentation channel in the Parameters menu first.")
return
if not dialog.popup_message_cancellable("Missing positions",
"This action will add the centroids of the segmented objects as"
" positions for any experiment that currently has no positions. No links over time will be created."):
return
worker_job.submit_job(self._window, _AddMissingPositionsTask(self._segmented_channel))
self.update_status("Started creating positions from segmentation...")
def _set_processing_mode(self, mode: _MaskProcessingMode):
"""Changes self._mask_processing_size and self._mask_processing_mode"""
new_size = 0
question = mode.get_size_question()
if question is not None:
new_size = dialog.prompt_float("Set size", question, minimum=0, maximum=1000,
default=self._mask_processing_size_um)
if new_size is None:
return
self._mask_processing_size_um = new_size
self._mask_processing_mode = mode
self.refresh_data()
self._window.set_status("Set the mask processing mode to \"" + mode.get_name() + "\".")
def _set_intensity_key(self):
"""Prompts the user for a new intensity key."""
new_key = dialog.prompt_str("Storage key",
"Under what key should the intensities be stored?"
"\nYou can choose a different value than the default if you want"
" to maintain different sets of intensities.",
default=self._intensity_key)
if new_key is not None and len(new_key) > 0:
self._intensity_key = new_key
def _set_segmented_channel(self):
"""Prompts the user for a new value of self._segmentation_channel."""
current_channel = self._segmented_channel if self._segmented_channel is not None else self._display_settings.image_channel
channel_count = len(self._find_available_channels())
new_channel_index = dialog.prompt_int("Select a channel", f"What channel do you want to use"
f" (1-{channel_count}, inclusive)?", minimum=1,
maximum=channel_count,
default=current_channel.index_one)
if new_channel_index is not None:
self._segmented_channel = ImageChannel(index_one=new_channel_index)
self.refresh_data()
def _set_measurement_channel_one(self):
"""Prompts the user for a new value of self._channel1."""
current_channel = self._measurement_channel if self._measurement_channel is not None else self._display_settings.image_channel
channel_count = len(self._find_available_channels())
new_channel_index = dialog.prompt_int("Select a channel", f"What channel do you want to use"
f" (1-{channel_count}, inclusive)?", minimum=1,
maximum=channel_count,
default=current_channel.index_one)
if new_channel_index is not None:
self._measurement_channel = ImageChannel(index_zero=new_channel_index - 1)
self.refresh_data()
def _set_border_exclusion_mode(self):
"""Prompts the user for a new value of self._exclude_border_mode."""
current_mode = self._exclude_border_mode
options = list(_ExcludeBorderMode)
option_names = list()
for option in options:
if option == current_mode:
option_names.append(option.get_display_name() + " (current)")
else:
option_names.append(option.get_display_name())
new_index = option_choose_dialog.prompt_list("Border exclusion mode",
f"Select the border exclusion mode. For 2D images, both modes are equivalent.",
"Mode:", option_names)
if new_index is not None:
self._exclude_border_mode = options[new_index]
self.refresh_data()
self.update_status("Set the border exclusion mode to \"" + options[new_index].get_display_name() + "\".")
def _find_available_channels(self) -> set[ImageChannel]:
"""Finds all channels that are available in all open experiments."""
channels = set()
for experiment in self._window.get_active_experiments():
for channel in experiment.images.get_channels():
channels.add(channel)
return channels
def _intensity_key_already_exists(self, key: str) -> bool:
for experiment in self._window.get_active_experiments():
if experiment.positions.has_position_data_with_name(key):
return True
return False
def _record_intensities(self):
channels = self._find_available_channels()
if self._segmented_channel is None or self._segmented_channel not in channels:
raise UserError("Invalid segmentation channel", "Please set a segmentation channel in the Parameters menu.")
if self._measurement_channel is None or self._measurement_channel not in channels:
raise UserError("Invalid first channel", "Please set a measurement channel to measure in"
" using the Parameters menu.")
missing_positions_experiment_name = self._find_missing_positions_experiment()
if missing_positions_experiment_name is not None:
raise UserError("No positions", f"The experiment \"{missing_positions_experiment_name}\" has no"
f" tracking data. You can use `Edit -> Create positions from segmentation...` to create"
f" positions first.")
if self._intensity_key_already_exists(self._intensity_key):
if not dialog.prompt_confirmation("Intensities", "Warning: previous intensities stored under the key "
"\""+self._intensity_key+"\" will be overwritten.\n\n"
"This cannot be undone. Do you want to continue?\n\n"
"If you press Cancel, you can go back and choose a"
" different key in the Parameters menu."):
return
worker_job.submit_job(self._window,
_RecordIntensitiesJob(self._segmented_channel, self._measurement_channel,
intensity_key=self._intensity_key, mask_processing_mode=self._mask_processing_mode,
mask_processing_size_um=self._mask_processing_size_um,
border_exclusion_mode=self._exclude_border_mode))
self.update_status("Started recording all intensities...")
def should_show_image_reconstruction(self) -> bool:
if self._segmented_channel is None:
return False # Nothing to draw
if self._display_settings.image_channel not in {self._measurement_channel, self._segmented_channel}:
return False # Nothing to draw for this channel
return True
def reconstruct_image(self, time_point: TimePoint, z: int, rgb_canvas_2d: ndarray):
"""Draws the labels in color to the rgb image."""
if self._segmented_channel is None:
return # Nothing to draw
if self._display_settings.image_channel not in {self._measurement_channel, self._segmented_channel}:
return # Nothing to draw for this channel
if self._segmented_channel == self._display_settings.image_channel:
# Avoid drawing on top of the same image
rgb_canvas_2d[:, :, 0:3] = 0
if rgb_canvas_2d.shape[-1] == 4:
rgb_canvas_2d[:, :, 3] = 1 # Also erase alpha channel
resolution = self._experiment.images.resolution()
offset_z = self._experiment.images.offsets.of_time_point(time_point).z
image_z_of_interest = int(round(z + offset_z))
def get_image(requested_image_z: int) -> ndarray:
requested_z = int(round(requested_image_z - offset_z))
return self._experiment.images.get_image_slice_2d(time_point, self._segmented_channel, requested_z)
labels = self._mask_processing_mode.process_mask_2d(resolution, get_image, self._mask_processing_size_um, image_z_of_interest)
if labels is None:
return # No image here
colored: ndarray = self._label_colormap(labels.flatten())
colored = colored.reshape((rgb_canvas_2d.shape[0], rgb_canvas_2d.shape[1], 4))
rgb_canvas_2d[:, :, :] += colored[:, :, 0:3]
rgb_canvas_2d.clip(min=0, max=1, out=rgb_canvas_2d)
def reconstruct_image_3d(self, time_point: TimePoint, rgb_canvas_3d: ndarray):
"""Draws the labels in color to the rgb image."""
if self._segmented_channel is None:
return # Nothing to draw
if self._display_settings.image_channel not in {self._measurement_channel, self._segmented_channel}:
return # Nothing to draw for this channel
if self._segmented_channel == self._display_settings.image_channel:
# Avoid drawing on top of the same image
rgb_canvas_3d[:, :, :, 0:3] = 0
if rgb_canvas_3d.shape[-1] == 4:
rgb_canvas_3d[:, :, :, 3] = 1 # Also erase alpha channel
label_image = self._experiment.images.get_image_stack(self._time_point, self._segmented_channel)
if label_image is None:
return # Nothing to show for this time point
colored: ndarray = self._label_colormap(label_image.flatten())
colored = colored.reshape((rgb_canvas_3d.shape[0], rgb_canvas_3d.shape[1], rgb_canvas_3d.shape[2], 4))
rgb_canvas_3d[:, :, :, :] += colored[:, :, :, 0:3]
rgb_canvas_3d.clip(min=0, max=1, out=rgb_canvas_3d)
def _get_figure_title(self) -> str:
return (f"Intensity measurement (pre-existing segmentation)\n"
f"Time point {self._time_point.time_point_number()} (z={self._get_figure_title_z_str()}, "
f"c={self._get_figure_title_channel_str()})")
def _find_missing_positions_experiment(self) -> Name | None:
for experiment in self._window.get_active_experiments():
if not experiment.positions.has_positions():
return experiment.name
return None