def get_parent_object_mask_image(self, parent_object_id): '''Extracts the bounding box for a given parent object from :attr:`parent_label_image <jtlib.features.PointPattern.parent_label_image>`. Returns ------- numpy.ndarray[bool] mask image for given object ''' bbox = self._parent_bboxes[parent_object_id] img = utils.extract_bbox(self.parent_label_image, bbox=bbox, pad=1) return img == parent_object_id
def get_object_mask_image(self, object_id): '''Extracts the bounding box for a given object from :attr:`label_image <jtlib.features.Features.label_image>`. Returns ------- numpy.ndarray[bool] mask image for given object ''' bbox = self._bboxes[object_id] img = utils.extract_bbox(self.label_image, bbox=bbox, pad=1) return img == object_id
def get_object_intensity_image(self, object_id): '''Extracts the bounding box for a given object from :attr:`intensity_image <jtlib.features.Features.intensity_image>`. Returns ------- numpy.ndarray[numpy.uint16 or numpy.uint8] intensity image for given object; the size of the image is determined by the bounding box of the object ''' if self.intensity_image is None: raise ValueError('No intensity image available.') bbox = self._bboxes[object_id] return utils.extract_bbox(self.intensity_image, bbox=bbox, pad=1)
def get_points_object_label_image(self, parent_object_id): '''Extracts the bounding box for a given parent object from :attr:`label_image <jtlib.features.PointPattern.label_image>`. Parameters ---------- parant_object_id: int ID of object in :attr:`parent_label_image <jtlib.features.PointPattern.parent_label_image>` Returns ------- numpy.ndarray[numpy.int32] label image for all objects falling within the bounding box of the given parent object ''' bbox = self._parent_bboxes[parent_object_id] parent_img = self.get_parent_object_mask_image(parent_object_id) img = utils.extract_bbox(self.label_image, bbox=bbox, pad=1) img[~parent_img] = 0 return img
def separate_clumped_objects(clumps_image, min_cut_area, min_area, max_area, max_circularity, max_convexity): '''Separates objects in `clumps_image` based on morphological criteria. Parameters ---------- clumps_image: numpy.ndarray[Union[numpy.int32, numpy.bool]] objects that should be separated min_cut_area: int minimal area an object must have (prevents cuts that would result in too small objects) min_area: int minimal area an object must have to be considered a clump max_area: int maximal area an object can have to be considered a clump max_circularity: float maximal circularity an object must have to be considerd a clump max_convexity: float maximal convexity an object must have to be considerd a clump Returns ------- numpy.ndarray[numpy.uint32] separated objects See also -------- :class:`jtlib.features.Morphology` ''' logger.info('separate clumped objects') label_image, n_objects = mh.label(clumps_image) if n_objects == 0: logger.debug('no objects') return label_image pad = 1 cutting_pass = 1 separated_image = label_image.copy() while True: logger.info('cutting pass #%d', cutting_pass) cutting_pass += 1 label_image = mh.label(label_image > 0)[0] f = Morphology(label_image) values = f.extract() index = ( (min_area < values['Morphology_Area']) & (values['Morphology_Area'] <= max_area) & (values['Morphology_Convexity'] <= max_convexity) & (values['Morphology_Circularity'] <= max_circularity) ) clumped_ids = values[index].index.values not_clumped_ids = values[~index].index.values if len(clumped_ids) == 0: logger.debug('no more clumped objects') break mh.labeled.remove_regions(label_image, not_clumped_ids, inplace=True) mh.labeled.relabel(label_image, inplace=True) bboxes = mh.labeled.bbox(label_image) for oid in np.unique(label_image[label_image > 0]): bbox = bboxes[oid] logger.debug('process clumped object #%d', oid) obj_image = extract_bbox(label_image, bboxes[oid], pad=pad) obj_image = obj_image == oid # Rescale distance intensities to make them independent of clump size dist = mh.stretch(mh.distance(obj_image)) # Find peaks that can be used as seeds for the watershed transform thresh = mh.otsu(dist) peaks = dist > thresh n = mh.label(peaks)[1] if n == 1: logger.debug( 'only one peak detected - perform iterative erosion' ) # Iteratively shrink the peaks until we have two peaks that we # can use to separate the clump. while True: tmp = mh.morph.open(mh.morph.erode(peaks)) n = mh.label(tmp)[1] if n == 2 or n == 0: if n == 2: peaks = tmp break peaks = tmp # Select the two biggest peaks, since we want only two objects. peaks = mh.label(peaks)[0] sizes = mh.labeled.labeled_size(peaks) index = np.argsort(sizes)[::-1][1:3] for label in np.unique(peaks): if label not in index: peaks[peaks == label] = 0 peaks = mh.labeled.relabel(peaks)[0] regions = mh.cwatershed(np.invert(dist), peaks) # Use the line separating watershed regions to make the cut se = np.ones((3,3), np.bool) line = mh.labeled.borders(regions, Bc=se) line[~obj_image] = 0 line = mh.morph.dilate(line) # Ensure that cut is reasonable given user-defined criteria test_cut_image = obj_image.copy() test_cut_image[line] = False subobjects, n_subobjects = mh.label(test_cut_image) sizes = mh.labeled.labeled_size(subobjects) smaller_object_area = np.min(sizes) smaller_id = np.where(sizes == smaller_object_area)[0][0] smaller_object = subobjects == smaller_id do_cut = ( (smaller_object_area > min_cut_area) & (np.sum(line) > 0) ) if do_cut: logger.debug('cut object #%d', oid) y, x = np.where(line) y_offset, x_offset = bboxes[oid][[0, 2]] - pad - 1 y += y_offset x += x_offset label_image[y, x] = 0 separated_image[y, x] = 0 else: logger.debug('don\'t cut object #%d', oid) mh.labeled.remove_regions(label_image, oid, inplace=True) return mh.label(separated_image)[0]
def separate_clumped_objects(clumps_image, min_cut_area, min_area, max_area, max_circularity, max_convexity, allow_trimming=True): '''Separates objects in `clumps_image` based on morphological criteria. Parameters ---------- clumps_image: numpy.ndarray[Union[numpy.int32, numpy.bool]] objects that should be separated min_cut_area: int minimal area an object must have (prevents cuts that would result in too small objects) min_area: int minimal area an object must have to be considered a clump max_area: int maximal area an object can have to be considered a clump max_circularity: float maximal circularity an object must have to be considerd a clump max_convexity: float maximal convexity an object must have to be considerd a clump allow_trimming: boolean Some cuts may create a tiny third object. If this boolean is true, tertiary objects < trimming_threshold (10) pixels will be removed Returns ------- numpy.ndarray[numpy.uint32] separated objects See also -------- :class:`jtlib.features.Morphology` ''' logger.info('separate clumped objects') trimming_threshold = 10 label_image, n_objects = mh.label(clumps_image, NEIGHBORHOOD8) if n_objects == 0: logger.debug('no objects') return label_image pad = 1 cutting_pass = 1 separated_image = label_image.copy() while True: logger.info('cutting pass #%d', cutting_pass) cutting_pass += 1 label_image = mh.label(label_image > 0, NEIGHBORHOOD8)[0] f = Morphology(label_image) values = f.extract() index = ((min_area < values['Morphology_Area']) & (values['Morphology_Area'] <= max_area) & (values['Morphology_Convexity'] <= max_convexity) & (values['Morphology_Circularity'] <= max_circularity)) clumped_ids = values[index].index.values not_clumped_ids = values[~index].index.values if len(clumped_ids) == 0: logger.debug('no more clumped objects') break mh.labeled.remove_regions(label_image, not_clumped_ids, inplace=True) mh.labeled.relabel(label_image, inplace=True) bboxes = mh.labeled.bbox(label_image) for oid in np.unique(label_image[label_image > 0]): bbox = bboxes[oid] logger.debug('process clumped object #%d', oid) obj_image = extract_bbox(label_image, bboxes[oid], pad=pad) obj_image = obj_image == oid # Rescale distance intensities to make them independent of clump size dist = mh.stretch(mh.distance(obj_image)) # Find peaks that can be used as seeds for the watershed transform thresh = mh.otsu(dist) peaks = dist > thresh n = mh.label(peaks)[1] if n == 1: logger.debug( 'only one peak detected - perform iterative erosion') # Iteratively shrink the peaks until we have two peaks that we # can use to separate the clump. while True: tmp = mh.morph.open(mh.morph.erode(peaks)) n = mh.label(tmp)[1] if n == 2 or n == 0: if n == 2: peaks = tmp break peaks = tmp # Select the two biggest peaks, since we want only two objects. peaks = mh.label(peaks)[0] sizes = mh.labeled.labeled_size(peaks) index = np.argsort(sizes)[::-1][1:3] for label in np.unique(peaks): if label not in index: peaks[peaks == label] = 0 peaks = mh.labeled.relabel(peaks)[0] regions = mh.cwatershed(np.invert(dist), peaks) # Use the line separating watershed regions to make the cut line = mh.labeled.borders(regions, NEIGHBORHOOD8) line[~obj_image] = 0 # Ensure that cut is reasonable given user-defined criteria test_cut_image = obj_image.copy() test_cut_image[line] = False subobjects, n_subobjects = mh.label(test_cut_image, NEIGHBORHOOD8) sizes = mh.labeled.labeled_size(subobjects) smaller_object_area = np.min(sizes) # Deal with an edge-case: If trimming is active & there are more # than 2 objects created by the cut, check if they are very small. # If so, remove them. if allow_trimming and n_subobjects > 2 and smaller_object_area < trimming_threshold: tiny_objects = np.nonzero( sizes < trimming_threshold)[0].tolist() # Remove objects by adding them to the cutting line for trim_obj in tiny_objects: line[subobjects == trim_obj] = True logger.debug('Trimming an object of size: {}'.format( sizes[trim_obj])) # Redo calculation if split should be applied test_cut_image = obj_image.copy() test_cut_image[line] = False subobjects, n_subobjects = mh.label(test_cut_image, NEIGHBORHOOD8) sizes = mh.labeled.labeled_size(subobjects) smaller_object_area = np.min(sizes) logger.debug('Number of objects: {}'.format(n_subobjects)) do_cut = ((smaller_object_area > min_cut_area) & (np.sum(line) > 0)) if do_cut: logger.debug('cut object #%d', oid) y, x = np.where(line) y_offset, x_offset = bboxes[oid][[0, 2]] - pad y += y_offset x += x_offset label_image[y, x] = 0 separated_image[y, x] = 0 else: logger.debug('don\'t cut object #%d', oid) mh.labeled.remove_regions(label_image, oid, inplace=True) return mh.label(separated_image, NEIGHBORHOOD8)[0]