def test_slicing(): """Test the `slice_depth` method.""" column = CoreColumn(img1, top=1.0, base=2.0) height = column.height # Superset should have no effect superset_column = column.slice_depth(top=0.0, base=3.0) assert superset_column == column # These should reduce data slice1 = column.slice_depth(base=1.5) assert slice1.height < column.height slice2 = column.slice_depth(top=1.5) assert slice2.height < column.height # These should not be allowed with pytest.raises(AssertionError): _ = column.slice_depth(top=1.75, base=1.25) with pytest.raises(AssertionError): _ = column.slice_depth(top=2.5) with pytest.raises(AssertionError): _ = column.slice_depth(base=0.5)
def test_image_only_save_load(): """Test numpy image-only save.""" save_column = CoreColumn(img1, top=1.0, base=2.0) with tempfile.TemporaryDirectory() as TEMP_PATH: save_column.save(TEMP_PATH, name='testcol', pickle=False, image=True, depths=False) load_column = CoreColumn.load(TEMP_PATH, 'testcol', top=1.0, base=2.0) assert load_column == save_column, 'Loaded should match saved.'
def test_addition(): """Test various column combination possibilities.""" column1 = CoreColumn(img1, top=1.0, base=2.0) column2 = CoreColumn(img2, top=2.0, base=3.0) column3 = CoreColumn(img3, top=3.0, base=4.0) # Adjacent images -> should not fill between one_plus_two = column1 + column2 assert one_plus_two.height == (height1 + height2) # Gap is bigger than default `add_tol` with pytest.raises(UserWarning): _ = column1 + column3 # Should only be able to add in depth order with pytest.raises(UserWarning): _ = column2 + column1 # Change `add_tol` column1.add_tol = 1.0 column1.add_mode = "collapse" one_plus_three = column1 + column3 assert one_plus_three.height == (height1 + height3), "collapse == naive vstack" # Make sure 'fill' ends up filling column1.add_mode = "fill" one_plus_three = column1 + column3 assert one_plus_three.height > ( height1 + height3), "`fill` should have filled something"
def test_construction(): """Try various construction arguments that should fail or succeed.""" # 1D array should fail with pytest.raises(ValueError): _ = CoreColumn(np.random.random(100), top=1.0, base=2.0) # 4D array should fail with pytest.raises(ValueError): _ = CoreColumn(np.expand_dims(img1, -1), top=1.0, base=2.0) # No depth info should fail with pytest.raises(AssertionError): _ = CoreColumn(img1) # Just depths should be fine _ = CoreColumn(img1, depths=np.linspace(1.0, 2.0, num=img1.shape[0])) # Depth and image size mismatch should fail with pytest.raises(AssertionError): _ = CoreColumn(img1, depths=np.linspace(1.0, 2.0, num=100)) # Grayscale should be allowed gray_column = CoreColumn(color.rgb2gray(img1), top=1.0, base=2.0) assert gray_column.channels == 1, "Grayscale image should have 1 channel"
def segment( self, img, depth_range, add_tol=None, add_mode="fill", layout_params={}, show=False, colors=None, ): """Detect and segment core columns in `img`, return single aggregated `CoreColumn` instance. Parameters ---------- img : str or array Filename or RGB image array to segment. depth_range : list(float) Top and bottom depths of set of columns in image. add_tol : float, optional Tolerance for adding discontinuous `CoreColumn`s. Default=None results in tolerance ~ image resolution. add_mode : one of {'fill', 'collapse'}, optional Add mode for generated `CoreColumn` instances (see `CoreColumn` docs) layout_params : dict, optional Any layout parameters to override. show : boolean, optional Set to True to show image with predictions overlayed. colors : list, optional A list of RGBA tuples, one for each in `class_names` (excluding 'BG'). Values should be in range [0.0, 1.0], If None, uses random colors. Has no effect unless `show=True`. Returns ------- img_col : CoreColumn Single aggregated ``CoreColumn`` instance """ # Note: assignment calls setter to update, checks validity self.layout_params = layout_params # Is `depth_range` sane? if min(depth_range) == 0.0 or depth_range[1] - depth_range[0] == 0.0: raise UserWarning( f"`depth_range` {depth_range} starts at 0.0 or has no extent") # If `img` points to a file, read it. Otherwise assumed to be valid image array. if isinstance(img, (str, Path)): print(f"Reading file: {img}") img = io.imread(img) # Set up expected number of columns and their top/base depths col_tops, col_bases = self.expected_tops_bases( depth_range, self.layout_params["col_height"]) num_expected = len(col_tops) # Get MRCNN column predictions preds = self.model.detect([img], verbose=0)[0] if show: if colors is not None: assert len(colors) == ( len(self.class_names) - 1), "Number of `colors` must match number of classes" colors = [colors[i - 1] for i in preds["class_ids"]] viz.show_preds(img, preds, self.class_names, colors=colors) # Select masks for column class col_masks = preds["masks"][:, :, preds["class_ids"] == self.column_class_id] # Check that number of columns matches expectation num_cols = col_masks.shape[-1] if num_cols != num_expected: raise UserWarning( f"Number of detected columns {num_cols} does not match \ expectation of {num_expected}") # Convert 3D binary masks to 2D integer labels array col_labels = utils.masks_to_labels(col_masks) # Get sorted `skimage` regions for column masks col_regions = utils.sort_regions(measure.regionprops(col_labels), self.layout_params["order"]) # Figure out crop endpoints, set related args crop_axis = 0 if self.layout_params["orientation"] == "l2r" else 1 # Set up `endpts` for bbox adjustment if self.endpts_is_auto: if self.layout_params["endpts"] == "auto": regions = col_regions else: # 'auto_all' mode regions = measure.regionprops( utils.masks_to_labels(preds["masks"])) endpts = utils.maximum_extent(regions, crop_axis) elif self.endpts_is_class: measure_idxs = np.where( preds["class_ids"] == self.endpts_class_id)[0] # If object not detected, then ignore for cropping if measure_idxs.size == 0: print( "`endpts` class not detected, cropping will use `auto` method" ) regions = col_regions # Otherwise, use bbox of instance with highest confidence score else: best_idx = measure_idxs[np.argmax( preds["scores"][measure_idxs])] regions = measure.regionprops( (preds["masks"][:, :, best_idx]).astype(np.int)) endpts = utils.maximum_extent(regions, crop_axis) elif self.endpts_is_coords: endpts = self.layout_params["endpts"] else: raise RuntimeError() # Set single argument lambda functions to apply to column regions / region images crop_fn = lambda region: utils.crop_region( img, col_labels, region, axis=crop_axis, endpts=endpts) transform_fn = lambda region: utils.rotate_vertical( region, self.layout_params["orientation"]) # Apply cropping and rotation to column regions crops = [transform_fn(crop_fn(region)) for region in col_regions] # Assemble `CoreColumn` objects from masked/cropped image regions cols = [ CoreColumn(crop, top=t, base=b, add_tol=add_tol, add_mode=add_mode) for crop, t, b in zip(crops, col_tops, col_bases) ] # Slice the bottom depth if necessary cols[-1] = cols[-1].slice_depth(base=depth_range[1]) # Return the concatenation of all column objects return reduce(add, cols)