def test_rightmost_coordinate(self): # Polygon dummy_record = Record([[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert geometry.rightmost_coordinate == (1, 0) # Point dummy_record = Record([0, 1], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert geometry.rightmost_coordinate == (0, 1)
def test_coordinates(self): # Polygon dummy_record = Record([[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert geometry.coordinates == geometry._coordinates # Point dummy_record = Record([0, 1], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert geometry.coordinates == geometry._coordinates
def test_min_x(self): # Polygon dummy_record = Record([[[0, 0], [1, 1], [2, 2], [3, 3], [0, 0]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert isinstance(geometry.min_x, int) assert geometry.min_x == 0 # Point dummy_record = Record([0, 0], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) assert isinstance(geometry.min_x, int) assert geometry.min_x == 0
def test_centroid(self): # Polygon dummy_record = Record([[[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) centroid = geometry.centroid assert isinstance(centroid, tuple) assert centroid == (1, 1) # Point dummy_record = Record([1, 2], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) centroid = geometry.centroid assert isinstance(centroid, tuple) assert centroid == (1, 2)
def data_point_like(): # Fake records records = [ Record([[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ('ship', ), confidence=0.9), Record([[[710, 100], [710, 150], [760, 150], [760, 100], [710, 100]]], ('boat', ), confidence=0.857), Record([[[500, 500], [550, 550], [500, 600], [450, 550], [500, 500]]], ('boat', ), confidence=0.72), Record([[[250, 0], [300, 0], [275, 25], [300, 50], [250, 50], [275, 25], [250, 0]]], ('boat', ), confidence=0), Record([[[300, 300], [320, 280], [420, 380], [400, 400], [300, 300]]], ('boat', ), confidence=0.3), Record([[[650, 300], [670, 280], [770, 380], [750, 400], [650, 300]]], ('ship', ), confidence=0.1), Record([[[-20, 600], [50, 600], [50, 650], [-20, 650], [-20, 600]]], ('ship', ), confidence=0.927), Record([[[700, 600], [800, 600], [800, 650], [700, 650], [700, 600]]], ('vessel', ), confidence=0.927), Record([[[500, -50], [550, -50], [550, 50], [500, 50], [500, -50]]], ('boat', ), confidence=0.5647), Record([[[500, 750], [550, 750], [550, 850], [500, 850], [500, 750]]], ('vessel', ), confidence=0.1763), ] # Create collection records_collection = RecordCollection(*records) # Instantiate a tile tile = TileWrapper(np.zeros((768, 768, 3), dtype=np.uint8), filename=Path('84c1b28caecf.png')) # Create a data point like return tile, records_collection
def test_add_title(self): import PIL.Image import numpy as np # Create data points records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ['car'], confidence=0.9), ] records_collection = RecordCollection(*records) annotation = Annotation(records_collection) tile = TileWrapper(np.zeros((100, 100, 3)), filename='test.png') data_point = DataPoint(tile, annotation) data_points = [data_point] # Color engine interface simple_categorical_interface = { 'name': 'Name(Main, Secondary)', 'type': 'categorical', 'schema': { 'Ship': Color(26, 188, 156, ctype='sRGB255'), 'Car': Color(241, 196, 15, ctype='sRGB255'), 'Truck': Color(41, 128, 185, ctype='sRGB255'), 'Wind-turbines': Color(236, 240, 241, ctype='sRGB255') } } # Parameters width, height = (300, 300) background_color = (0, 0, 0) title_size = 25 # Init compositor _compositor = Compositor( data_points=data_points, color_engine_interface=simple_categorical_interface) image = PIL.Image.fromarray(np.zeros((width, height))) # Add title final_image = _compositor._add_title(mosaic=image, title='', background_color=background_color, title_size=title_size) assert isinstance(final_image, PIL.Image.Image) assert final_image.width == width assert final_image.height == height + 2 * title_size
def test_compute_label_starting_point(self): # Check dummy_record = Record([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) starting_point = TagPainter._compute_label_starting_point( geometry=geometry, position=Position.RIGHT) assert isinstance(starting_point, tuple) assert starting_point == (1, 0) starting_point = TagPainter._compute_label_starting_point( geometry=geometry, position=Position.LEFT) assert isinstance(starting_point, tuple) assert starting_point == (0, 0)
def test_compute_label_coordinates(self): # Parameters _painter = TagPainter(Confidence()) # Check dummy_record = Record([[[0, 5], [20, 5], [20, 25], [0, 25], [0, 5]]], ('car', ), confidence=0.9) geometry = Geometry(record=dummy_record) text = '' font = PIL.ImageFont.load_default() # size = 11 text_width, text_height = font.getsize(text) tile_width = 50 label_coordinates, text_coordinates = _painter._compute_label_coordinates( geometry=geometry, text=text, font=font, tile_width=tile_width) assert isinstance(label_coordinates, list) assert len(label_coordinates) == 5 for coordinate in label_coordinates: assert isinstance(coordinate, tuple) assert len(coordinate) == 2 assert isinstance(text_coordinates, tuple)
def _make_record(*property_pair): r = Record([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], ('dummy', ), 2.0) for property_name, value in property_pair: if property_name not in ('labels', 'confidence'): r.properties[property_name] = value return r
def test_record_collection(self): import geojson r = Record([0, 1, 2], ('car', 'road vehicle'), some_property='some property', another_property=45) r2 = Record([[[0, 0], [0, 1], [1, 1], [0, 0]]], ('car', 'road vehicle'), some_property='some property', another_property=42) r3 = Record([[[0, 0], [0, 1], [1, 1], [0, 0]]], ('car', 'road vehicle'), some_property='some properties', another_property=42) rc = RecordCollection(r, r2) crc_frc = CategoricalRecordCollection.from_record_collection( 'another_property', rc) crc = CategoricalRecordCollection('another_property', r, r2) assert len(crc) == 2 assert len(crc_frc) == 2 assert crc[1] == r2 assert crc_frc[1] == r2 assert crc[1, 0] == r2 assert crc_frc[1, 0] == r2 crc[1, 0] = r3 crc_frc[1, 0] = r3 assert crc[1, 0] == r3 assert crc_frc[1, 0] == r3 assert crc.loc[42, 0] == r3 assert crc_frc.loc[42, 0] == r3 crc.loc[42, 0] = r2 crc_frc.loc[42, 0] = r2 assert crc.loc[42, 0] == r2 assert crc_frc.loc[42, 0] == r2 assert geojson.dumps(crc, sort_keys=True) == '{"features": [' \ '{"geometry": {"coordinates": [0, 1, 2], "type": "Point"}, ' \ '"properties": {"another_property": 45, ' \ '"category": ["car", "road vehicle"], ' \ '"confidence": null, "some_property": "some property"}, ' \ '"type": "Feature"}, ' \ '{"geometry": {"coordinates": ' \ '[[[0, 0], [0, 1], [1, 1], [0, 0]]], ' \ '"type": "Polygon"}, ' \ '"properties": {"another_property": 42, ' \ '"category": ["car", "road vehicle"], ' \ '"confidence": null, ' \ '"some_property": "some property"}, "type": "Feature"}], ' \ '"type": "FeatureCollection"}' assert geojson.dumps(crc_frc, sort_keys=True) == '{"features": [' \ '{"geometry": {"coordinates": [0, 1, 2], "type": "Point"}, ' \ '"properties": {"another_property": 45, ' \ '"category": ["car", "road vehicle"], ' \ '"confidence": null, "some_property": "some property"}, ' \ '"type": "Feature"}, ' \ '{"geometry": {"coordinates": ' \ '[[[0, 0], [0, 1], [1, 1], [0, 0]]], ' \ '"type": "Polygon"}, ' \ '"properties": {"another_property": 42, ' \ '"category": ["car", "road vehicle"], ' \ '"confidence": null, ' \ '"some_property": "some property"}, "type": "Feature"}], ' \ '"type": "FeatureCollection"}'
def test_constructor(self): # Create data points records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ['car'], confidence=0.9), ] records_collection = RecordCollection(*records) annotation = Annotation(records_collection) tile = TileWrapper(np.zeros((100, 100, 3)), filename='test.png') tile_2 = TileWrapper(np.zeros((200, 200, 3)), filename='test_2.png') data_point = DataPoint(tile, annotation) data_point_2 = DataPoint(tile_2, annotation) # Color engine interface simple_categorical_interface = { 'name': 'Name(Main, Secondary)', 'type': 'categorical', 'schema': { 'Ship': Color(26, 188, 156, ctype='sRGB255'), 'Car': Color(241, 196, 15, ctype='sRGB255'), 'Truck': Color(41, 128, 185, ctype='sRGB255'), 'Wind-turbines': Color(236, 240, 241, ctype='sRGB255') } } # Valid datapoints data_points_1 = [data_point] data_points_2 = [data_point, data_point_2] data_points_3 = (data_point_2, ) data_points_4 = (data_point, data_point_2) data_points_5 = [data_points_1] data_points_6 = [data_points_3] data_points_7 = (data_points_2, ) data_points_8 = (data_points_4, ) # Invalid data points inv_data_points_1 = None inv_data_points_2 = ['test'] inv_data_points_3 = ('test', data_point) # Checks Compositor(data_points=data_points_1, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_2, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_3, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_4, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_5, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_6, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_7, color_engine_interface=simple_categorical_interface) Compositor(data_points=data_points_8, color_engine_interface=simple_categorical_interface) with pytest.raises(AttributeError): Compositor(data_points=inv_data_points_1, color_engine_interface=simple_categorical_interface) with pytest.raises(AttributeError): Compositor(data_points=inv_data_points_2, color_engine_interface=simple_categorical_interface) with pytest.raises(AttributeError): Compositor(data_points=inv_data_points_3, color_engine_interface=simple_categorical_interface)
def test_plot(self): import PIL.Image import numpy as np # Create data points records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ['car'], confidence=0.9, confidence_confidence=0.9, color=Color(26, 188, 156)), ] records_collection = RecordCollection(*records) annotation = Annotation(records_collection) tile = TileWrapper(np.zeros((100, 100, 3), dtype=np.uint8), filename='test.png') data_point = DataPoint(tile, annotation) data_points = [] # Legend simple_categorical_interface = { 'name': 'Name(Main, Secondary)', 'type': 'categorical', 'schema': { 'Ship': Color(26, 188, 156, ctype='sRGB255'), 'Car': Color(241, 196, 15, ctype='sRGB255'), 'Truck': Color(41, 128, 185, ctype='sRGB255'), 'Wind-turbines': Color(236, 240, 241, ctype='sRGB255') } } kwargs = { 'plot_centers': False, 'plot_confidences': True, 'zoom': 1, 'alpha': 128, 'scale': 1, 'axis': 0, 'background_color': (48, 56, 68, 255), 'item_margins': (10, 10), 'main_axis_align': 'start', 'minor_axis_align': 'start' } # Accumulates datapoints (flattened) nb_datapoints = 15 for _ in range(nb_datapoints): data_points.append(data_point) # Parameters _compositor = Compositor( data_points=data_points, color_engine_interface=simple_categorical_interface) n_cols = 10 margins = (5, 5) # Test with neither title nor legend (use AdaptiveImagePositionGenerator) final_image = _compositor.plot(n_cols=n_cols, margins=margins, title=None, center=True, **kwargs) assert isinstance(final_image, PIL.Image.Image) assert final_image.width > n_cols * (100 + 2 * margins[0]) assert final_image.height > 2 * (100 + 2 * margins[1]) # 2 rows assert final_image.height < 3 * (100 + 2 * margins[1] ) # but less than 3 rows # Test with neither title nor legend (use SimpleImagePositionGenerator) final_image = _compositor.plot(n_cols=n_cols, margins=margins, title=None, center=False, **kwargs) assert isinstance(final_image, PIL.Image.Image) assert final_image.width > n_cols * (100 + 2 * margins[0]) assert final_image.height > 2 * (100 + 2 * margins[1]) # 2 rows assert final_image.height < 3 * (100 + 2 * margins[1] ) # but less than 3 rows # Nested datapoints nested_data_points = [[data_point, data_point, data_point, data_point], [data_point, data_point, data_point], [data_point, data_point, data_point, data_point], [ data_point, data_point, data_point, data_point, data_point ], [data_point, data_point]] _compositor = Compositor( data_points=nested_data_points, color_engine_interface=simple_categorical_interface) final_image = _compositor.plot(n_cols=n_cols, margins=margins, title=None, center=True, **kwargs) # Check 5 cols and 5 rows (plus each tile title) painter_title_height = 70 assert isinstance(final_image, PIL.Image.Image) assert final_image.width > 5 * (100 + 2 * margins[0]) assert final_image.height > 5 * (100 + 2 * margins[1]) assert final_image.height < 5 * (100 + painter_title_height + 2 * margins[1]) # Add title final_image_with_title = _compositor.plot(n_cols=n_cols, margins=margins, title='Test', center=True, **kwargs) assert isinstance(final_image, PIL.Image.Image) assert final_image_with_title.width == final_image.width assert final_image_with_title.height > final_image.height # Add legend and title final_image_with_legend = _compositor.plot(n_cols=n_cols, margins=margins, title='Test', center=True, **kwargs) assert isinstance(final_image, PIL.Image.Image) assert final_image_with_legend.width == final_image_with_title.width assert final_image_with_legend.height == final_image_with_title.height
def test_add_legend(self): import PIL.Image import numpy as np # Create data points records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ['car'], confidence=0.9), ] records_collection = RecordCollection(*records) annotation = Annotation(records_collection) tile = TileWrapper(np.zeros((100, 100, 3)), filename='test.png') data_point = DataPoint(tile, annotation) data_points = [data_point] # Legend simple_categorical_interface = { 'name': 'Name(Main, Secondary)', 'type': 'categorical', 'schema': { 'Ship': Color(26, 188, 156, ctype='sRGB255'), 'Car': Color(241, 196, 15, ctype='sRGB255'), 'Truck': Color(41, 128, 185, ctype='sRGB255'), 'Wind-turbines': Color(236, 240, 241, ctype='sRGB255') } } legend_config = { 'scale': 1, 'axis': 0, 'item_margins': (10, 10), 'main_axis_align': 'start', 'minor_axis_align': 'start' } # Parameters width, height = (300, 300) background_color = (0, 0, 0) # Init compositor _compositor = Compositor( data_points=data_points, color_engine_interface=simple_categorical_interface) image = PIL.Image.fromarray(np.zeros((width, height))) # Add legend final_image = _compositor._add_legend( mosaic=image, background_color=background_color, **legend_config) # Checks (vertical mode) assert isinstance(final_image, PIL.Image.Image) assert final_image.height == height assert final_image.width > width # Checks horizontal mode legend_config['axis'] = 1 final_image = _compositor._add_legend( mosaic=image, background_color=background_color, **legend_config) assert isinstance(final_image, PIL.Image.Image) assert final_image.width == width assert final_image.height > height
def test_draw(self): # Fake records invalid_records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ('car', ), confidence=0.9), Record([[[100, 100], [110, 110], [100, 100]]], ('car', ), confidence=0.9), ] records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ('car', ), confidence=0.9, color=Color(255, 128, 128)), Record([[[100, 100], [100, 150], [100, 100]]], ('car', ), confidence=0.9, color=Color(128, 255, 255)), Record([100, 100], ('ship', ), confidence=0.3, color=Color(128, 255, 255)), ] # Create collection invalid_records_collection = RecordCollection(*invalid_records) records_collection = RecordCollection(*records) # Finally, the annotation invalid_annotation = Annotation(invalid_records_collection) annotation = Annotation(records_collection) # Instantiate a tile tile = TileWrapper(np.zeros((300, 300, 4), dtype=np.uint8), filename='test.png') # Create a datapoint invalid_datapoint = DataPoint(tile, invalid_annotation) datapoint = DataPoint(tile, annotation) # Parameters _painter = Painter(fill=True) # Sanity check with pytest.raises(AttributeError): _painter.draw(invalid_datapoint) # Draw datapoint final_image = _painter.draw(datapoint) assert isinstance(final_image, PIL.Image.Image) assert final_image.width == 300 assert final_image.height == 300 + _painter.TITLE_HEIGHT # With centers _painter = Painter(plot_centers=True) # Sanity check with pytest.raises(AttributeError): _painter.draw(invalid_datapoint) # Draw datapoint final_image = _painter.draw(datapoint) assert isinstance(final_image, PIL.Image.Image) assert final_image.width == 300 assert final_image.height == 300 + _painter.TITLE_HEIGHT # Test zoom zoom = 2.5 _painter = Painter(zoom=zoom) final_image = _painter.draw(datapoint) assert isinstance(final_image, PIL.Image.Image) assert final_image.width == 300 * zoom assert final_image.height == (300 + _painter.TITLE_HEIGHT) * zoom # Check invalid geometry class InvalidRecord(object): def __init__(self, type, coordinates): self.id = '1' self.type = type self.coordinates = coordinates self.labels = Label('test'), self.color = Color(255, 128, 128, ctype='sRGB255') records = [ Record( [[[100, 100], [100, 150], [150, 150], [150, 100], [100, 100]]], ('car', ), confidence=0.9, color=Color(255, 128, 128)), Record([[[100, 100], [100, 150], [100, 100]]], ('car', ), confidence=0.9, color=Color(128, 255, 255)), InvalidRecord(type='Test', coordinates=[]) ] # Create data point records_collection = RecordCollection(*records) annotation = Annotation(records_collection) tile = TileWrapper(np.zeros((300, 300, 4), dtype=np.uint8), filename='test.png') data_point = DataPoint(tile, annotation) _painter = Painter(fill=True) with pytest.raises(ValueError): _painter.draw(data_point)
def test_constructor(self): dummy_record_list = Record([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], ('car', ), confidence=0.9) dummy_record_tuple = Record((( (0, 0), (0, 1), (1, 1), (1, 0), (0, 0), ), ), ('car', ), confidence=0.9) geometry_list = Geometry(record=dummy_record_list) geometry_tuple = Geometry(record=dummy_record_tuple) # Check coordinates format assert isinstance(geometry_list.coordinates, list) for i, coordinate in enumerate(geometry_list.coordinates): assert isinstance(coordinate, tuple) assert coordinate == tuple(dummy_record_list.coordinates[0][i]) assert isinstance(geometry_tuple.coordinates, list) for i, coordinate in enumerate(geometry_tuple.coordinates): assert isinstance(coordinate, tuple) assert coordinate == tuple(dummy_record_tuple.coordinates[0][i]) # Check exceptions class InvalidRecord(object): def __init__(self, type, coordinates): self.type = type self.coordinates = coordinates with pytest.raises(ValueError): Geometry(record=object()) with pytest.raises(ValueError): Geometry(record=InvalidRecord(type='Point', coordinates=[1])) with pytest.raises(ValueError): Geometry(record=InvalidRecord(type='Point', coordinates=[1, 2, 3])) with pytest.raises(ValueError): Geometry(record=InvalidRecord(type='Polygon', coordinates=[[]])) with pytest.raises(ValueError): Geometry(record=InvalidRecord( type='Polygon', coordinates=[[[0, 0], [1, 0], [1, 1]]])) with pytest.raises(ValueError): Geometry(record=InvalidRecord( type='Test', coordinates=[[[0, 0], [1, 0], [1, 1]]])) with pytest.raises(TypeError): Geometry(record=InvalidRecord(type='Point', coordinates='Test')) with pytest.raises(TypeError): Geometry( record=InvalidRecord(type='Polygon', coordinates=['Hello'])) # Test zoom = 2 dummy_record = Record([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]], ('car', ), confidence=0.9) expected_coordinates = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] geometry_list = Geometry(record=dummy_record, zoom=2) assert isinstance(geometry_list.coordinates, list) for i, coordinate in enumerate(geometry_list.coordinates): assert isinstance(coordinate, tuple) assert coordinate == expected_coordinates[i] # Test zoom = 0.6 dummy_record = Record([[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]], ('car', ), confidence=0.9) expected_coordinates = [(0, 0), (0, 6), (6, 6), (6, 0), (0, 0)] geometry_list = Geometry(record=dummy_record, zoom=0.6) assert isinstance(geometry_list.coordinates, list) for i, coordinate in enumerate(geometry_list.coordinates): assert isinstance(coordinate, tuple) assert coordinate == expected_coordinates[i]
def draw(self): """Draw the legend, along the given axis. The generated legend is a grid with each cell having the dimensions of the biggest item to draw. The grid is then filled along the main axis first. The size of the legend along the main axis is fixed while the size along the minor one is variable. Returns: :class:`~PIL.Image.Image`: The output legend. """ # Init legend item drawer drawer = LegendItemDrawer(background_color=self._background_color, scale=self._scale, margins=self._item_margins) margin = int(drawer.TEXT_SIZE * self._scale) # Draw all items items = self._draw_items(drawer) # No items to draw means empty legend if not items: return PIL.Image.new(mode='RGBA', size=(0, 0)) # Compute position for each item position_generator = LegendItemPositionGenerator( items_sizes=[item.size for item in items], axis=self._axis, max_size_along_axis=self._max_size_along_axis - 2 * margin, main_axis_align=self._main_axis_align, minor_axis_align=self._minor_axis_align) items_with_position = [ (items[i], position) for i, position in enumerate(list(position_generator)) ] # Prepare tag if needed upper_margin = 0 draw = None if self._plot_tag: upper_margin = int( (2 * 2 + 0.75 * drawer.TEXT_SIZE + 2 * 4) * self._scale) # Prepare record and descriptor tag_descriptor = CategoricalDescriptor(name='name') tag_record = Record( coordinates=[[[0.5 * margin, int(upper_margin / 2)], [0.5 * margin, int(upper_margin / 2) + 1], [0.5 * margin + 1, int(upper_margin / 2) + 1], [0.5 * margin + 1, int(upper_margin / 2)], [0.5 * margin, int(upper_margin / 2)]]], labels=['Dummy'], name=self._tag_descriptor.__descriptor__['name'], categorical_descriptor_name=0.5, color=Color(*get_outline_color(self._background_color), ctype='sRGB255')) tag_descriptor.update(RecordCollection(tag_record)) # Prepare TagPainter and Draw tag_painter = TagPainter(tag_descriptor, text_margin=2 * self._scale, text_size=0.75 * drawer.TEXT_SIZE * self._scale) draw = Draw(size=(int(position_generator.legend_size[0] + 2 * margin), upper_margin), zoom=1, mode='RGBA', background_color=self._background_color) # Draw legend tag tag_painter.draw(tag_record, draw=draw) # Create legend legend = PIL.Image.new( mode='RGBA', size=(position_generator.legend_size[0] + 2 * margin, position_generator.legend_size[1] + 2 * margin + upper_margin), color=self._background_color) for item, position in items_with_position: legend.alpha_composite(im=item, dest=(position[0] + margin, position[1] + margin + upper_margin)) if draw is not None: legend.alpha_composite(im=draw.overlay, dest=(0, 4)) if self._color_engine_interface['type'] == 'categorical': bottom_margin = position_generator.legend_size[ 1] - position_generator.true_legend_size[1] drawer.draw_outline_with_title(self._primary_descriptor_name, legend, margins=(0, upper_margin, 0, bottom_margin)) return legend