:orphan: Source: bactoscoop/image.py =========================== .. raw:: html
1# -*- coding: utf-8 -*-
2"""
3Created on Wed Apr 5 09:45:53 2023
4
5@author: Bart Steemans. Govers Lab.
6"""
7from .features import Features
8from . import utilities as u
9from .logging_utils import tqdm_if_verbose
10import numpy as np
11import pandas as pd
12import warnings
13import cv2 as cv
14import logging
15
16warnings.filterwarnings("ignore")
17bactoscoop_logger = logging.getLogger("logger")
18
19
20class Image:
21
22 def __init__(
23 self,
24 image,
25 image_name,
26 frame,
27 mask=None,
28 processed_mesh_dataframe=None,
29 px=0.065,
30 ):
31 """
32 Initialize an instance of your class.
33
34 Parameters
35 ----------
36 image : phase contrast image
37 The phase contrast image associated with this instance.
38 image_name : str
39 The name or identifier of the image.
40 frame : int
41 The frame number
42 mask : mask, optional
43 The segmented image.
44 processed_mesh_dataframe : pd.DataFrame, optional
45 A processed mesh dataframe if available. (default is None)
46 px : float, optional
47 The pixel size in micrometers. (default is 0.065)
48 """
49
50 self.mask = mask
51 self.cells = [] # List of cell objects
52 self.px = px # micron per pixel
53 self.image_name = image_name
54 self.frame = frame
55 self.image = image
56 self.channels = None # Dictionary of the color channels
57
58 self.joined_mask = None # Mask images after cell joining
59 self.processing_mask = None # Optional filtered mask used only for processing
60 self.prefilter_summary = None
61 self.mesh_dataframe = (
62 None # Resulting dataframe containing the meshes of the cells
63 )
64 self.processed_mesh_dataframe = processed_mesh_dataframe
65
66 self.im_interp2d = None # 2D interpolation of the phase contrast image
67 self.chann_interp2d = {} # Cached interpolators of background-corrected channels
68 self.bg_channels = (
69 {}
70 ) # Dictionary of the 2D interpolations of the different color channels
71 self.inverted_image = None # Placeholder for the inverted phase contrast image
72 self.feature_errors = []
73 self.object_detection_errors = []
74 self.shift_errors = []
75
76 def calculate_features(
77 self,
78 method,
79 channel,
80 all_data,
81 use_shifted_contours,
82 shift_signal,
83 max_mesh_size,
84 retain_contour_on_object_mesh_failure_channels=None,
85 ):
86 """
87 Calculate features for cells based on the specified method and channel.
88
89 This method calculates features for individual cells in the image based on the provided method
90 and channel. It also allows for adding profiling data if required.
91
92 Parameters
93 ----------
94 method : str
95 The method used to calculate features for cells.
96 The methods are: 'morphological', 'profiling', 'svm', 'objects', 'phase'. (To be expanded)
97 channel : str
98 The channel for which features are calculated.
99 add_profiling_data : bool
100 Whether to include profiling data in the feature calculations.
101
102 Returns
103 -------
104 pd.DataFrame
105 A DataFrame containing the calculated features for the cells.
106 """
107 features = Features(self)
108 if retain_contour_on_object_mesh_failure_channels is None:
109 retained_channels = frozenset()
110 elif isinstance(retain_contour_on_object_mesh_failure_channels, str):
111 raise TypeError(
112 "retain_contour_on_object_mesh_failure_channels must be a list "
113 "of channel names, not a string."
114 )
115 else:
116 retained_channels = frozenset(
117 retain_contour_on_object_mesh_failure_channels
118 )
119 if not all(isinstance(name, str) for name in retained_channels):
120 raise TypeError(
121 "retain_contour_on_object_mesh_failure_channels must contain "
122 "only channel names."
123 )
124 result_df = None
125 self.feature_errors = []
126
127 # First, filter out cells that should be discarded by discard_large_meshes
128 filtered_cells = []
129 for cell in self.cells:
130 cell.discard = False
131 features.discard_large_meshes(cell, max_mesh_size)
132
133 if not cell.discard:
134 filtered_cells.append(cell)
135
136 # Update self.cells to only contain non-discarded cells
137 self.cells = filtered_cells
138
139 # Now, process the remaining cells
140 for cell in self.cells:
141 feature_calculation_method = f"{method}"
142
143 if hasattr(features, feature_calculation_method):
144 try:
145 calculation_method = getattr(features, feature_calculation_method)
146
147 if method == "objects":
148 calculation_method(
149 cell, channel, all_data, use_shifted_contours,
150 shift_signal,
151 retain_contour_on_object_mesh_failure=(
152 channel in retained_channels
153 ),
154 )
155 else:
156 calculation_method(
157 cell, channel, all_data, use_shifted_contours,
158 shift_signal,
159 )
160
161 except Exception as e:
162 cell.discard = True
163 self.feature_errors.append(
164 {
165 "cell_id": cell.cell_id,
166 "error_type": type(e).__name__,
167 "error": str(e),
168 }
169 )
170 if feature_calculation_method == "svm":
171
172 filtered_cells = []
173 for cell in self.cells:
174 if not cell.discard:
175 filtered_cells.append(cell)
176
177 # Update self.cells to only contain non-discarded cells
178 self.cells = filtered_cells
179 result_df = self.features_to_dataframe(method)
180 return result_df
181
182 def features_to_dataframe(self, method):
183 """
184 Convert cell features stored in a dictionary to a pandas DataFrame.
185
186 This method takes a dictionary of cell features, specified by the method name, and converts it into a pandas DataFrame.
187 Additionally, it adds information features to the DataFrame.
188
189 Parameters
190 ----------
191 method : str
192 The name of the method used for feature calculation.
193
194 Returns
195 -------
196 pd.DataFrame
197 A DataFrame containing cell features.
198 """
199 features_df = self.dataframe_from_dict(f"{method}_features")
200
201 self.add_info_feature_df(features_df)
202
203 return features_df
204
205 def dataframe_from_dict(self, feature_dictionary):
206 """
207 Convert a feature dictionary to a pandas DataFrame.
208
209 This method takes a feature dictionary, which maps cell IDs to feature values, and converts it into a pandas DataFrame.
210
211 Parameters
212 ----------
213 feature_dictionary : str
214 The name of the feature dictionary to convert.
215
216 Returns
217 -------
218 pd.DataFrame
219 A DataFrame containing cell features.
220 """
221 data = {
222 cell.cell_id: getattr(cell, feature_dictionary)
223 for cell in self.cells
224 if not cell.discard
225 }
226
227 df = pd.DataFrame.from_dict(data, orient="index")
228
229 return df
230
231 def add_info_feature_df(self, dataframe):
232 """
233 Add information columns to a feature DataFrame.
234
235 This method adds information columns, including 'image_name', 'cell_id', and 'frame',
236 to an existing DataFrame.
237
238 Parameters
239 ----------
240 dataframe : pd.DataFrame
241 The DataFrame to which information columns will be added.
242 """
243
244 dataframe.insert(0, "image_name", self.image_name)
245 dataframe.insert(
246 0, "cell_id", [cell.cell_id for cell in self.cells if not cell.discard]
247 )
248 dataframe.insert(0, "frame", self.frame)
249
250 def _active_mask(self):
251 return self.processing_mask if self.processing_mask is not None else self.mask
252
253 def apply_neighbor_prefilter(self, max_neighbors=None, connectivity=1):
254 self.processing_mask = None
255 self.prefilter_summary = None
256
257 if max_neighbors is None:
258 return None
259
260 if self.mask is None:
261 raise ValueError("Mask image is not loaded")
262
263 filtered_mask, stats = u.filter_labels_by_neighbor_count(
264 self.mask,
265 max_neighbors=max_neighbors,
266 connectivity=connectivity,
267 )
268
269 if stats["removed_label_count"] > 0:
270 self.processing_mask = filtered_mask
271
272 self.prefilter_summary = stats
273 bactoscoop_logger.info(
274 "Neighbor prefilter on %s kept %s/%s labels (removed %s with > %s neighbors)",
275 self.image_name,
276 stats["kept_label_count"],
277 stats["total_label_count"],
278 stats["removed_label_count"],
279 stats["max_neighbors"],
280 )
281 return stats
282
283 def join_split_pipeline(
284 self,
285 join_thresh=4,
286 split_thresh=0.3,
287 CD_width=False,
288 smoothing=0.1,
289 neighbor_filter_max_neighbors=None,
290 neighbor_filter_connectivity=1,
291 max_daughter_cell_mesh_rows=800,
292 ):
293 """
294 Perform a pipeline for joining and splitting cells.
295
296 This method is used within the batch_mesh_process() method of ImageCollection. It involves joining cells based on
297 a specified threshold, converting masks to mesh data, splitting cells based on a specified threshold,
298 and creating cell objects.
299
300 Parameters
301 ----------
302 join_thresh : int, optional
303 The threshold for joining cells in pixels. (default is 4)
304 split_thresh : float, optional
305 The threshold for splitting cells. (default is 0.3)
306 max_daughter_cell_mesh_rows : int, optional
307 Maximum rebuilt daughter mesh rows (default 800, minimum 4).
308 Oversized resampling is rejected before its dense allocations.
309
310 Returns
311 -------
312 None
313 """
314 max_daughter_cell_mesh_rows = u._validate_max_daughter_cell_mesh_rows(
315 max_daughter_cell_mesh_rows
316 )
317 self.apply_neighbor_prefilter(
318 max_neighbors=neighbor_filter_max_neighbors,
319 connectivity=neighbor_filter_connectivity,
320 )
321 self.join_cells(join_thresh, smoothing)
322 self.mask2mesh(smoothing)
323 self.split_cells(
324 split_thresh, CD_width,
325 max_daughter_cell_mesh_rows=max_daughter_cell_mesh_rows,
326 )
327 self.create_cell_object()
328
329 def create_cell_object(self, verbose=True):
330 """
331 Create Cell objects from mesh data.
332
333 This method creates Cell objects based on mesh data contained in a DataFrame. The Cell objects are
334 initialized with contour, mesh, and midline data, and they are added to the list of cells in the Image object.
335
336 Parameters
337 ----------
338 verbose : bool, optional
339 Whether to print a message indicating the source of the Cell objects (processed or unprocessed meshes). (default is True)
340
341 Returns
342 -------
343 None
344 """
345 self.cells = []
346
347 if self.processed_mesh_dataframe is None:
348 try:
349 dataframe = self.mesh_dataframe
350 message_prefix = "Cell objects are created from unprocessed meshes"
351 except AttributeError:
352 bactoscoop_logger.info(
353 "No meshes from the masks. Either process the masks or load in mesh data from a .pkl file."
354 )
355 return
356 else:
357 dataframe = self.processed_mesh_dataframe
358 message_prefix = "Cell objects are created from processed meshes"
359
360 for i, row in dataframe.iterrows():
361 if "cell_id" not in row or pd.isna(row["cell_id"]):
362 cell_ID = i
363 else:
364 cell_ID = row["cell_id"]
365 contours = np.array(row["contour"])
366 meshes = np.array(row["mesh"])
367 midlines = np.array(row["midline"])
368 cell = Cell(contours, meshes, midlines, self.image.shape, cell_ID)
369 self.add_cell(cell)
370
371 num_cells_created = len(self.cells)
372 message = f"{message_prefix}. {num_cells_created} Cell objects are created."
373
374 if verbose:
375 print(message)
376
377 def add_cell(self, cell):
378 """
379 Add a Cell object to the list of cells in the Image object.
380 """
381 self.cells.append(cell)
382
383 def get_inverted_image(self):
384 """
385 Calculate the inverted image for phase contrast.
386
387 This method computes the inverted image for phase contrast imaging. It subtracts the maximum pixel value
388 from the original image and does a background subtraction (bgr = mean(background intensity)) to obtain the inverted image.
389
390 """
391 inverted_image = np.subtract(np.max(self.image), self.image)
392 bgr = u.median_background(inverted_image, self.mask) #
393 self.inverted_image = np.maximum(inverted_image - bgr, 0)
394 self.im_interp2d = None
395
396 def get_bg_channel(self, channel):
397 _, bg_matrix = u.construct_bg_function(
398 self.channels[channel], self.mask
399 )
400 self.bg_channels[channel] = np.maximum(self.channels[channel] - bg_matrix, 0)
401 self.chann_interp2d.pop(channel, None)
402
403 def get_inverted_interp2d(self):
404 if self.inverted_image is None:
405 self.get_inverted_image()
406 if self.im_interp2d is None:
407 self.im_interp2d = u.interp2d(self.inverted_image)
408 return self.im_interp2d
409
410 def get_channel_interp2d(self, channel):
411 if channel not in self.bg_channels:
412 self.get_bg_channel(channel)
413 if channel not in self.chann_interp2d:
414 self.chann_interp2d[channel] = u.interp2d(self.bg_channels[channel])
415 return self.chann_interp2d[channel]
416
417 def channel_interp2d(self):
418 """
419 Interpolate channel images using 2D spline interpolation.
420
421 This method interpolates the channel images using 2D spline interpolation and stores the interpolated images
422 in the 'chann_interp2d' dictionary.
423 """
424 for channel in self.channels:
425 self.get_channel_interp2d(channel)
426
427 def join_cells(self, thresh=None, smoothing=0.1):
428 """
429 Join cells based on pole-to-pole distance.
430
431 This method attempts to join adjacent cells within the image based on the distance between their poles. It identifies cell pairs
432 with pole-to-pole distances below a specified threshold and merges them into a single cell. The joined cell IDs are updated in the
433 'joined_mask' array.
434
435 Parameters
436 ----------
437 thresh : float, optional
438 The distance threshold for joining cells. Cells with pole-to-pole distances below this value will be merged. If None, no
439 threshold is applied, and all cells will be considered for joining. (default is None)
440
441 """
442
443 mask = self._active_mask()
444 if mask is None:
445 raise ValueError("Mask image is not loaded")
446 bactoscoop_logger.info(">>> Joining cells ...")
447
448 pole_1 = []
449 pole_2 = []
450 cell_ID = []
451 cell_labels = np.unique(mask)
452 cell_labels = cell_labels[cell_labels != 0]
453
454 for cell in tqdm_if_verbose(cell_labels):
455 temp_mask = np.where(mask == cell, 1, 0)
456 temp_mask = cv.convertScaleAbs(temp_mask)
457
458 _, cropped_mask, _, x, y = u.crop_image(mask_to_crop=temp_mask)
459
460 cell_contours = u.get_object_contours(cropped_mask, smoothing=smoothing)
461 for cn in cell_contours:
462 try:
463 skeleton = u.extract_skeleton(cropped_mask)
464 except Exception:
465 skeleton = []
466 cell_ID.append(cell)
467 continue
468 if np.any(skeleton):
469 extended_skeleton, pole1, pole2 = u.extend_skeleton(
470 skeleton, cn, find_pole1=True, find_pole2=True
471 )
472 pole1 += np.array([y, x])
473 pole2 += np.array([y, x])
474 if pole1.shape == (1, 2) and pole2.shape == (1, 2):
475 pole_1.append(pole1)
476 pole_2.append(pole2)
477 else:
478 cell_ID.append(cell)
479 else:
480 cell_ID.append(cell)
481
482 if pole_1 == []:
483 cell_pairs = []
484 else:
485 pole_1 = np.vstack(np.array(pole_1))
486 pole_2 = np.vstack(np.array(pole_2))
487
488 cell_pairs = u.get_cell_pairs(pole_1, pole_2, cell_ID, thresh)
489
490 if not np.any(cell_pairs):
491 self.joined_mask = np.copy(mask)
492 else:
493 # Create a copy of the original masks array to store the new masks
494 new_masks = np.copy(mask)
495 dsu = {}
496 for mask in np.unique(cell_pairs):
497 dsu[mask + 1] = mask + 1
498 # Merge masks that belong to the same group
499 for mask1, mask2 in cell_pairs:
500 root1 = mask1 + 1
501 while root1 != dsu[root1]:
502 root1 = dsu[root1]
503 root2 = mask2 + 1
504 while root2 != dsu[root2]:
505 root2 = dsu[root2]
506 if root1 != root2:
507 dsu[root1] = root2
508
509 # Update the masks with the merged values
510 for mask, root in dsu.items():
511 new_masks[new_masks == mask] = root
512 unique_masks = np.unique(new_masks)
513 # Create a dictionary that maps unique mask values to new mask values starting from 1
514 mask_dict = {mask: i for i, mask in enumerate(unique_masks)}
515
516 # Use the dictionary to relabel the new_masks array and the new_labels array
517 new_masks_relabel = np.vectorize(mask_dict.get)(new_masks)
518 self.joined_mask = new_masks_relabel
519
520 def mask2mesh(self, smoothing=0.1):
521 """
522 Create meshes from cell masks.
523
524 This method processes cell masks to create corresponding meshes. It generates contours and skeletons for each cell in the mask,
525 extends the skeleton, and calculates additional geometric properties, such as length and width. Based on these properties, it
526 straightens the cell contour and creates a mesh representation. The resulting mesh data, including contour, mesh, and midline,
527 is stored in a DataFrame and associated with the image frame.
528
529 This method is based on the cell profile creation of the MOMIA package (doi: 10.1016/j.celrep.2021.110154)
530
531 """
532 if self.joined_mask is not None:
533 mask = self.joined_mask
534 bactoscoop_logger.info(">>> Creating meshes from joined masks ...")
535 else:
536 mask = self._active_mask()
537 bactoscoop_logger.info(">>> Creating meshes from raw masks ...")
538 # loop through all cells in the frame
539 self.mesh_dataframe = u.get_cellular_mesh(mask, smoothing=smoothing)
540 self.mesh_dataframe["frame"] = self.frame
541 self.mesh_dataframe["image_name"] = self.image_name
542
543 def split_cells(self, thresh=0.3, CD_width=False, max_daughter_cell_mesh_rows=800):
544 """
545 Split cells based on a constriction degree threshold.
546
547 This method separates cells within the image based on a given constriction degree threshold. It calculates the constriction degree
548 for each cell and, if it exceeds the threshold, splits the cell into two separate cells. The new mesh data, including contour, mesh,
549 and midline, is stored in a DataFrame and associated with the image frame.
550
551 This method is based on the splitting algorithm in the Oufti software package (doi: 10.1111/mmi.13264).
552
553 Parameters
554 ----------
555 thresh : float, optional
556 The constriction degree threshold for splitting cells. Cells with a constriction degree greater than this threshold will be split.
557 The default is 0.3.
558
559 max_daughter_cell_mesh_rows : int, optional
560 Maximum final mesh rows per daughter (default 800, minimum 4).
561 Reject oversized predicted row counts before reconstruction, then
562 require both final daughters to have 4..limit rows. If either fails,
563 neither daughter nor the parent enters the processed dataframe.
564
565 """
566 max_daughter_cell_mesh_rows = u._validate_max_daughter_cell_mesh_rows(
567 max_daughter_cell_mesh_rows
568 )
569 self.get_inverted_image()
570 im_interp2d = u.interp2d(self.inverted_image)
571
572 bactoscoop_logger.info(">>> Splitting cells ...")
573
574 new_meshes = []
575 new_contours = []
576 new_midlines = []
577
578 for row in tqdm_if_verbose(self.mesh_dataframe.itertuples(index=False)):
579
580 x1 = row.mesh[:, 0]
581 y1 = row.mesh[:, 1]
582 x2 = row.mesh[:, 2]
583 y2 = row.mesh[:, 3]
584 contour = row.contour
585 midline = row.midline
586
587 complete_mesh = np.array(np.column_stack((x1, y1, x2, y2)))
588
589 step_length_px = u.get_step_length(x1, y1, x2, y2, self.px)
590
591 width_px = u.get_width(x1, y1, x2, y2) * self.px
592 if CD_width:
593 weighted_intprofile = width_px
594 else:
595 intprofile = u.measure_smoothened_intensity(
596 midline, im_interp2d, width=5
597 )
598 weighted_intprofile = u.get_weighted_intprofile(intprofile, width_px)
599
600 constrDegree, relPos, constrDegree_abs, ctpos, _= (
601 u.constr_degree_single_cell_min(
602 weighted_intprofile,
603 step_length_px,
604 upper_limit=0.95,
605 lower_limit=0.05,
606 )
607 )
608
609 if 0.95 > constrDegree > thresh:
610 try:
611 x, y, left, right = u.split_point(x1, y1, x2, y2, ctpos)
612
613 mesh1rec = np.concatenate(
614 (complete_mesh[: (ctpos - 2)], right, np.array([[x, y, x, y]]))
615 )
616 mesh2rec = np.concatenate(
617 (np.array([[x, y, x, y]]), left, complete_mesh[(ctpos + 2) :])
618 )
619
620 mesh1, contour1, midline1 = u.split_mesh2mesh(
621 mesh1rec[:, 0], mesh1rec[:, 1], mesh1rec[:, 2], mesh1rec[:, 3],
622 max_daughter_cell_mesh_rows=max_daughter_cell_mesh_rows,
623 )
624 mesh2, contour2, midline2 = u.split_mesh2mesh(
625 mesh2rec[:, 0], mesh2rec[:, 1], mesh2rec[:, 2], mesh2rec[:, 3],
626 max_daughter_cell_mesh_rows=max_daughter_cell_mesh_rows,
627 )
628 except Exception as e:
629 bactoscoop_logger.debug(f"{e}")
630 continue
631 if all(4 <= len(daughter) <= max_daughter_cell_mesh_rows for daughter in (mesh1, mesh2)):
632 new_meshes.append(mesh1)
633 new_meshes.append(mesh2)
634
635 new_contours.append(contour1)
636 new_contours.append(contour2)
637
638 new_midlines.append(midline1)
639 new_midlines.append(midline2)
640 else:
641 bactoscoop_logger.debug(
642 "Splitting resulted in a daughter mesh outside 4-%s rows",
643 max_daughter_cell_mesh_rows,
644 )
645
646 else:
647
648 new_meshes.append(complete_mesh)
649 new_contours.append(contour)
650 new_midlines.append(midline)
651 self.processed_mesh_dataframe = pd.DataFrame(
652 {"mesh": new_meshes, "contour": new_contours, "midline": new_midlines}
653 )
654 self.processed_mesh_dataframe["frame"] = self.frame
655 self.processed_mesh_dataframe["image_name"] = self.image_name
656
657 def object_detection(
658 self,
659 channels=None,
660 smoothing=0.1,
661 align=False,
662 log_sigma=3,
663 kernel_width=4,
664 min_overlap_ratio=0.01,
665 max_external_ratio=0.1,
666 ):
667 """
668 Detect subcellular objects within cell contours in specified channels.
669
670 This method performs object detection within the contours of cells for the specified channels. It uses a Laplacian of Gaussian (LoG)
671 filter to detect subcellular objects and returns their contours. The detected objects are stored in a DataFrame, including the frame,
672 cell ID, channel name, and object contours.
673
674 Parameters
675 ----------
676 channels : List of str, optional
677 The list of channel names for which to perform object detection.
678
679 log_sigma : int, optional
680 The sigma parameter of the LoG filter used in object detection. Increasing this parameter may detect larger objects.
681 The default is 3.
682
683 kernel_width : int, optional
684 The kernel width used for dilating object masks. Increasing this value results in larger masks.
685 The default is 4.
686
687 min_overlap_ratio : float, optional
688 The minimum overlap ratio between detected objects and cell contours to keep the object. Objects with lower overlap are discarded.
689 The default is 0.01.
690
691 max_external_ratio : float, optional
692 The maximum ratio an object is allowed to lie outside the cell contour. Objects exceeding this ratio are discarded.
693 The default is 0.1.
694
695 Returns
696 -------
697 pd.DataFrame
698 A DataFrame containing information about the detected objects, including frame, cell ID, channel name, and object contours.
699 """
700
701 all_objects = []
702 self.object_detection_errors = []
703
704 for channel in channels:
705 # A repeat attempt replaces this channel, even when it fails or
706 # detects no objects. Results for other channels remain intact.
707 for cell in self.cells:
708 cell.object_meshdata.pop(channel, None)
709
710 if channel in self.channels:
711
712 for cell in self.cells:
713 if align:
714 img = self.image
715 else:
716 img = None
717 try:
718 meshdata = u.get_object_mesh(
719 cell.contour,
720 self.channels[channel],
721 img,
722 cell.cell_id,
723 smoothing,
724 log_sigma,
725 kernel_width,
726 min_overlap_ratio,
727 max_external_ratio,
728 )
729 cell.object_meshdata[channel] = meshdata
730 for mesh_error in meshdata.get("mesh_construction_errors", []):
731 self.object_detection_errors.append(
732 {
733 "cell_id": cell.cell_id,
734 "channel": channel,
735 "error_type": mesh_error["error_type"],
736 "error": (
737 f"Object {mesh_error['object_index']} mesh construction "
738 f"failed: {mesh_error['error']}"
739 ),
740 "recovered": True,
741 }
742 )
743
744 if cell.object_meshdata[channel]["object_contour"]:
745 all_objects.append(
746 {
747 "frame": self.frame,
748 "cell_id": cell.cell_id,
749 "cell_mesh": cell.mesh,
750 "channel": channel,
751 "object_contours": cell.object_meshdata[channel][
752 "object_contour"
753 ],
754 "object_mesh": cell.object_meshdata[channel][
755 "object_mesh"
756 ],
757 "object_midline": cell.object_meshdata[channel][
758 "object_midline"
759 ],
760 }
761 )
762
763 except IndexError as e:
764 cell.object_meshdata.pop(channel, None)
765 self.object_detection_errors.append(
766 {
767 "cell_id": cell.cell_id,
768 "channel": channel,
769 "error_type": type(e).__name__,
770 "error": str(e),
771 }
772 )
773
774 detections_df = pd.DataFrame(all_objects)
775
776 return detections_df
777
778 def shift_correction(
779 self,
780 shifted_channel,
781 log_sigma,
782 kernel_width,
783 min_overlap_ratio,
784 max_external_ratio,
785 phase_log_sigma,
786 phase_closing_level,
787 signal_closing_level,
788 max_shift_correction,
789 ):
790 self.shift_errors = []
791 if not self.cells:
792 bactoscoop_logger.warning(
793 f"No cells available for shift correction in frame {self.frame}."
794 )
795 return 0.0
796
797 for channel in shifted_channel:
798 if channel not in self.channels:
799 raise ValueError(
800 f"'{shifted_channel}' is not present in the channels attribute."
801 )
802 failed_attempts = 0
803 for cell in self.cells:
804 try:
805 cell.shifted_contour, cell.shifted_mesh, cell.shifted_midline = (
806 u.shift_contour(
807 self.image,
808 self.channels[channel],
809 cell.contour,
810 cell.mesh,
811 cell.midline,
812 log_sigma,
813 kernel_width,
814 min_overlap_ratio,
815 max_external_ratio,
816 phase_log_sigma,
817 phase_closing_level,
818 signal_closing_level,
819 max_shift_correction,
820 )
821 )
822 except Exception as e:
823 self.shift_errors.append(
824 {
825 "cell_id": cell.cell_id,
826 "channel": channel,
827 "error_type": type(e).__name__,
828 "error": str(e),
829 }
830 )
831 cell.shifted_contour = None
832 cell.shifted_mesh = None
833 cell.shifted_midline = None
834 if cell.shifted_contour is None:
835 failed_attempts += 1
836 return (len(self.cells) - failed_attempts) / len(self.cells)
837
838
839class Cell:
840 """
841 Class representing a single cell in an image.
842
843 This class represents a single cell within an image, characterized by its contour, mesh, and midline data. It is identified by a unique cell ID.
844
845 Attributes
846 ----------
847 discard : bool
848 A flag to indicate whether the cell should be discarded during analysis.
849
850 contour : ndarray
851 The contour data of the cell.
852
853 mesh : ndarray
854 The mesh data (4xN).
855
856 midline : ndarray
857 The midline data describing the cell's central axis (2xN).
858
859 cell_id : int
860 A unique identifier for the cell.
861
862 x1, y1, x2, y2 : ndarray
863 Arrays representing the coordinates of the cell's mesh.
864
865 profile_mesh : None
866 Placeholder for profile mesh data.
867
868 object_contours : dict
869 A dictionary to store detected subcellular object contours in different channels.
870
871 morphological_features : dict
872 Dictionary to store morphological features of the cell.
873
874 phase_features : dict
875 Dictionary to store phase contrast features of the cell.
876
877 objects_features : dict
878 Dictionary to store features related to subcellular objects within the cell.
879
880 svm_features : dict
881 Dictionary to store support vector machine (SVM) features.
882
883 profiling_features : dict
884 Dictionary to store profiling features.
885
886 profiling_data : dict
887 Dictionary to store data related to the profiling of the cell.
888 """
889
890 def __init__(self, contour, mesh, midline, shape, cell_id):
891
892 self.discard = False
893 self.contour = contour
894 self.mesh = mesh
895 self.midline = midline
896 self.cell_id = cell_id
897
898 self.shifted_contour = None
899 self.shifted_mesh = None
900 self.shifted_midline = None
901
902 self.x1 = self.mesh[:, 0]
903 self.y1 = self.mesh[:, 1]
904 self.x2 = self.mesh[:, 2]
905 self.y2 = self.mesh[:, 3]
906
907 self.profile_mesh = None
908
909 self.object_meshdata = {}
910
911 self.morphological_features = {}
912 self.phase_features = {}
913 self.objects_features = {}
914 self.svm_features = {}
915 self.profiling_features = {}
916 self.signal_correlation_features = {}
917 self.profiling_data = {}