import pathlib
from typing import Any, Callable, List, Optional, Sequence, Type, Union
import ipywidgets as widgets
import traitlets
from ..base import LabellingWidgetMixin
from .canvases.abstract_canvas import AbstractAnnotationCanvas
from .canvases.box import BoundingBoxAnnotationCanvas
from .canvases.point import PointAnnotationCanvas
from .canvases.polygon import PolygonAnnotationCanvas
class Annotator(LabellingWidgetMixin, widgets.VBox):
"""A generic image annotation widget.
Parameters
----------
canvas : AbstractAnnotationCanvas
An annotation canvas that implements displaying & annotating
images.
options : List[str], optional
The list of classes you'd like to annotate.
data_postprocessor : Optional[Callable[[List[dict]], Any]], optional
A function that transforms the annotation data. By default None.
"""
options = traitlets.List(
list(), allow_none=False, help="The possible classes"
)
options.__doc__ = """The possible classes"""
CanvasClass: Type[AbstractAnnotationCanvas]
def __init__(
self,
canvas_size=(700, 500),
options: Sequence[str] = (),
data_postprocessor: Optional[Callable[[List[dict]], Any]] = None,
**kwargs,
):
"""Create an annotation widget for images.
Parameters
----------
canvas_size : tuple, optional, by default (700, 500)
options : Sequence[str], optional
The classes to be annotated, by default ()
data_postprocessor : Optional[Callable[[List[dict]], Any]], optional
A function to post-process the data, by default None.
"""
layout = {"width": f"{canvas_size[0]}px"}
layout.update(kwargs.pop("layout", {}))
super().__init__(layout=layout)
self.canvas = self.CanvasClass(canvas_size, classes=options)
self.data_postprocessor = data_postprocessor
# controls for the data entry:
data_controls = []
self.options = options
self.class_selector = widgets.Dropdown(
description="Class:",
options=options,
layout=widgets.Layout(flex="1 1 auto"),
)
widgets.link((self, "options"), (self.class_selector, "options"))
widgets.link(
(self.class_selector, "value"), (self.canvas, "current_class")
)
data_controls.append(self.class_selector)
extra_buttons = []
button_layout = widgets.Layout(
# width="auto",
min_width="80px",
flex="1 1 auto",
max_width="120px",
)
extra_buttons = [self.undo_button, self.skip_button]
if hasattr(self.canvas, "editing"):
self.edit_button = widgets.ToggleButton(
description="Edit", icon="pencil", layout=button_layout
)
widgets.link((self.edit_button, "value"), (self.canvas, "editing"))
extra_buttons.append(self.edit_button)
extra_buttons = widgets.HBox(
extra_buttons,
layout={
"align_items": "stretch",
"justify_content": "flex-end",
"flex_flow": "row wrap",
},
)
self.data_controls = widgets.VBox(
children=(
widgets.HTML("Data input settings"),
widgets.HBox(
(self.class_selector,),
layout={
"align_items": "stretch",
"justify_content": "flex-end",
},
),
extra_buttons,
widgets.HBox(
(self.submit_button,),
layout={"justify_content": "flex-end"},
),
),
layout={"flex": "1 1 auto", "max_width": "600px"},
)
# controls for the visualisation of the data:
viz_controls = []
if hasattr(self.canvas, "opacity"):
self.opacity_slider = widgets.FloatSlider(
description="Opacity", value=0.5, min=0, max=1, step=0.025
)
widgets.link(
(self.opacity_slider, "value"), (self.canvas, "opacity")
)
viz_controls.append(self.opacity_slider)
if hasattr(self.canvas, "point_size"):
self.point_size_slider = widgets.IntSlider(
description="Point size", value=5, min=1, max=20, step=1
)
widgets.link(
(self.point_size_slider, "value"), (self.canvas, "point_size")
)
viz_controls.append(self.point_size_slider)
self.brightness_slider = widgets.FloatLogSlider(
description="Brightness", value=1, min=-1, max=1, step=0.0001
)
widgets.link(
(self.brightness_slider, "value"),
(self.canvas, "image_brightness"),
)
viz_controls.append(self.brightness_slider)
self.contrast_slider = widgets.FloatLogSlider(
description="Contrast", value=1, min=-1, max=1, step=0.0001
)
widgets.link(
(self.contrast_slider, "value"), (self.canvas, "image_contrast")
)
viz_controls.append(self.contrast_slider)
self.visualisation_controls = widgets.VBox(
children=(widgets.HTML("Visualisation settings"), *viz_controls),
layout={"flex": "1 1 auto"},
)
self.all_controls = widgets.HBox(
children=(self.visualisation_controls, self.data_controls),
layout={
"width": f"{self.canvas.width}px",
"justify_content": "space-between",
},
)
self.submit_callbacks: List[Callable[[Any], None]] = []
self.undo_callbacks: List[Callable[[], None]] = []
self.skip_callbacks: List[Callable[[], None]] = []
self.children = self.children + (
self.canvas,
self.all_controls,
self.canvas.error_output_widget,
)
def display(self, image: Union[widgets.Image, pathlib.Path]):
"""Clear the annotations and display an image
Parameters
----------
image : widgets.Image, pathlib.Path, np.ndarray
The image, or the path to the image.
"""
self.canvas.clear()
self.canvas.load_image(image)
@property
def data(self):
"""The annotation data."""
if self.data_postprocessor is not None:
return self.data_postprocessor(self.canvas.data)
else:
return self.canvas.data
def undo(self, _: Optional[Any] = None): # noqa: D001
if self.canvas._undo_queue:
undo = self.canvas._undo_queue.pop()
undo()
else:
super().undo()
def _handle_keystroke(self, event):
super()._handle_keystroke(event)
keys = [str(i) for i in range(1, 10)] + ["0"]
for key, option in zip(keys, self.class_selector.options):
if event.get("key") == key:
self.class_selector.value = option
[docs]class PolygonAnnotator(Annotator):
"""An annotator for drawing polygons on an image.
To draw a polygon, click anywhere you'd like to start. Continue to click
along the edge of the polygon until arrive back where you started. To
finish, simply click the first point (highlighted in red). It may be
helpful to increase the point size if you're struggling (using the slider).
You can change the class of a polygon using the dropdown menu while the
polygon is still "open", or unfinished. If you make a mistake, use the Undo
button until the point that's wrong has disappeared.
You can move, but not add / subtract polygon points, by clicking the "Edit"
button. Simply drag a point you want to adjust. Again, if you have
difficulty aiming at the points, you can increase the point size.
You can increase or decrease the contrast and brightness of the image
using the sliders to make it easier to annotate. Sometimes you need to see
what's behind already-created annotations, and for this purpose you can
make them more see-through using the "Opacity" slider.
You can select the class of the polygon you are creating by choosing it
from the dropdown menu, or by using the hotkeys 1-0 (mapped in order in
which the classes appear in the dropdown).
Parameters
----------
canvas_size : (int, int), optional
Size of the annotation canvas in pixels.
classes : List[str], optional
The list of classes you want to create annotations for, by default
None.
"""
CanvasClass = PolygonAnnotationCanvas
@property
def data(self):
"""
The annotation data, as List[ Dict ].
The format is a list of dictionaries, with the following key / value
combinations:
+------------------+-------------------------+
|``'type'`` | ``'polygon'`` |
+------------------+-------------------------+
|``'label'`` | ``<class label>`` |
+------------------+-------------------------+
|``'points'`` | ``<list of xy-tuples>`` |
+------------------+-------------------------+
"""
return super().data
@data.setter
def data(self, value): # noqa: D001
self.canvas.data = value
[docs]class PointAnnotator(Annotator):
"""An annotator for drawing points on an image.
To add a point, select the class using the dropdown menu, and click
anywhere on the image. You can undo adding points, and you can adjust the
point's position using the "Edit" button. To make this easier, you may
want to adjust the point size using the appropriate slider.
You can increase or decrease the contrast and brightness of the image
using the sliders to make it easier to annotate. Sometimes you need to see
what's behind already-created annotations, and for this purpose you can
make them more see-through using the "Opacity" slider.
You can select the class of the point you are creating by choosing it
from the dropdown menu, or by using the hotkeys 1-0 (mapped in order in
which the classes appear in the dropdown).
Parameters
----------
canvas_size : (int, int), optional
Size of the annotation canvas in pixels.
classes : List[str], optional
The list of classes you want to create annotations for, by default
None.
"""
CanvasClass = PointAnnotationCanvas
@property
def data(self):
"""
The annotation data, as List[ Dict ].
The format is a list of dictionaries, with the following key / value
combinations:
+------------------+-------------------------+
|``'type'`` | ``'point'`` |
+------------------+-------------------------+
|``'label'`` | ``<class label>`` |
+------------------+-------------------------+
|``'coordinates'`` | ``<xy-tuple>`` |
+------------------+-------------------------+
"""
return super().data
@data.setter
def data(self, value): # noqa: D001
self.canvas.data = value
[docs]class BoxAnnotator(Annotator):
"""An annotator for drawing boxes on an image.
To add a box, simply click on one of the corners, and drag the mouse to
the corner opposite. The box will grow as you drag your mouse. To adjust
the box after, you can click the "Edit" button and drag any of the corners
to where you want them.
You can increase or decrease the contrast and brightness of the image
using the sliders to make it easier to annotate. Sometimes you need to see
what's behind already-created annotations, and for this purpose you can
make them more see-through using the "Opacity" slider.
You can select the class of the box you are creating by choosing it
from the dropdown menu, or by using the hotkeys 1-0 (mapped in order in
which the classes appear in the dropdown).
Parameters
----------
canvas_size : (int, int), optional
Size of the annotation canvas in pixels.
classes : List[str], optional
The list of classes you want to create annotations for, by default
None.
"""
CanvasClass = BoundingBoxAnnotationCanvas
@property
def data(self):
"""
The annotation data, as List[ Dict ].
The format is a list of dictionaries, with the following key / value
combinations:
+------------------+-------------------------------+
|``'type'`` | ``'box'`` |
+------------------+-------------------------------+
|``'label'`` | ``<class label>`` |
+------------------+-------------------------------+
|``'xyxy'`` | ``<tuple of x0, y0, x1, y1>`` |
+------------------+-------------------------------+
"""
return super().data
@data.setter
def data(self, value): # noqa: D001
self.canvas.data = value