def extract_labels_from_mask(mask: np.ndarray, color_id_to_obj_class: collections.Mapping) -> list: """ Extract multiclass instances from grayscale mask and save it to labels list. Args: mask: multiclass grayscale mask color_id_to_obj_class: dict of objects classes assigned to color id (e.g. {1: ObjClass('cat), ...}) Returns: list of labels with bitmap geometry """ from skimage import measure from scipy import ndimage zero_offset = 1 if 0 in color_id_to_obj_class else 0 if zero_offset > 0: mask = mask + zero_offset labeled, labels_count = measure.label(mask, connectivity=1, return_num=True) objects_slices = ndimage.find_objects(labeled) labels = [] for object_index, slices in enumerate(objects_slices, start=1): crop = mask[slices] sub_mask = crop * (labeled[slices] == object_index).astype(np.int) class_index = np.max(sub_mask) - zero_offset if class_index in color_id_to_obj_class: bitmap = Bitmap(data=sub_mask.astype(np.bool), origin=PointLocation(slices[0].start, slices[1].start)) label = Label(geometry=bitmap, obj_class=color_id_to_obj_class.get(class_index)) labels.append(label) return labels
def test_to_contours(self): bitmap = Bitmap(data=np.array( [[1, 1, 0, 1, 1, 1], [1, 1, 0, 1, 0, 1], [0, 0, 0, 1, 1, 1], [1, 0, 0, 1, 0, 1], [1, 0, 0, 1, 1, 1], [1, 0, 0, 0, 0, 0], [1, 0, 0, 1, 1, 1]], dtype=np.bool), origin=PointLocation(10, 110)) polygons = bitmap.to_contours() exteriors_points = [ np.array([[10, 113], [14, 113], [14, 115], [10, 115]]), np.array([[10, 110], [11, 110], [11, 111], [10, 111]]) ] interiors_points = [[], [ np.array([[13, 113], [12, 114], [13, 115], [14, 114]]), np.array([[11, 113], [10, 114], [11, 115], [12, 114]]) ], []] self.assertEqual(len(polygons), 2) for polygon, target_exterior, target_interiors in zip( polygons, exteriors_points, interiors_points): self.assertTrue( np.equal(polygon.exterior_np, target_exterior).all()) self.assertTrue( all( np.equal(p_inter, t_inter) for p_inter, t_inter in zip( polygon.interior_np, target_interiors))) json.dumps(polygon.to_json()) self.assertIsInstance(polygon, Polygon)
def geometry_to_bitmap(geometry, radius: int = 0, crop_image_shape: tuple = None) -> list: """ Args: geometry: Geometry type which implemented 'draw', 'translate' and 'to_bbox` methods radius: half of thickness of drawed vector elements crop_image_shape: if not None - crop bitmap object by this shape (HxW) Returns: Bitmap (geometry) object """ thickness = radius + 1 bbox = geometry.to_bbox() extended_bbox = Rectangle(top=bbox.top - radius, left=bbox.left - radius, bottom=bbox.bottom + radius, right=bbox.right + radius) bitmap_data = np.full(shape=(extended_bbox.height, extended_bbox.width), fill_value=False) geometry = geometry.translate(-extended_bbox.top, -extended_bbox.left) geometry.draw(bitmap_data, color=True, thickness=thickness) origin = PointLocation(extended_bbox.top, extended_bbox.left) bitmap_geometry = Bitmap(data=bitmap_data, origin=origin) if crop_image_shape is not None: crop_rect = Rectangle.from_size(*crop_image_shape) return bitmap_geometry.crop(crop_rect) return [bitmap_geometry]
def from_json(cls, json_data): ''' The function from_json convert bitmap from json format to Bitmap class object. :param json_data: input bitmap in json format :return: Bitmap class object ''' json_root_key = cls._impl_json_class_name() if json_root_key not in json_data: raise ValueError( 'Data must contain {} field to create MultichannelBitmap object.' .format(json_root_key)) if ORIGIN not in json_data[json_root_key] or DATA not in json_data[ json_root_key]: raise ValueError( '{} field must contain {} and {} fields to create MultichannelBitmap object.' .format(json_root_key, ORIGIN, DATA)) col, row = json_data[json_root_key][ORIGIN] data = cls.base64_2_data(json_data[json_root_key][DATA]) labeler_login = json_data.get(LABELER_LOGIN, None) updated_at = json_data.get(UPDATED_AT, None) created_at = json_data.get(CREATED_AT, None) sly_id = json_data.get(ID, None) class_id = json_data.get(CLASS_ID, None) return cls(data=data, origin=PointLocation(row=row, col=col), sly_id=sly_id, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at)
def crop(self, rect): ''' Crop the current Polygon with a given rectangle, if polygon cat't be cropped it generate exception error :param rect: Rectangle class object :return: list of Poligon class objects ''' from supervisely.geometry.point_location import PointLocation try: # points = [ # PointLocation(row=rect.top, col=rect.left), # PointLocation(row=rect.top, col=rect.right + 1), # PointLocation(row=rect.bottom + 1, col=rect.right + 1), # PointLocation(row=rect.bottom + 1, col=rect.left) # ] points = [ PointLocation(row=rect.top, col=rect.left), PointLocation(row=rect.top, col=rect.right), PointLocation(row=rect.bottom, col=rect.right), PointLocation(row=rect.bottom, col=rect.left) ] #points = rect.corners # old implementation with 1 pixel error (right bottom) # #@TODO: investigate here (critical issue) clipping_window_shpl = ShapelyPolygon(points_to_row_col_list(points)) self_shpl = ShapelyPolygon(self.exterior_np, holes=self.interior_np) intersections_shpl = self_shpl.buffer(0).intersection(clipping_window_shpl) mapping_shpl = mapping(intersections_shpl) except Exception: logger.warn('Polygon cropping exception, shapely.', exc_info=True) # raise # if polygon is invalid, just print warning and skip it # @TODO: need more investigation here return [] intersections = shapely_figure_to_coords_list(mapping_shpl) # Check for bad cropping cases (e.g. empty points list) out_polygons = [] for intersection in intersections: if isinstance(intersection, list) and len(intersection) > 0 and len(intersection[0]) >= 3: exterior = row_col_list_to_points(intersection[0], do_round=True) interiors = [] for interior_contour in intersection[1:]: if len(interior_contour) > 2: interiors.append(row_col_list_to_points(interior_contour, do_round=True)) out_polygons.append(Polygon(exterior, interiors)) return out_polygons
def setUp(self): self.origin = PointLocation(0, 4) self.mask = np.array([[0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0]], dtype=np.bool) self.mask_no_margins = self.mask[:, 1:-1] self.bitmap = Bitmap(data=self.mask, origin=self.origin)
def from_json(cls, data): ''' The function from_json convert Node from json format to Node class object. :param data: input node in json format :return: Node class object ''' # TODO validations loc = data[LOC] return cls(location=PointLocation(row=loc[1], col=loc[0]), disabled=data.get(DISABLED, False))
def transform_point(self, point): ''' The function transform_point calculates new parameters of point after rotating :param point: PointLocation class object :return: PointLocation class object ''' point_np_uniform = np.array([point.row, point.col, 1]) transformed_np = self.affine_matrix.dot(point_np_uniform) # Unwrap numpy types so that round() produces integer results. return PointLocation(row=round(transformed_np[0].item()), col=round(transformed_np[1].item()))
def flipud(self, img_size): ''' The function fliplr flip the current Bitmap object in vertical and return the copy of the current Bitmap object :param img_size: size of the image :return: Bitmap class object with new data(numpy array) and PointLocation ''' flipped_mask = np.flip(self.data, axis=0) flipped_origin = PointLocation( row=(img_size[0] - flipped_mask.shape[0] - self.origin.row), col=self.origin.col) return self.__class__(data=flipped_mask, origin=flipped_origin)
def __init__(self, data: np.ndarray, origin: PointLocation = None, expected_data_dims=None, sly_id=None, class_id=None, labeler_login=None, updated_at=None, created_at=None): super().__init__(sly_id=sly_id, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at) """ :param origin: PointLocation class object :param data: np.ndarray """ if origin is None: origin = PointLocation(row=0, col=0) if not isinstance(origin, PointLocation): raise TypeError( 'BitmapBase "origin" argument must be "PointLocation" object!') if not isinstance(data, np.ndarray): raise TypeError( 'BitmapBase "data" argument must be numpy array object!') data_dims = len(data.shape) if expected_data_dims is not None and data_dims != expected_data_dims: raise ValueError( 'BitmapBase "data" argument must be a {}-dimensional numpy array. ' 'Instead got {} dimensions'.format(expected_data_dims, data_dims)) self._origin = origin.clone() self._data = data.copy()
def crop(self, rect): ''' Crop the current MultichannelBitmap object with a given rectangle :param rect: Rectangle class object :return: MultichannelBitmap class object ''' maybe_cropped_area = self.to_bbox().crop(rect) if len(maybe_cropped_area) == 0: return [] else: [cropped_area] = maybe_cropped_area cropped_origin = PointLocation(row=cropped_area.top, col=cropped_area.left) cropped_area_in_data = cropped_area.translate(drow=-self._origin.row, dcol=-self.origin.col) return [MultichannelBitmap(data=cropped_area_in_data.get_cropped_numpy_slice(self._data), origin=cropped_origin,)]
def rotate(self, rotator): ''' The function rotate render the bitmap within the full image canvas and rotate the whole canvas with a given rotator (ImageRotator class object contain size of image and angle to rotate) :param rotator: ImageRotator class object :return: MultichannelBitmap class object ''' full_img_data = np.zeros(rotator.src_imsize + self.data.shape[2:], dtype=self.data.dtype) full_img_data[ self.origin.row:(self.origin.row + self.data.shape[0]), self.origin.col:(self.origin.col + self.data.shape[1]), ...] = self.data[:, :, ...] rotated_full_data = rotator.rotate_img(full_img_data, use_inter_nearest=True) # Rotate the bounding box to find out the bounding box of the rotated bitmap within the full image. rotated_bbox = self.to_bbox().rotate(rotator) rotated_origin = PointLocation(row=rotated_bbox.top, col=rotated_bbox.left) return MultichannelBitmap(data=rotated_bbox.get_cropped_numpy_slice(rotated_full_data), origin=rotated_origin)
def from_json(cls, data): ''' The function from_json convert Point from json format to Point class object. :param data: input Point in json format :return: Point class object ''' labeler_login = data.get(LABELER_LOGIN, None) updated_at = data.get(UPDATED_AT, None) created_at = data.get(CREATED_AT, None) sly_id = data.get(ID, None) class_id = data.get(CLASS_ID, None) return cls.from_point_location(PointLocation.from_json(data), sly_id=sly_id, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at)
def resize_origin_and_bitmap(origin: PointLocation, bitmap: np.ndarray, in_size, out_size): ''' Change PointLocation and resize numpy array to match a certain size :return: new PointLocation class object and numpy array ''' new_size = restore_proportional_size(in_size=in_size, out_size=out_size) row_scale = new_size[0] / in_size[0] col_scale = new_size[1] / in_size[1] # TODO: Double check (+restore_proportional_size) or not? bitmap.shape and in_size are equal? # Make sure the resulting size has at least one pixel in every direction (i.e. limit the shrinkage to avoid having # empty bitmaps as a result). scaled_rows = max(round(bitmap.shape[0] * row_scale), 1) scaled_cols = max(round(bitmap.shape[1] * col_scale), 1) scaled_origin = PointLocation(row=round(origin.row * row_scale), col=round(origin.col * col_scale)) scaled_bitmap = resize_inter_nearest(bitmap, (scaled_rows, scaled_cols)) return scaled_origin, scaled_bitmap
def crop(self, rect): ''' Crop the current Bitmap object with a given rectangle :param rect: Rectangle class object :return: Bitmap class object ''' maybe_cropped_bbox = self.to_bbox().crop(rect) if len(maybe_cropped_bbox) == 0: return [] else: [cropped_bbox] = maybe_cropped_bbox cropped_bbox_relative = cropped_bbox.translate( drow=-self.origin.row, dcol=-self.origin.col) cropped_mask = cropped_bbox_relative.get_cropped_numpy_slice( self._data) if not np.any(cropped_mask): return [] return [ Bitmap(data=cropped_mask, origin=PointLocation(row=cropped_bbox.top, col=cropped_bbox.left)) ]
def setUp(self): self.origin = PointLocation(row=0, col=4) self.data = np.array([[[0.0, 0.1], [0.2, 0.3], [0.4, 0.5]], [[0.6, 0.7], [0.8, 0.9], [1.0, 1.1]]], dtype=np.float64) self.bitmap = MultichannelBitmap(data=self.data, origin=self.origin)
def point_location(self) -> PointLocation: ''' The function point_location create PointLocation class object from Point class object :return: PointLocation class object ''' return PointLocation(row=self.row, col=self.col)