:orphan: Source: bactoscoop/features.py ============================== .. raw:: html
  1# -*- coding: utf-8 -*-
     2"""
     3Created on Wed Apr  5 09:45:56 2023
     4
     5@author: Bart Steemans. Govers Lab.
     6"""
     7from . import utilities as u
     8import numpy as np
     9import logging
    10
    11bactoscoop_logger = logging.getLogger("logger")
    12
    13
    14class Features:
    15
    16    def __init__(self, image_obj):
    17
    18        self.image_obj = image_obj
    19
    20    def discard_large_meshes(self, cell, max_mesh_size=600):
    21        """
    22        Discard cells with meshes that are too small or large.
    23
    24        This method checks the size of a cell's mesh and flags it for discard if the mesh size does not fall within the acceptable range.
    25
    26        Parameters
    27        ----------
    28        cell : Cell
    29            The Cell object representing the cell to be checked.
    30
    31        Returns
    32        -------
    33        None
    34        """
    35
    36        if not 4 <= len(cell.mesh) <= max_mesh_size:
    37
    38            bactoscoop_logger.debug(
    39                f"The mesh of Cell number {cell.cell_id} is too small or large"
    40            )
    41            cell.discard = True
    42    def discard_bad_meshes(self, cell):
    43
    44        px = self.image_obj.px
    45        step_length = u.get_step_length(cell.x1, cell.y1, cell.x2, cell.y2, px)
    46        _, width_not_ordered = u.get_avg_width(cell.x1, cell.y1, cell.x2, cell.y2, px)
    47        cell_area = u.get_area(cell.contour, px)
    48        mesh_area = u.get_mesh_area(width_not_ordered, step_length)
    49        ratio = mesh_area / cell_area
    50
    51        if 0.9 <= ratio <= 1.1:
    52            cell.discard = False
    53        else:
    54            bactoscoop_logger.debug(f"Cell mesh with cell id {cell.cell_id} is discarded due to bad mesh quality")
    55            cell.discard = True
    56
    57    def contour_selection(self, cell, use_shifted_contours):
    58        if use_shifted_contours and cell.shifted_contour is not None:
    59            return cell.shifted_contour, cell.shifted_midline, cell.shifted_mesh
    60        else:
    61            return cell.contour, cell.midline, cell.mesh
    62
    63    def morphological(self, cell, channel, all_data, *args):
    64        """
    65        Calculate morphological features for a cell.
    66
    67        """
    68        cell.morphological_features = {}
    69
    70        # Prepare cropped mask and image for morphological measurements and constriction degree
    71        if self.image_obj.inverted_image is None:
    72            self.image_obj.get_inverted_image()
    73        cell_mask, cropped_signal, _, _, x_offset, y_offset = u.get_cell_mask(
    74            cell.contour, cell.midline, self.image_obj.inverted_image
    75        )
    76        cropped_contour, cropped_ml, cropped_msh = u.get_cropped_cell_data(
    77            cell.contour, cell.midline, cell.mesh, x_offset, y_offset
    78        )
    79        cropped_signal_interp2d = u.interp2d(cropped_signal)
    80
    81        # Morphological measurements
    82        px = self.image_obj.px
    83        step_length = u.get_step_length(cell.x1, cell.y1, cell.x2, cell.y2, px)
    84
    85        cell.morphological_features["cell_length"] = u.get_length(step_length)
    86        cell.morphological_features["cell_width"], width_not_ordered = u.get_avg_width(
    87            cell.x1, cell.y1, cell.x2, cell.y2, px
    88        )
    89        cell.morphological_features["cell_aspect_ratio"] = (
    90            cell.morphological_features["cell_length"]
    91            / cell.morphological_features["cell_width"]
    92        )
    93        cell.profiling_data["cell_widthno"] = width_not_ordered
    94
    95        cell_area = u.get_area(cell.contour, px)
    96        cell.morphological_features["cell_area"] = cell_area
    97        cell.morphological_features["cell_volume"] = u.get_volume(
    98            cell.x1, cell.y1, cell.x2, cell.y2, step_length, px
    99        )
   100        cell.morphological_features["cell_surface_area"] = u.get_surface_area(
   101            width_not_ordered, step_length
   102        )
   103        cell.morphological_features["cell_SOV"] = u.get_surface_area_over_volume(
   104            cell.morphological_features["cell_surface_area"],
   105            cell.morphological_features["cell_volume"],
   106        )
   107
   108        per, circ, poa, sph = u.get_cell_perimeter_measurements(
   109            cell.contour, cell.morphological_features["cell_area"], px
   110        )
   111        cell.morphological_features["cell_perimeter"] = per
   112        cell.morphological_features["cell_circularity"] = circ
   113        cell.morphological_features["cell_compactness"] = poa
   114        cell.morphological_features["cell_sphericity"] = sph
   115        cell.morphological_features["cell_width_variability"] = (
   116            u.get_cell_width_variability(width_not_ordered)
   117        )
   118
   119        ecc, sol, conv = u.get_cell_convexity(cell_mask)
   120        cell.morphological_features["cell_eccentricity"] = ecc
   121        cell.morphological_features["cell_solidity"] = sol
   122        cell.morphological_features["cell_convexity"] = conv
   123
   124        con_curvature, con_max_c, con_min_c, con_mean_c, con_std_c = (
   125            u.get_curvature_characteristics(cell.contour, px)
   126        )
   127        cell.morphological_features["cell_max_contour_curvature"] = con_max_c
   128        cell.morphological_features["cell_min_contour_curvature"] = con_min_c
   129        cell.morphological_features["cell_mean_contour_curvature"] = con_mean_c
   130        cell.morphological_features["cell_std_contour_curvature"] = con_std_c
   131        cell.morphological_features["cell_contour_bending_energy"] = u.bending_energy(
   132            cell.contour, con_curvature, px
   133        )
   134
   135        mid_curvature, mid_max_c, mid_min_c, mid_mean_c, mid_std_c = (
   136            u.get_curvature_characteristics(cell.midline, px)
   137        )
   138        cell.morphological_features["cell_max_midline_curvature"] = mid_max_c
   139        cell.morphological_features["cell_min_midline_curvature"] = mid_min_c
   140        cell.morphological_features["cell_mean_midline_curvature"] = mid_mean_c
   141        cell.morphological_features["cell_std_midline_curvature"] = mid_std_c
   142        cell.morphological_features["cell_midline_bending_energy"] = u.bending_energy(
   143            cell.midline, mid_curvature, px
   144        )
   145        cell.morphological_features["cell_midline_sinuosity"] = u.sinuosity(
   146            cell.midline
   147        )
   148
   149        cell.morphological_features["cell_area_asymmetry"] = u.get_area_asymmetry(
   150            step_length, width_not_ordered
   151        )
   152
   153        regionprops_features = u.get_additional_regionprops_features(cell_mask, px)
   154
   155        # Store additional regionprops features
   156        cell.morphological_features["max_feret_diameter"] = regionprops_features[
   157            "F_MAX_FERET_DIAMETER"
   158        ]
   159        cell.morphological_features["equivalent_diameter"] = regionprops_features[
   160            "F_EQUIVALENT_DIAMETER"
   161        ]
   162        cell.morphological_features["max_radius"] = regionprops_features[
   163            "F_MAXIMUM_RADIUS"
   164        ]
   165        cell.morphological_features["mean_radius"] = regionprops_features[
   166            "F_MEAN_RADIUS"
   167        ]
   168        cell.morphological_features["median_radius"] = regionprops_features[
   169            "F_MEDIAN_RADIUS"
   170        ]
   171        cell.morphological_features["extent"] = regionprops_features["F_EXTENT"]
   172
   173        # Store moment-related features
   174        moment_types = [
   175            "moments",
   176            "moments_central",
   177            "moments_normalized",
   178            "moments_hu",
   179            "inertia_tensor",
   180            "inertia_tensor_eigvals",
   181        ]
   182        for moment_type in moment_types:
   183            for key in regionprops_features:
   184                if f"{moment_type}" in key:
   185                    cell.morphological_features[key] = regionprops_features[key]
   186
   187        # Constriction degree calculations
   188        width_px = u.get_width(cell.x1, cell.y1, cell.x2, cell.y2) * px
   189
   190        width_profile = width_px
   191        (
   192            cell.morphological_features["cell_constriction_degree_width"],
   193            cell.morphological_features["cell_rel_constr_pos_width"],
   194            constrDegree_abs,
   195            ctpos,
   196            _,
   197        ) = u.constr_degree_single_cell_min(width_profile, step_length)
   198
   199        axial_profile = u.measure_smoothened_intensity(
   200            cropped_ml, cropped_signal_interp2d, width=5
   201        )
   202        (
   203            cell.morphological_features["cell_constriction_degree_axial"],
   204            cell.morphological_features["cell_rel_constr_pos_axial"],
   205            _,
   206            _,
   207            _,
   208        ) = u.constr_degree_single_cell_min(axial_profile, step_length)
   209
   210        weighted_axial_profile = u.get_weighted_intprofile(axial_profile, width_px)
   211        (
   212            cell.morphological_features["cell_constriction_degree_width_axial"],
   213            cell.morphological_features["cell_rel_constr_pos_width_axial"],
   214            _,
   215            _,
   216            _,
   217        ) = u.constr_degree_single_cell_min(weighted_axial_profile, step_length)
   218
   219        try:
   220            cell.profile_mesh = u.get_profile_mesh(
   221                cropped_msh, u.get_width(cell.x1, cell.y1, cell.x2, cell.y2)
   222            )
   223        except MemoryError:
   224            cell.discard = True
   225            pass
   226        mesh_intensity = cropped_signal_interp2d.ev(
   227            cell.profile_mesh[0], cell.profile_mesh[1]
   228        ).T
   229        mesh_intensity_profile = u.gaussian_smoothing(mesh_intensity)
   230        (
   231            cell.morphological_features["cell_constriction_degree_mesh"],
   232            cell.morphological_features["cell_rel_constr_pos_mesh"],
   233            _,
   234            _,
   235            _,
   236        ) = u.constr_degree_single_cell_min(mesh_intensity_profile, step_length)
   237
   238        # Discarding cells with an erroneous mesh
   239        self.discard_bad_meshes(cell)
   240
   241    def profiling(
   242        self, cell, channel, all_data, use_shifted_contours, shift_signal, *args
   243    ):
   244        """
   245        Calculate profiling features for a cell.
   246
   247        """
   248        cell.profiling_features = {}
   249        px = self.image_obj.px
   250
   251        if self.image_obj.channels is None or channel not in self.image_obj.channels:
   252            raise AttributeError(
   253                f"No objects are detected for channel {channel} or the channel is not loaded"
   254            )
   255            return
   256
   257        if channel not in self.image_obj.bg_channels:
   258            self.image_obj.get_bg_channel(channel)
   259            bactoscoop_logger.debug("Background subtraction completed")
   260
   261        contour, midline, mesh = self.contour_selection(cell, use_shifted_contours)
   262
   263        if shift_signal:
   264            cropped_signal, cropped_mask, _, x_offset, y_offset = u.crop_image(
   265                self.image_obj.bg_channels[channel],
   266                contour,
   267                phase_img=self.image_obj.image,
   268            )
   269        else:
   270            cropped_signal, cropped_mask, _, x_offset, y_offset = u.crop_image(
   271                self.image_obj.bg_channels[channel], contour, phase_img=None
   272            )
   273
   274        cropped_contour, cropped_ml, cropped_msh = u.get_cropped_cell_data(
   275            contour, midline, mesh, x_offset, y_offset
   276        )
   277        cropped_signal_interp2d = u.interp2d(cropped_signal)
   278
   279        try:
   280            profile_mesh = u.get_profile_mesh(
   281                cropped_msh, u.get_width(cell.x1, cell.y1, cell.x2, cell.y2)
   282            )
   283        except MemoryError:
   284            cell.discard = True
   285            pass
   286
   287        # Intensity features
   288        cell.profiling_features["axial_intensity"] = u.measure_smoothened_intensity(
   289            cropped_ml, cropped_signal_interp2d, width=10
   290        )
   291        cell.profiling_features["step_length_demograph"] = (
   292            u.get_cumulative_step_length_midline(cropped_ml, px)
   293        )
   294        cell.profiling_features["normalized_axial_intensity"] = u.normalize_per_cell(
   295            cell.profiling_features["axial_intensity"]
   296        )
   297        cell.profiling_features["cell_length"] = u.line_length(cropped_ml) * px
   298        mesh_intensity = cropped_signal_interp2d.ev(profile_mesh[0], profile_mesh[1]).T
   299        cell.profiling_features["raw_average_mesh_intensity"] = np.average(
   300            mesh_intensity, axis=0
   301        )
   302        cell.profiling_features["average_mesh_intensity"] = u.gaussian_smoothing(
   303            mesh_intensity
   304        )
   305        cell.profiling_features["raw_normalized_average_mesh_intensity"] = (
   306            u.normalize_per_cell(cell.profiling_features["raw_average_mesh_intensity"])
   307        )
   308        cell.profiling_features["normalized_average_mesh_intensity"] = (
   309            u.normalize_per_cell(cell.profiling_features["average_mesh_intensity"])
   310        )
   311
   312        cell.profiling_features["normalized_average_mesh_intensity"] = (
   313            u.normalize_per_cell(cell.profiling_features["average_mesh_intensity"])
   314        )
   315
   316        step_length = u.get_step_length(cell.x1, cell.y1, cell.x2, cell.y2, px)
   317        # useful for DAPI signal
   318        (
   319            cell.profiling_features["signal_constriction_degree_mesh"],
   320            cell.profiling_features["signal_rel_constr_pos_mesh"],
   321            _,
   322            _,
   323            _,
   324        ) = u.constr_degree_single_cell_min(
   325            cell.profiling_features["average_mesh_intensity"], step_length
   326        )
   327
   328        (
   329            glcm_dissimilarity,
   330            glcm_correlation,
   331            glcm_homogeneity,
   332            glcm_energy,
   333            glcm_contrast,
   334            glcm_entropy,
   335        ) = u.calculate_haralick_features(
   336            cropped_contour,
   337            cropped_signal,
   338        )
   339        cell.profiling_features["cell_mesh_intensity_mean"] = np.mean(
   340            cell.profiling_features["average_mesh_intensity"]
   341        )
   342        cell.profiling_features["cell_mesh_intensity_std"] = np.std(
   343            cell.profiling_features["average_mesh_intensity"]
   344        )
   345        cell.profiling_features["cell_mesh_kurtosis"] = u.get_kurtosis(
   346            cell.profiling_features["average_mesh_intensity"]
   347        )
   348        cell.profiling_features["cell_mesh_skewness"] = u.get_skew(
   349            cell.profiling_features["average_mesh_intensity"]
   350        )
   351
   352
   353
   354        cell.profiling_features["cell_glcm_dissimilarity"] = glcm_dissimilarity
   355        cell.profiling_features["cell_glcm_correlation"] = glcm_correlation
   356        cell.profiling_features["cell_glcm_homogeneity"] = glcm_homogeneity
   357        cell.profiling_features["cell_glcm_energy"] = glcm_energy
   358        cell.profiling_features["cell_glcm_contrast"] = glcm_contrast
   359        cell.profiling_features["cell_glcm_shannon_entropy"] = glcm_entropy
   360
   361        avg_width, _ = u.get_avg_width_no_px(cell.x1, cell.y1, cell.x2, cell.y2)
   362        cell.profiling_features["radial_intensity_distribution"] = u.radial_intensities(
   363            cropped_signal_interp2d,
   364            cropped_contour,
   365            width=avg_width,
   366            min_val=np.min(cropped_signal),
   367            max_val=np.max(cropped_signal),
   368            num_erosions=10,
   369            erosion_scale=None,
   370        )
   371
   372        # Discarding cells with an erroneous mesh
   373        self.discard_bad_meshes(cell)
   374
   375    def svm(self, cell, channel, all_data, *args):
   376        """
   377        Calculate features of a cell to be used in training the Support Vector Machine (SVM) or curate a dataset using a trained SVM model.
   378
   379        """
   380        cell.svm_features = {}
   381
   382        # Preparation cropped mask and images for intensity measurements
   383        if self.image_obj.inverted_image is None:
   384            self.image_obj.get_inverted_image()
   385        cropped_signal, cropped_mask, cropped_contour, x_offset, y_offset = (
   386            u.crop_image(self.image_obj.inverted_image, cell.contour)
   387        )
   388        cropped_signal_interp2d = u.interp2d(cropped_signal)
   389
   390        px = self.image_obj.px
   391        step_length = u.get_step_length(cell.x1, cell.y1, cell.x2, cell.y2, px)
   392
   393        # Contour is added here for labeling purposes
   394        cell.svm_features["contour"] = cell.contour
   395
   396        # Morphological features
   397        cell.svm_features["cell_length"] = u.get_length(step_length)
   398        cell.svm_features["cell_width"], width_not_ordered = u.get_avg_width(
   399            cell.x1, cell.y1, cell.x2, cell.y2, px
   400        )
   401        cell.svm_features["cell_area"] = u.get_area(cell.contour, px)
   402        cell.svm_features["cell_volume"] = u.get_volume(
   403            cell.x1, cell.y1, cell.x2, cell.y2, step_length, px
   404        )
   405        cell.svm_features["cell_surface_area"] = u.get_surface_area(
   406            width_not_ordered, step_length
   407        )
   408        (
   409            curvature,
   410            cell.svm_features["max_curvature"],
   411            cell.svm_features["min_curvature"],
   412            cell.svm_features["mean_curvature"],
   413            std_c,
   414        ) = u.get_curvature_characteristics(cell.contour, px)
   415        (
   416            cell.svm_features["cell_perimeter"],
   417            cell.svm_features["cell_circularity"],
   418            cell.svm_features["cell_POA"],
   419            cell.svm_features["cell_sphericity"],
   420        ) = u.get_cell_perimeter_measurements(
   421            cell.contour, cell.svm_features["cell_area"], px
   422        )
   423
   424        # Cell surface intensity features
   425        total_int, max_int, mean_int = u.get_total_phaco_intensity(
   426            cropped_contour, cropped_signal.shape, cropped_signal_interp2d
   427        )
   428        cell.svm_features["phaco_total_intensity"] = total_int
   429        cell.svm_features["phaco_max_intensity"] = max_int
   430        cell.svm_features["phaco_mean_intensity"] = mean_int
   431
   432        # Contour intensity features
   433        phaco_contour_intensity = u.get_contour_intensity(
   434            cropped_contour, cropped_signal_interp2d
   435        )
   436        phaco_contour_intensity_variability = u.measure_contour_variability(
   437            phaco_contour_intensity
   438        )
   439
   440        sorted_phaco_contour_intensity = sorted(phaco_contour_intensity)
   441        cell.svm_features["phaco_contour_peaks"] = u.find_signal_peaks(
   442            phaco_contour_intensity, max_int
   443        )
   444        cell.svm_features["phaco_max_contour_intensity"] = np.max(
   445            phaco_contour_intensity
   446        )
   447        cell.svm_features["phaco_mean_contour_intensity"] = np.mean(
   448            phaco_contour_intensity
   449        )
   450        cell.svm_features["phaco_min_contour_intensity"] = np.mean(
   451            sorted_phaco_contour_intensity[:10]
   452        )
   453        cell.svm_features["phaco_max_contour_variability"] = np.max(
   454            phaco_contour_intensity_variability
   455        )
   456        cell.svm_features["phaco_mean_contour_variability"] = np.mean(
   457            phaco_contour_intensity_variability
   458        )
   459
   460        # Midline intensity features
   461        # If no midline available calculate it with mesh2midline( ).
   462        cropped_midline = cell.midline.copy()
   463        cropped_midline -= [x_offset, y_offset]
   464        cell.profiling_data["phaco_axial_intensity"] = u.measure_smoothened_intensity(
   465            cropped_midline, cropped_signal_interp2d, width=5
   466        )
   467
   468        cell.svm_features["midline_kurtosis"] = u.get_kurtosis(
   469            cell.profiling_data["phaco_axial_intensity"]
   470        )
   471        cell.svm_features["midline_skewness"] = u.get_skew(
   472            cell.profiling_data["phaco_axial_intensity"]
   473        )
   474
   475        # Expanded contour intensity features
   476        expanded_contour = u.expand_contour(cropped_contour, scale=2)
   477        eroded_contour = u.erode_contour(cropped_contour, scale=2)
   478        cell.profiling_data["phaco_expanded_contour_intensity"] = (
   479            u.get_contour_intensity(expanded_contour, cropped_signal_interp2d)
   480        )
   481        cell.profiling_data["phaco_eroded_contour_intensity"] = u.get_contour_intensity(
   482            eroded_contour, cropped_signal_interp2d
   483        )
   484
   485        cell.svm_features["phaco_max_expanded_contour_intensity"] = np.max(
   486            cell.profiling_data["phaco_expanded_contour_intensity"]
   487        )
   488        cell.svm_features["phaco_mean_expanded_contour_intensity"] = np.mean(
   489            cell.profiling_data["phaco_expanded_contour_intensity"]
   490        )
   491
   492        # Mesh gradient features
   493        cell.svm_features["phaco_cell_edge_gradient"] = np.average(
   494            cell.profiling_data["phaco_eroded_contour_intensity"]
   495            - cell.profiling_data["phaco_expanded_contour_intensity"]
   496        )
   497
   498        # Discarding cells with an erroneous mesh
   499        self.discard_bad_meshes(cell)
   500
   501    def objects(
   502        self, cell, channel, all_data, use_shifted_contours, *args,
   503        retain_contour_on_object_mesh_failure=False,
   504    ):
   505        """
   506        Calculates features related to the identified objects within a cell.
   507
   508        """
   509        cell.objects_features = {}
   510
   511        px = self.image_obj.px
   512
   513        if self.image_obj.channels is None or channel not in self.image_obj.channels:
   514            raise AttributeError(
   515                f"No objects are detected for channel {channel} or the channel is not loaded"
   516            )
   517            return
   518
   519        if channel not in self.image_obj.bg_channels:
   520            self.image_obj.get_bg_channel(channel)
   521            bactoscoop_logger.debug("Background subtraction completed")
   522
   523        if cell.object_meshdata[channel]["object_contour"]:
   524
   525            # Extract required cellular/object properties to extract the features
   526            step_length = u.get_step_length(cell.x1, cell.y1, cell.x2, cell.y2, px)
   527            cell_volume = u.get_volume(
   528                cell.x1, cell.y1, cell.x2, cell.y2, step_length, px
   529            )
   530            cell_area = u.get_area(cell.contour, px)
   531            cell_widthno = u.get_width(cell.x1, cell.y1, cell.x2, cell.y2)
   532            # Object properties and cropping to speed up analysis
   533            object_contours = cell.object_meshdata[channel]["object_contour"]
   534            object_meshes = cell.object_meshdata[channel]["object_mesh"]
   535            object_midlines = cell.object_meshdata[channel]["object_midline"]
   536            (
   537                object_masks,
   538                cropped_signal,
   539                cropped_oc,
   540                cropped_oml,
   541                cropped_oms,
   542                x_offset,
   543                y_offset,
   544            ) = u.get_object_masks(
   545                cell.contour,
   546                object_contours,
   547                object_midlines,
   548                object_meshes,
   549                self.image_obj.bg_channels[channel],
   550            )
   551
   552            cropped_contour, cropped_ml, cropped_msh = u.get_cropped_cell_data(
   553                cell.contour, cell.midline, cell.mesh, x_offset, y_offset
   554            )
   555            # Calculations without object mesh
   556            (
   557                rectl,
   558                rectw,
   559                cell.objects_features["cell_total_obj_rect_length"],
   560                cell.objects_features["cell_avg_obj_rect_width"],
   561            ) = u.get_object_rect_length(object_contours, px)
   562            aspect_r, cell.objects_features["cell_avg_obj_aspectratio"] = (
   563                u.get_object_aspect_ratio(rectl, rectw)
   564            )
   565            cell.objects_features["cell_avg_obj_width_dt"], width_no_dt = (
   566                u.get_object_avg_width(object_masks, px)
   567            )
   568            cell.objects_features["object_number"] = len(object_contours)
   569
   570            # Object area and NC ratio
   571            area_list, total_obj_area = u.get_object_area(object_contours, px)
   572            cell.objects_features["cell_total_obj_area"] = total_obj_area
   573            cell.objects_features["NC_ratio"] = total_obj_area / cell_area
   574
   575            # Curvature characteristics
   576            (
   577                curvatures,
   578                max_c,
   579                min_c,
   580                mean_c,
   581                std_c,
   582                cell.objects_features["cell_avg_obj_mean_c"],
   583                cell.objects_features["cell_avg_obj_std_c"],
   584            ) = u.get_object_curvature_characteristics(object_contours, px)
   585
   586            # Perimeter characteristics
   587            (
   588                perimeters,
   589                circularities,
   590                compactnesses,
   591                sphericities,
   592                avg_peri,
   593                avg_cir,
   594                avg_comp,
   595                avg_sph,
   596            ) = u.get_object_perimeter_measurements(object_contours, area_list, px)
   597            cell.objects_features["cell_avg_obj_perimeter"] = avg_peri
   598            cell.objects_features["cell_avg_obj_circularity"] = avg_cir
   599            cell.objects_features["cell_avg_obj_compactness"] = avg_comp
   600            cell.objects_features["cell_avg_obj_sphericity"] = avg_sph
   601
   602            (
   603                convexity,
   604                eccentricity,
   605                solidity,
   606                avg_convexity,
   607                avg_eccentricity,
   608                avg_solidity,
   609            ) = u.get_object_convexity(object_masks)
   610            cell.objects_features["cell_avg_obj_convexity"] = avg_convexity
   611            cell.objects_features["cell_avg_obj_eccentricity"] = avg_eccentricity
   612            cell.objects_features["cell_avg_obj_solidity"] = avg_solidity
   613
   614            # Object volume using approximation method (https://doi.org/10.1016/j.cell.2021.05.037)
   615            approx_object_volumes, total_volume = u.get_approx_nucleoid_volume(
   616                cell_volume, area_list, cell_area
   617            )
   618            cell.objects_features["cell_total_obj_volume_approx"] = total_volume
   619
   620            (
   621                contour_bending_energies,
   622                cell.objects_features["cell_obj_contour_avg_bending_energy"],
   623            ) = u.object_bending_energy(object_contours, px)
   624
   625            # A bad object mesh does not invalidate its detected contour. Check
   626            # every mesh before calculating any mesh-dependent aggregate.
   627            mesh_data_complete = (
   628                len(object_meshes) == len(object_contours)
   629                and len(object_midlines) == len(object_contours)
   630                and all(np.isfinite(area) and area > 0 for area in area_list)
   631                and all(
   632                    np.asarray(mesh).ndim == 2
   633                    and np.asarray(mesh).shape[0] >= 3
   634                    and np.asarray(mesh).shape[1] == 4
   635                    and np.all(np.isfinite(mesh))
   636                    and np.any(np.asarray(mesh)[:, 0])
   637                    and np.asarray(midline).ndim == 2
   638                    and np.asarray(midline).shape[0] >= 2
   639                    and np.asarray(midline).shape[1] == 2
   640                    and np.all(np.isfinite(midline))
   641                    and np.linalg.norm(midline[0] - midline[-1]) > 1e-9
   642                    for mesh, midline in zip(object_meshes, object_midlines)
   643                )
   644            )
   645            meshes_valid = False
   646            ratio_list = []
   647            if mesh_data_complete:
   648                obj_x1, obj_y1, obj_x2, obj_y2 = u.get_obj_mesh_coords(cropped_oms)
   649                obj_step_lengths, obj_lengths = u.get_object_step_length(
   650                    obj_x1, obj_y1, obj_x2, obj_y2, px
   651                )
   652                width_list, width_no, avg_obj_width = u.get_obj_avg_width(
   653                    obj_x1, obj_y1, obj_x2, obj_y2, px
   654                )
   655                ratio_list = u.ratio_mesh_over_total_area(
   656                    obj_step_lengths, width_no, area_list
   657                )
   658                meshes_valid = len(ratio_list) == len(object_contours) and all(
   659                    np.isfinite(ratio) and 0.9 <= ratio <= 1.1
   660                    for ratio in ratio_list
   661                )
   662
   663            mesh_feature_names = (
   664                "cell_total_obj_mesh_length",
   665                "cell_avg_obj_width",
   666                "cell_avg_obj_width_variability",
   667                "object_avg_consDegree",
   668                "cell_total_obj_volume",
   669                "cell_total_obj_surface_area",
   670                "cell_avg_obj_SOV",
   671                "cell_obj_midline_avg_bending_energy",
   672                "cell_avg_midline_sinuosity",
   673            )
   674            if meshes_valid:
   675                cropped_signal_interp2d = u.interp2d(cropped_signal)
   676                axial_intensity = u.measure_along_objects_midline_interp2d(
   677                    cropped_oml, cropped_signal_interp2d, width=5
   678                )
   679                cell.objects_features["cell_total_obj_mesh_length"] = sum(obj_lengths)
   680                cell.objects_features["cell_avg_obj_width"] = avg_obj_width
   681                width_var_list, cell.objects_features["cell_avg_obj_width_variability"] = (
   682                    u.get_object_width_variability(width_no)
   683                )
   684                (
   685                    constrDegree,
   686                    relPos,
   687                    absconstrDegree,
   688                    ctpos,
   689                    cell.objects_features["object_avg_consDegree"],
   690                    avg_abs_consDegree,
   691                ) = u.constr_degree(axial_intensity, obj_step_lengths, width_no)
   692                object_volumes, cell.objects_features["cell_total_obj_volume"] = (
   693                    u.get_nucleoid_volume(
   694                        obj_x1, obj_y1, obj_x2, obj_y2, obj_step_lengths, px
   695                    )
   696                )
   697                surface_area_list, cell.objects_features["cell_total_obj_surface_area"] = (
   698                    u.get_object_surface_area(width_no, obj_step_lengths)
   699                )
   700                sov_list, cell.objects_features["cell_avg_obj_SOV"] = (
   701                    u.get_object_surface_area_over_volume(
   702                        surface_area_list, object_volumes
   703                    )
   704                )
   705                (
   706                    midline_bending_energies,
   707                    cell.objects_features["cell_obj_midline_avg_bending_energy"],
   708                ) = u.object_bending_energy(object_midlines, px)
   709                sinuosity_list, cell.objects_features["cell_avg_midline_sinuosity"] = (
   710                    u.get_object_sinuosity(object_midlines)
   711                )
   712            else:
   713                bactoscoop_logger.debug(
   714                    "Object mesh quality failed for cell %s, channel %s "
   715                    "(complete=%s, ratios=%s); "
   716                    "%s",
   717                    cell.cell_id, channel, mesh_data_complete, ratio_list,
   718                    "keeping contour features and omitting mesh features"
   719                    if retain_contour_on_object_mesh_failure
   720                    else "omitting the objects feature row",
   721                )
   722                if not retain_contour_on_object_mesh_failure:
   723                    cell.objects_features = {}
   724                    cell.discard = True
   725                    return
   726                cell.objects_features.update(
   727                    {name: np.nan for name in mesh_feature_names}
   728                )
   729                sov_list = constrDegree = relPos = midline_bending_energies = sinuosity_list = np.nan
   730
   731            object_centroid_list = u.get_object_centroid(object_masks)
   732            (
   733                cell.objects_features["cell_avg_interobject_distance"],
   734                interobject_distances,
   735            ) = u.get_interobject_distance(object_centroid_list, px)
   736
   737            pole1, pole2 = u.get_pole_coordinates(cropped_msh)
   738
   739            (
   740                cell.objects_features["avg_pole_obj_distance"],
   741                min2distances,
   742                pole_distances,
   743            ) = u.get_pole_object_distances(cropped_msh, object_centroid_list, px)
   744            (
   745                cell.objects_features["l"],
   746                cell.objects_features["d"],
   747                cell.objects_features["l_norm"],
   748                cell.objects_features["d_norm"],
   749            ) = u.projection_on_midline(
   750                object_centroid_list, cropped_ml, cell_widthno, px
   751            )
   752            cell.objects_features["object_avg_center_distance"] = (
   753                u.get_avg_distance_from_center(cell.objects_features["l_norm"])
   754            )
   755
   756            # Texture features
   757            (
   758                glcm_object_dissimilarity,
   759                glcm_object_correlation,
   760                glcm_object_homogeneity,
   761                glcm_object_energy,
   762                glcm_object_contrast,
   763                glcm_object_entropy,
   764            ) = u.calculate_object_haralick_features(object_masks, cropped_signal)
   765            cell.objects_features["glcm_obj_dissimilarity"] = glcm_object_dissimilarity
   766            cell.objects_features["glcm_obj_correlation"] = glcm_object_correlation
   767            cell.objects_features["glcm_obj_homogeneity"] = glcm_object_homogeneity
   768            cell.objects_features["glcm_obj_energy"] = glcm_object_energy
   769            cell.objects_features["glcm_obj_contrast"] = glcm_object_contrast
   770            cell.objects_features["glcm_obj_shannon_entropy"] = glcm_object_entropy
   771
   772            object_intensity_values, object_intensities_list = u.get_object_intensities(
   773                object_masks, cropped_signal
   774            )
   775            cell.objects_features["objects_intensity_mean"] = np.mean(
   776                object_intensity_values
   777            )
   778            cell.objects_features["objects_intensity_std"] = np.std(
   779                object_intensity_values
   780            )
   781            cell.objects_features["objects_kurtosis"] = u.get_kurtosis(
   782                object_intensity_values
   783            )
   784            cell.objects_features["objects_skewness"] = u.get_skew(
   785                object_intensity_values
   786            )
   787
   788            if all_data:
   789                cell.objects_features["obj_areas"] = area_list
   790                cell.objects_features["obj_aspect_ratios"] = aspect_r
   791                cell.objects_features["max_obj_curvatures"] = max_c
   792                cell.objects_features["min_obj_curvatures"] = min_c
   793                cell.objects_features["mean_obj_curvatures"] = mean_c
   794                cell.objects_features["std_obj_curvatures"] = std_c
   795                cell.objects_features["obj_perimeters"] = perimeters
   796                cell.objects_features["obj_circularities"] = circularities
   797                cell.objects_features["obj_compactnesses"] = compactnesses
   798                cell.objects_features["obj_sphericities"] = sphericities
   799                cell.objects_features["obj_convexities"] = convexity
   800                cell.objects_features["obj_eccentricities"] = eccentricity
   801                cell.objects_features["obj_solidities"] = solidity
   802                cell.objects_features["obj_volumes_approx"] = approx_object_volumes
   803                cell.objects_features["obj_SOV"] = sov_list
   804                cell.objects_features["obj_constriction_degrees"] = constrDegree
   805                cell.objects_features["obj_relative_positions"] = relPos
   806                cell.objects_features["obj_contour_bending_energies"] = (
   807                    contour_bending_energies
   808                )
   809                cell.objects_features["obj_midline_bending_energies"] = (
   810                    midline_bending_energies
   811                )
   812                cell.objects_features["obj_interobject_distances"] = (
   813                    interobject_distances
   814                )
   815                cell.objects_features["obj_midline_sinuosities"] = sinuosity_list
   816                cell.objects_features["min_2_distances_from_pole"] = min2distances
   817                cell.objects_features["obj_pole_distances"] = pole_distances
   818                cell.objects_features["obj_intensity_values"] = object_intensity_values
   819                cell.objects_features["obj_mean_intensities"] = u.get_objects_mean(
   820                    object_intensities_list
   821                )
   822                cell.objects_features["obj_std_intensities"] = u.get_objects_std(
   823                    object_intensities_list
   824                )
   825                cell.objects_features["obj_kurtosis_intensities"] = (
   826                    u.get_objects_kurtosis(object_intensities_list)
   827                )
   828                cell.objects_features["obj_skewness_intensities"] = (
   829                    u.get_objects_skewness(object_intensities_list)
   830                )
   831
   832        else:
   833            raise ValueError("No object contours to calculate features from")
   834            cell.discard = True
   835
   836    def membrane(self, cell, channel, all_data, use_shifted_contours, shift_signal):
   837        cell.membrane_features = {}
   838        px = self.image_obj.px
   839
   840        if self.image_obj.channels is None or channel not in self.image_obj.channels:
   841            raise AttributeError(
   842                f"No objects are detected for channel {channel} or the channel is not loaded"
   843            )
   844            return
   845
   846        if channel not in self.image_obj.bg_channels:
   847            self.image_obj.get_bg_channel(channel)
   848            bactoscoop_logger.debug("Background subtraction completed")
   849
   850        if cell.object_meshdata[channel]["object_contour"]:
   851
   852            contour, midline, mesh = self.contour_selection(cell, use_shifted_contours)
   853
   854            if shift_signal:
   855                cropped_signal, cropped_mask, _, x_offset, y_offset = u.crop_image(
   856                    self.image_obj.bg_channels[channel],
   857                    contour,
   858                    phase_img=self.image_obj.image,
   859                )
   860            else:
   861                cropped_signal, cropped_mask, _, x_offset, y_offset = u.crop_image(
   862                    self.image_obj.bg_channels[channel],
   863                    contour,
   864                    phase_img=None
   865                )
   866
   867            cropped_contour, _, cropped_msh = u.get_cropped_cell_data(
   868                contour, midline, mesh, x_offset, y_offset
   869            )
   870            cropped_signal_interp2d = u.interp2d(cropped_signal)
   871
   872            p2p_contour = u.mesh2contour(
   873                cropped_msh[:, 0], cropped_msh[:, 1], cropped_msh[:, 2], cropped_msh[:, 3]
   874            )
   875            eroded_contour = u.erode_contour(p2p_contour, scale=2)
   876            eroded_contour_aligned = u.align_contour_start(eroded_contour, [cropped_msh[0, 0], cropped_msh[0, 1]])
   877            area = u.get_area(cropped_contour, px)
   878            cell.membrane_features["cell_perimeter"], _, _, _ = (
   879                u.get_cell_perimeter_measurements(cropped_contour, area, px)
   880            )
   881            binned_eroded_contour = u.change_contour_array_length(
   882                cell.membrane_features["cell_perimeter"], eroded_contour_aligned
   883            )
   884
   885            contour_intensities = u.measure_intensity_interp2d(
   886                binned_eroded_contour, cropped_signal_interp2d, width=6
   887            )
   888            contour_intensity_profile = u.gaussian_smoothing(contour_intensities)
   889            cell.membrane_features["contour_intensity"] = contour_intensity_profile
   890            cell.membrane_features["normalized_contour_intensity"] = u.normalize_per_cell(
   891                cell.membrane_features["contour_intensity"]
   892            )
   893            cell.membrane_features["complemented_contour_intensity"] = (
   894                u.get_complemented_contour_intensity(
   895                    cell.membrane_features["normalized_contour_intensity"]
   896                )
   897            )
   898
   899            cell.membrane_features["contour_intensity_mean"] = np.mean(
   900                contour_intensity_profile
   901            )
   902            cell.membrane_features["contour_intensity_std"] = np.std(
   903                contour_intensity_profile
   904            )
   905            cell.membrane_features["contour_intensity_kurtosis"] = u.get_kurtosis(
   906                contour_intensity_profile
   907            )
   908            cell.membrane_features["contour_intensity_skewness"] = u.get_skew(
   909                contour_intensity_profile
   910            )
   911            cell.membrane_features["contour_gradient_homogeneity"] = u.get_homogeneity(
   912                cell.membrane_features["normalized_contour_intensity"]
   913            )
   914
   915            (
   916                glcm_contour_dissimilarity,
   917                glcm_contour_correlation,
   918                glcm_contour_homogeneity,
   919                glcm_contour_energy,
   920                glcm_contour_contrast,
   921                glcm_contour_entropy,
   922            ) = u.calculate_contour_haralick_features(contour_intensities)
   923            cell.membrane_features["glcm_contour_dissimilarity"] = (
   924                glcm_contour_dissimilarity
   925            )
   926            cell.membrane_features["glcm_contour_correlation"] = glcm_contour_correlation
   927            cell.membrane_features["glcm_contour_homogeneity"] = glcm_contour_homogeneity
   928            cell.membrane_features["glcm_contour_energy"] = glcm_contour_energy
   929            cell.membrane_features["glcm_contour_contrast"] = glcm_contour_contrast
   930            cell.membrane_features["glcm_contour_shannon_entropy"] = glcm_contour_entropy
   931
   932            self.discard_bad_meshes(cell)
   933        else:
   934            raise ValueError("No object contours to calculate features from")
   935            cell.discard = True
   936#not yet functional 
   937    def colocalization(
   938        self,
   939        cell,
   940        signal1,
   941        signal2,
   942        shift_signal1,
   943        shift_signal2,
   944        all_data,
   945        use_shifted_contours,
   946    ):
   947
   948        cell.signal_correlation_features = {}
   949        px = self.image_obj.px
   950
   951        if (
   952            self.image_obj.channels is None
   953            or signal1 not in self.image_obj.channels
   954            or signal2 not in self.image_obj.channels
   955        ):
   956            raise AttributeError(
   957                f"Channel {signal1} or {signal2} is not detected or not loaded"
   958            )
   959
   960        if signal1 not in self.image_obj.bg_channels:
   961            self.image_obj.get_bg_channel(signal1)
   962            bactoscoop_logger.debug("Background subtraction completed for signal1")
   963
   964        if signal2 not in self.image_obj.bg_channels:
   965            self.image_obj.get_bg_channel(signal2)
   966            bactoscoop_logger.debug("Background subtraction completed for signal2")
   967
   968        contour, midline, mesh = self.contour_selection(cell, use_shifted_contours)
   969
   970        cropped_signal1, cropped_mask1, _, x_offset1, y_offset1 = (
   971            u.crop_signal_with_optional_shift(
   972                self.image_obj, signal1, contour, shift_signal1
   973            )
   974        )
   975        cropped_signal2, cropped_mask2, _, x_offset2, y_offset2 = (
   976            u.crop_signal_with_optional_shift(
   977                self.image_obj, signal2, contour, shift_signal2
   978            )
   979        )