Source code for agora.track_abc

#!/usr/bin/env jupyter

import typing as t
from abc import ABC
from os.path import join

import numpy as np
from skimage.measure import regionprops_table

from aliby.track.utils import calc_barycentre, pick_baryfun


[docs]class FeatureCalculator(ABC): """ Base class for making use of regionprops-based features. If no features are offered it uses most of them. This class is not to be used directly """ a_ind = None ma_ind = None x_ind = None y_ind = None
[docs] def __init__( self, feats2use: t.Collection[str], trapfeats: t.Optional[t.Collection[str]] = None, extrafeats: t.Optional[t.Collection[str]] = None, aweights: t.Optional[bool] = None, pixel_size: t.Optional[float] = None, ) -> None: self.feats2use = feats2use if trapfeats is None: trapfeats = () self.trapfeats = tuple(trapfeats) if extrafeats is None: extrafeats = () self.extrafeats = tuple(extrafeats) if aweights is None: aweights = None self.aweights = aweights if pixel_size is None: pixel_size = 0.182 self.pixel_size = pixel_size self.outfeats = self.get_outfeats() self.set_named_ids() self.tfeats = self.outfeats + self.tmp_outfeats + self.trapfeats self.ntfeats = len(self.tfeats)
def get_outfeats( self, feats2use: t.Optional[t.Collection[str]] = None ) -> tuple: if feats2use is None: feats2use = self.feats2use outfeats = tuple( regionprops_table(np.diag((1, 0)), properties=feats2use).keys() ) return outfeats def set_named_ids(self): # Manage calling feature outputs by name d = {"centroid-0": "xind", "centroid-1": "yind", "area": "aind"} tmp_d = { "barydist": ["centroid", "area"], "baryangle": ["centroid", "area"], "distance": ["centroid"], } nonbase_feats = self.trapfeats + self.extrafeats tmp_infeats = np.unique([j for x in nonbase_feats for j in tmp_d[x]]) self.tmp_infeats = tuple( [f for f in tmp_infeats if f not in self.feats2use] ) # feats that are only used to calculate others tmp_outfeats = ( self.get_outfeats(feats2use=tmp_infeats) if len(tmp_infeats) else [] ) self.tmp_outfeats = [] for feat in tmp_outfeats: if feat in self.outfeats: setattr(self, d[feat], self.outfeats.index(feat)) else: # Only add them if not in normal outfeats self.tmp_outfeats.append(feat) setattr( self, d[feat], len(self.outfeats) + self.tmp_outfeats.index(feat), ) self.tmp_outfeats = tuple(self.tmp_outfeats) self.out_merged = self.outfeats + self.tmp_outfeats def load_model(self, path, fname): model_file = join(path, fname) with open(model_file, "rb") as file_to_load: model = pickle.load(file_to_load) return model
[docs] def calc_feats_from_mask( self, masks: np.ndarray, feats2use: t.Optional[t.Tuple[str]] = None, trapfeats: t.Optional[t.Tuple[str]] = None, scale: t.Optional[bool] = True, pixel_size: t.Optional[float] = None, ): """ Calculate feature ndarray from ndarray of cell masks --- input :masks: ndarray (ncells, x_size, y_size), typically dtype bool :feats2use: list of strings with the feature properties to extract. If it is None it uses the ones set in self.feats2use. :trapfeats: List of str with additional features to use calculated immediately after basic features. :scale: bool, if True scales mask to a defined pixel_size. :pixel_size: float, used to rescale the object features. returns (ncells, nfeats) ndarray of features for input masks """ if pixel_size is None: pixel_size = self.pixel_size if feats2use is None: feats2use = self.feats2use + self.tmp_infeats if trapfeats is None: trapfeats = self.trapfeats ncells = masks.shape[0] if masks.ndim == 3 else masks.max() feats = np.empty((ncells, self.ntfeats)) # ncells * nfeats if masks.any(): if masks.ndim == 3: # Individual cells in dim 0 assert masks.sum( axis=(1, 2) ).all(), "Dimension with at least one empty outline slice" cell_feats = np.array( [ [ x[0] for x in regionprops_table( mask.astype(int), properties=feats2use ).values() ] for mask in masks ] ) elif masks.ndim == 2: # No overlap between cells cell_feats = np.array( [ x for x in regionprops_table( masks.astype(int), properties=feats2use ).values() ] ).T else: raise Exception( "TrackerException: masks do not have the appropiate dimensions" ) if scale: cell_feats = self.scale_feats(cell_feats, pixel_size) # Fill first sector, with directly extracted features feats[:, : len(self.out_merged)] = cell_feats if trapfeats: # Add additional features tfeats = self.calc_trapfeats(feats) feats[:, len(self.out_merged) :] = tfeats else: feats = np.zeros((0, self.ntfeats)) return feats
[docs] def calc_trapfeats(self, basefeats): """ Calculate trap-based features using basic ones. :basefeats: (n basic outfeats) 1-D array with features outputed by skimage.measure.regionprops_table requires self.aind self.aweights self.xind self.yind self.trapfeats returns (ntrapfeats) 1-D array with """ if self.aweights is not None: weights = basefeats[:, self.aind] else: weights = None barycentre = calc_barycentre( basefeats[:, [self.xind, self.yind]], weights=weights ) trapfeat_nd = np.empty((basefeats.shape[0], len(self.trapfeats))) for i, trapfeat in enumerate(self.trapfeats): trapfeat_nd[:, i] = pick_baryfun(trapfeat)( basefeats[:, [self.xind, self.yind]], barycentre ) return trapfeat_nd
[docs] def scale_feats(self, feats: np.ndarray, pixel_size: float): """ input :feats: np.ndarray (ncells * nfeatures) :pixel_size: float Value used to normalise the images. returns Rescaled list of feature values """ area = pixel_size**2 scaling = {None: 1, "linear": pixel_size, "square": area} degrees_feats = { None: ["eccentricity", "extent", "orientation", "solidity"], "linear": [ "centroid-0", "centroid-1", "minor_axis_length", "major_axis_length", "perimeter", "perimeter_crofton", "equivalent_diameter", ], "square": [ "area", "convex_area", "bbox_area", "equivalent_diameter", ], } scaler = { feat: scaling[k] for k, feats in degrees_feats.items() for feat in feats } # for k in feats.keys(): # feats[k] /= scaler[k] return feats * [scaler[feat] for feat in self.out_merged]