# default_exp im2im_annotator

Image to image annotator

The image to image annotator (Im2ImAnnotator) allows the users to navigate through multiple images and select multiple options in any one of them. The current notebook develops this annotator.

State

The data shared across the annotator are:

  • The annotations attribute represents all annotations options that could be selected and the user’s answers;

  • The disp_number attribute represents the number of options to be displayed;

  • The question_value attribute represents the label to be shown above the selectable options;

  • The n_rows and n_cols displays the number of options to be shows per rows and columns respectively;

  • The image_path attibute it’s the image been currently annotated;

  • The im_width and im_height displays the image size;

  • The label_width and label_height displays the selectable options size;

View

For the view an internal component called ImCanvas was developed and used to display the current image been annotated.

# export
if is_building_docs():
    class ImCanvas(Image):
        def __init__(
            self,
            width: int = 150,
            height: int = 150,
            has_border: bool = False,
            fit_canvas: bool = False
        ):
            super().__init__(width=width, height=height)
            image = PILImage.new('RGB', (100, 100), (255, 255, 255))
            b = io.BytesIO()
            image.save(b, format='PNG')
            self.value = b.getvalue()

        def _draw_image(self, image_path: str):
            self.value = Image.from_file(image_path).value

        def _clear_image(self):
            pass

        def observe_client_ready(self, cb=None):
            pass
else:
    class ImCanvas(HBox):  # type: ignore
        def __init__(
            self,
            width: int = 150,
            height: int = 150,
            has_border: bool = False,
            fit_canvas: bool = False
        ):
            self.has_border = has_border
            self.fit_canvas = fit_canvas
            self._canvas = Canvas(width=width, height=height)
            super().__init__([self._canvas])

        def _draw_image(self, image_path: str):
            img_render_strategy = ImageRenderer(
                clear=True,
                has_border=self.has_border,
                fit_canvas=self.fit_canvas
            )

            self._image_scale = img_render_strategy.render(
                self._canvas,
                image_path
            )

        def _clear_image(self):
            self._canvas.clear()

        # needed to support voila
        # https://ipycanvas.readthedocs.io/en/latest/advanced.html#ipycanvas-in-voila
        def observe_client_ready(self, cb=None):
            self._canvas.on_client_ready(cb)

The Im2ImAnnotatorGUI uses the ImCanvas developed before and the component CaptureGrid that displays the selectable options on the view.

grid = Grid(
    width=50,
    height=50,
    n_rows=2,
    n_cols=3
)
im2im_state_dict = {
    'im_height': 200,
    'im_width': 200,
    'grid': grid
}

output = OutputImageLabel()
state_to_widget = LabelStoreCaster(output)

app_state = AppWidgetState()
im2im_state = Im2ImState(**im2im_state_dict)  # type: ignore

im2im_ = Im2ImAnnotatorGUI(
    state_to_widget=state_to_widget,
    app_state=app_state,
    im2im_state=im2im_state
)

im2im_._im2im_state.image_path = '../data/projects/im2im1/pics/Grass1.png'
im2im_

Controller

# remove if the results folder exists this allows
# the next command to construct the annotation path
! rm -rf ../data/projects/im2im1/results
# export

class Im2ImAnnotator(Annotator):
    """
    Represents image-to-image annotator.

    Gives an ability to itarate through image dataset,
    map images with labels for classification,
    export final annotations in json format

    """

    def __init__(
        self,
        project_path: Path,
        input_item: InputImage,
        output_item: Union[OutputImageLabel, OutputLabel],
        annotation_file_path,
        n_rows=None,
        n_cols=None,
        label_autosize=False,
        question=None,
        has_border=False
    ):
        assert input_item, "WARNING: Provide valid Input"
        assert output_item, "WARNING: Provide valid Output"

        self.project_path = project_path
        self.input_item = input_item
        self.output_item = output_item
        app_state = AppWidgetState(uuid=str(id(self)))

        super().__init__(app_state)

        grid = Grid(
            width=output_item.width,
            height=output_item.height,
            n_rows=n_rows,
            n_cols=n_cols
        )

        self.im2im_state = Im2ImState(
            uuid=str(id(self)),
            grid=grid,
            annotations=LabelStore(),
            im_height=input_item.height,
            im_width=input_item.width
        )

        self.storage = JsonLabelStorage(
            im_dir=project_path / input_item.dir,
            label_dir=self._get_label_dir(),
            annotation_file_path=annotation_file_path
        )

        self.controller = Im2ImAnnotatorController(
            app_state=self.app_state,
            im2im_state=self.im2im_state,
            storage=self.storage,
            input_item=input_item,
            output_item=output_item,
            question=question,
        )

        self.state_to_widget = LabelStoreCaster(output_item)

        self.view = Im2ImAnnotatorGUI(
            app_state=self.app_state,
            im2im_state=self.im2im_state,
            state_to_widget=self.state_to_widget,
            label_autosize=label_autosize,
            on_navi_clicked=self.controller.idx_changed,
            on_save_btn_clicked=self.controller.save_annotations,
            on_grid_box_clicked=self.controller.handle_grid_click,
            has_border=has_border,
            fit_canvas=input_item.fit_canvas
        )

        self.app_state.subscribe(self._on_annotation_step_change, 'annotation_step')

        # draw current image and bbox only when client is ready
        self.view.on_client_ready(self.controller.handle_client_ready)

    def _on_annotation_step_change(self, annotation_step: AnnotatorStep):
        disabled_buttons = annotation_step == AnnotatorStep.EXPLORE
        if disabled_buttons:
            self.view._grid_box.clear()
        self.state_to_widget.widgets_disabled = disabled_buttons
        self.view._save_btn.disabled = disabled_buttons

        # forces annotator to have img loaded
        self.controller._update_im()
        self.controller._update_state()
        self.view.load_menu(self.im2im_state.annotations)

    def _get_label_dir(self) -> Union[Iterable[str], Path]:
        if isinstance(self.output_item, OutputImageLabel):
            return self.project_path / self.output_item.dir
        elif isinstance(self.output_item, OutputLabel):
            return self.output_item.class_labels
        else:
            raise ValueError(
                "output_item should have type OutputLabel or OutputImageLabel"
            )

    def __repr__(self):
        display(self.view)
        return ""

    def to_dict(self, only_annotated=True):
        return self.controller.to_dict(only_annotated)
! rm -rf ..data/projects/im2im1/results
proj_path = validate_project_path('../data/projects/im2im1')
anno_file_path = construct_annotation_path(
    file_name='../data/projects/im2im1/results/annotation.json')

in_p = InputImage(image_dir='pics', image_width=300, image_height=300)

out_p = OutputImageLabel(label_dir='class_images', label_width=150, label_height=50)

im2im = Im2ImAnnotator(
    project_path=proj_path,
    input_item=in_p,
    output_item=out_p,
    annotation_file_path=anno_file_path,
    n_cols=2,
    question="HelloWorld"
)

im2im

@pytest.fixture
def im2im_class_labels_fixture():
    ! rm -rf ../data/projects/im2im1/results/annotation.json

    proj_path = validate_project_path('../data/projects/im2im1')
    anno_file_path = construct_annotation_path(
        file_name='../data/projects/im2im1/results/annotation.json')

    in_p = InputImage(image_dir='pics', image_width=300, image_height=300)

    out_p = OutputLabel(class_labels=('horse', 'airplane', 'dog'))

    im2im = Im2ImAnnotator(
        project_path=proj_path,
        input_item=in_p,
        output_item=out_p,
        annotation_file_path=anno_file_path,
        n_cols=2,
        question="Testing classes"
    )

    # force fixture to already load its children
    im2im.controller.idx_changed(0)
    assert len(im2im.view._grid_box.children) > 0

    return im2im