# default_exp capture_annotator

Capture annotator

The current notebook develop a annotator (Capture Annotator) that displays multiple items options, allowing users to select multiple of them and save their answers.

State

The data shared across the annotator are:

  • The annotations attribute represents all annotations 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 all_none attribute represents that no option was selected;

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

View

The CaptureAnnotatorGUI joins the internal component (GridMenu) with the navi component and its interaction.

Storage

The CaptureAnnotationStorage saves the user annotations on the disk

Controller

The controller communicates with the state and the storage layer, updating the states and saving the data on disk using the storage module.

# remove if the results folder exists this allows
# the next command to construct the annotation path
! rm -rf ../data/projects/capture1/results
from ipyannotator.storage import construct_annotation_path

anno_file_path = construct_annotation_path(project_path)

storage = CaptureAnnotationStorage(
    input_item_path=project_path / imz.dir,
    annotation_file_path=anno_file_path
)

app_state = AppWidgetState()
capture_state = CaptureState(grid=grid)

caController = CaptureAnnotatorController(
    app_state=app_state,
    capture_state=capture_state,
    storage=storage,
    input_item=imz,
    output_item=grid_bx,
    annotation_file_path=anno_file_path,
    question='hello'
)

caController._capture_state.disp_number = 9  # should be synced from gui

We have 16 images in capture1 project on disk, so first screen should load 9 images; 7 images (16-9) left for second screen.

caController.images
[Path('../data/projects/capture1/pics/pink25x25.png'),
 Path('../data/projects/capture1/pics/pink50x125.png'),
 Path('../data/projects/capture1/pics/pink50x50.png'),
 Path('../data/projects/capture1/pics/pink50x50_0.png'),
 Path('../data/projects/capture1/pics/pink50x50_1.png'),
 Path('../data/projects/capture1/pics/pink50x50_3.png'),
 Path('../data/projects/capture1/pics/teal125x50.png'),
 Path('../data/projects/capture1/pics/teal50x50.png'),
 Path('../data/projects/capture1/pics/teal50x50_0.png'),
 Path('../data/projects/capture1/pics/teal50x50_1.png'),
 Path('../data/projects/capture1/pics/teal50x50_2.png'),
 Path('../data/projects/capture1/pics/teal50x50_3.png'),
 Path('../data/projects/capture1/pics/teal50x50_4.png'),
 Path('../data/projects/capture1/pics/teal50x50_5.png'),
 Path('../data/projects/capture1/pics/teal50x50_6.png'),
 Path('../data/projects/capture1/pics/teal75x75.png')]

This will output the path of all the 16 images

[Path('../data/projects/capture1/pics/pink25x25.png'),
 Path('../data/projects/capture1/pics/pink50x125.png'),
 Path('../data/projects/capture1/pics/pink50x50.png'),
 Path('../data/projects/capture1/pics/pink50x50_0.png'),
 Path('../data/projects/capture1/pics/pink50x50_1.png'),
 Path('../data/projects/capture1/pics/pink50x50_3.png'),
 Path('../data/projects/capture1/pics/teal125x50.png'),
 Path('../data/projects/capture1/pics/teal50x50.png'),
 Path('../data/projects/capture1/pics/teal50x50_0.png'),
 Path('../data/projects/capture1/pics/teal50x50_1.png'),
 Path('../data/projects/capture1/pics/teal50x50_2.png'),
 Path('../data/projects/capture1/pics/teal50x50_3.png'),
 Path('../data/projects/capture1/pics/teal50x50_4.png'),
 Path('../data/projects/capture1/pics/teal50x50_5.png'),
 Path('../data/projects/capture1/pics/teal50x50_6.png'),
 Path('../data/projects/capture1/pics/teal75x75.png')]

List of image names for the 1st screen:

caController._capture_state.annotations
LabelStore({'../data/projects/capture1/pics/pink25x25.png': {}, '../data/projects/capture1/pics/pink50x125.png': {}, '../data/projects/capture1/pics/pink50x50.png': {}, '../data/projects/capture1/pics/pink50x50_0.png': {}, '../data/projects/capture1/pics/pink50x50_1.png': {}, '../data/projects/capture1/pics/pink50x50_3.png': {}, '../data/projects/capture1/pics/teal125x50.png': {}, '../data/projects/capture1/pics/teal50x50.png': {}, '../data/projects/capture1/pics/teal50x50_0.png': {}})
{'../data/projects/capture1/pics/pink25x25.png': {},
 '../data/projects/capture1/pics/pink50x125.png': {},
 '../data/projects/capture1/pics/pink50x50.png': {},
 '../data/projects/capture1/pics/pink50x50_0.png': {},
 '../data/projects/capture1/pics/pink50x50_1.png': {},
 '../data/projects/capture1/pics/pink50x50_3.png': {},
 '../data/projects/capture1/pics/teal125x50.png': {},
 '../data/projects/capture1/pics/teal50x50.png': {},
 '../data/projects/capture1/pics/teal50x50_0.png': {}}

Suppose state change from gui:

caController._capture_state.annotations[
    '../data/projects/capture1/pics/pink25x25.png'] = {'answer': False}

(Next-> button emulation)

Increment index to initiate annotation save and switch state for a new screen

caController._app_state.index = 1
caController._capture_state.annotations
LabelStore({'../data/projects/capture1/pics/pink25x25.png': {'answer': False}, '../data/projects/capture1/pics/pink50x125.png': {}, '../data/projects/capture1/pics/pink50x50.png': {}, '../data/projects/capture1/pics/pink50x50_0.png': {}, '../data/projects/capture1/pics/pink50x50_1.png': {}, '../data/projects/capture1/pics/pink50x50_3.png': {}, '../data/projects/capture1/pics/teal125x50.png': {}, '../data/projects/capture1/pics/teal50x50.png': {}, '../data/projects/capture1/pics/teal50x50_0.png': {}})

(<-Prev button emulation)

Decrement index to initiate annotation save and switch state for previous screen, loading existing annotation

# error: "CaptureAnnotatorController" has no attribute "index"
caController.index = 0  # type: ignore
caController._capture_state.annotations
LabelStore({'../data/projects/capture1/pics/pink25x25.png': {'answer': False}, '../data/projects/capture1/pics/pink50x125.png': {}, '../data/projects/capture1/pics/pink50x50.png': {}, '../data/projects/capture1/pics/pink50x50_0.png': {}, '../data/projects/capture1/pics/pink50x50_1.png': {}, '../data/projects/capture1/pics/pink50x50_3.png': {}, '../data/projects/capture1/pics/teal125x50.png': {}, '../data/projects/capture1/pics/teal50x50.png': {}, '../data/projects/capture1/pics/teal50x50_0.png': {}})
caController.select_none({'new': True})
caController._capture_state
CaptureState(annotations=LabelStore({'../data/projects/capture1/pics/pink25x25.png': {'answer': False}, '../data/projects/capture1/pics/pink50x125.png': {}, '../data/projects/capture1/pics/pink50x50.png': {}, '../data/projects/capture1/pics/pink50x50_0.png': {}, '../data/projects/capture1/pics/pink50x50_1.png': {}, '../data/projects/capture1/pics/pink50x50_3.png': {}, '../data/projects/capture1/pics/teal125x50.png': {}, '../data/projects/capture1/pics/teal50x50.png': {}, '../data/projects/capture1/pics/teal50x50_0.png': {}}), grid=Grid(width=100, height=100, n_rows=5, n_cols=5, disp_number=9, display_label=False), question_value='<center><p style="font-size:20px;">hello</p></center>', all_none=False, _uuid=None, event_map={}, disp_number=9)
#export

class CaptureAnnotator(Annotator):
    debug_output = Output(layout={'border': '1px solid black'})
    """
    Represents capture annotator.

    Gives an ability to itarate through image dataset,
    select images of same class,
    export final annotations in json format

    """
#     @debug_output.capture(clear_output=True)

    def __init__(
        self,
        project_path: Path,
        input_item,
        output_item,
        annotation_file_path,
        n_rows=3,
        n_cols=3,
        disp_number=9,
        question=None,
        filter_files=None
    ):

        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
        self._annotation_file_path = annotation_file_path
        self._n_rows = n_rows
        self._n_cols = n_cols
        self._question = question
        self._filter_files = filter_files

        app_state = AppWidgetState(
            uuid=str(id(self)),
            **{'size': (input_item.width, input_item.height)}
        )

        super().__init__(app_state)

        grid = Grid(
            width=input_item.width,
            height=input_item.height,
            n_rows=n_rows,
            n_cols=n_cols,
            display_label=False,
            disp_number=disp_number
        )

        self.capture_state = CaptureState(
            uuid=str(id(self)),
            annotations=LabelStore(),
            grid=grid
        )

        self.storage = CaptureAnnotationStorage(
            input_item_path=project_path / input_item.dir,
            annotation_file_path=annotation_file_path
        )

        self.controller = CaptureAnnotatorController(
            app_state=self.app_state,
            storage=self.storage,
            capture_state=self.capture_state,
            input_item=input_item,
            output_item=output_item,
            question=question,
            n_rows=n_rows,
            n_cols=n_cols,
            filter_files=filter_files
        )

        self.view = CaptureAnnotatorGUI(
            capture_state=self.capture_state,
            app_state=self.app_state,
            save_btn_clicked=self.controller.save_annotations,
            grid_box_clicked=self.controller.handle_grid_click,
            on_navi_clicked=self.controller.idx_changed,
            select_none_changed=self.controller.select_none
        )

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

    def to_dict(self, only_annotated=True) -> dict:
        return self.storage.to_dict(only_annotated)
ca_annotator

# it should not mark annotations as False
# if user navigates (or clicks on save button)
# without clicking on any cell or select all none checkbox

assert ca_annotator.capture_state.annotations['../data/projects/capture1/pics/pink25x25.png'] == {}
assert ca_annotator.storage.annotations['../data/projects/capture1/pics/pink25x25.png'] is None

ca_annotator.view._save_btn.click()
ca_annotator.view._navi._next_btn.click()

assert ca_annotator.storage.annotations['../data/projects/capture1/pics/pink25x25.png'] == {}

ca_annotator.view._navi._next_btn.click()

# it doesn't fill all annotations as False when loading
assert ca_annotator.capture_state.annotations['../data/projects/capture1/pics/pink25x25.png'] == {}
assert ca_annotator.storage.annotations['../data/projects/capture1/pics/pink25x25.png'] == {}

# it can select a grid item (it update the state, but not save at storage)

ca_annotator.app_state.index = 0
ca_annotator.controller.handle_grid_click(
    event={},
    value='pink25x25.png'
)

assert ca_annotator.capture_state.annotations['../data/projects/capture1/pics/pink25x25.png'] == {
    'answer': True}

assert ca_annotator.storage.annotations[
    '../data/projects/capture1/pics/pink25x25.png'] != {'answer': True}

# it select the remain of the grid item as False if user clicks on save button

ca_annotator.view._save_btn.click()

assert ca_annotator.storage.annotations[
    '../data/projects/capture1/pics/pink25x25.png'] == {'answer': True}
assert ca_annotator.storage.annotations[
    '../data/projects/capture1/pics/pink50x125.png'] == {'answer': False}
assert ca_annotator.capture_state.annotations[
    '../data/projects/capture1/pics/pink50x125.png'] == {'answer': False}

# it save annotations status when user navigates

ca_annotator.controller.handle_grid_click(
    event=None,
    value='pink50x125.png'
)

assert ca_annotator.capture_state.annotations['../data/projects/capture1/pics/pink50x125.png'] == {
    'answer': True}

assert ca_annotator.storage.annotations[
    '../data/projects/capture1/pics/pink50x125.png'] == {'answer': False}

ca_annotator.view._navi._next_btn.click()

assert ca_annotator.storage.annotations[
    '../data/projects/capture1/pics/pink50x125.png'] == {'answer': True}

# it can sync select none checkbox when navigating

ca_annotator.view._none_checkbox.value = True
ca_annotator.view._navi._next_btn.click()
assert ca_annotator.view._none_checkbox.value is False
ca_annotator.view._navi._next_btn.click()
assert ca_annotator.view._none_checkbox.value is True

# annotators doesn't share states with each other

assert ca_annotator.app_state.index == 1

other_annotator = CaptureAnnotator(
    proj_path,
    input_item=in_p,
    output_item=out_p,
    annotation_file_path=anno_file_path,
    n_cols=3,
    question="Select pink squares"
)

assert other_annotator.app_state.index == 0
assert ca_annotator.app_state.index == 1
assert ca_annotator.app_state.index != other_annotator.app_state.index
ca_annotator.storage.annotations
JsonCaptureStorage({'../data/projects/capture1/pics/pink25x25.png': {'answer': True}, '../data/projects/capture1/pics/pink50x125.png': {'answer': True}, '../data/projects/capture1/pics/pink50x50.png': {'answer': False}, '../data/projects/capture1/pics/pink50x50_0.png': {'answer': False}, '../data/projects/capture1/pics/pink50x50_1.png': {'answer': False}, '../data/projects/capture1/pics/pink50x50_3.png': {'answer': False}, '../data/projects/capture1/pics/teal125x50.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_0.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_1.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_2.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_3.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_4.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_5.png': {'answer': False}, '../data/projects/capture1/pics/teal50x50_6.png': {'answer': False}, '../data/projects/capture1/pics/teal75x75.png': {'answer': False}})
ca_annotator.to_dict()
{'../data/projects/capture1/pics/pink25x25.png': {'answer': True},
 '../data/projects/capture1/pics/pink50x125.png': {'answer': True},
 '../data/projects/capture1/pics/pink50x50.png': {'answer': False},
 '../data/projects/capture1/pics/pink50x50_0.png': {'answer': False},
 '../data/projects/capture1/pics/pink50x50_1.png': {'answer': False},
 '../data/projects/capture1/pics/pink50x50_3.png': {'answer': False},
 '../data/projects/capture1/pics/teal125x50.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_0.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_1.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_2.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_3.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_4.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_5.png': {'answer': False},
 '../data/projects/capture1/pics/teal50x50_6.png': {'answer': False},
 '../data/projects/capture1/pics/teal75x75.png': {'answer': False}}