Example #1
0
class ChartMainAnnotator(Screen):
    def __init__(self, size, chart_dir, annotation_dir, admin_mode):
        Screen.__init__(self, "Chart Ground Truth Annotation Interface", size)

        # load the chart directory info ...
        self.chart_image_list = ImageInfo.ListChartDirectory(chart_dir, "")
        print("A total of {0:d} chart images were found".format(
            len(self.chart_image_list)))

        self.general_background = (20, 50, 85)
        self.text_color = (255, 255, 255)
        self.admin_mode = admin_mode

        # about the grid ....
        self.tb_grid_cols = 6
        self.tb_grid_rows = 4
        self.tb_width = 160
        self.tb_height = 120
        self.tb_outer_margin = 16
        self.tb_inner_margin = 4
        self.tb_label_height = 14  # this is approx
        self.tb_progress_h = 16
        self.tb_progress_border = 4

        self.tb_col_width = (self.tb_outer_margin + self.tb_width)
        self.tb_row_height = (self.tb_outer_margin + self.tb_inner_margin * 2 +
                              self.tb_progress_h + self.tb_label_height +
                              self.tb_height)

        self.elements.back_color = self.general_background

        self.chart_dir = chart_dir
        self.annotation_dir = annotation_dir

        # define here all graphic object references ....
        # ...for thumbnails ....
        self.container_thumbnails = None
        self.thumbnails_images = None
        self.thumbnails_status = None
        self.thumbnails_labels = None
        self.paginator = None
        # ... right side image options ...
        self.lbl_page_descriptor = None
        self.container_image_info = None
        self.img_display = None
        self.img_title = None
        self.btn_label_image = None
        self.btn_next_image = None
        self.btn_prev_image = None
        # ... general ...
        self.btn_exit = None

        # ... create them!!! .....
        self.create_controllers()

        # ... load current page data ....
        self.current_page = None
        self.selected_element = 0
        self.load_page(None, 0)

    def create_controllers(self):
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # thumbnail grid parameters
        tb_empty = np.zeros((3, 4, 3), dtype=np.uint8)
        default_status = self.gen_status_image([0] * 6, self.tb_width,
                                               self.tb_progress_h,
                                               self.tb_progress_border)

        # create the thumbnail container ....
        tb_contained_width = self.tb_col_width * self.tb_grid_cols + self.tb_outer_margin
        tb_contained_height = self.tb_row_height * self.tb_grid_rows + self.tb_outer_margin
        self.container_thumbnails = ScreenContainer(
            "container_nav_buttons", (tb_contained_width, tb_contained_height),
            back_color=(15, 30, 50))
        self.container_thumbnails.position = (10, 10)
        self.elements.append(self.container_thumbnails)

        # generate the default grid ....
        self.thumbnails_images = []
        self.thumbnails_status = []
        self.thumbnails_labels = []
        for row in range(self.tb_grid_rows):
            for col in range(self.tb_grid_cols):
                corner_x = self.tb_outer_margin + self.tb_col_width * col
                corner_y = self.tb_outer_margin + self.tb_row_height * row

                tb_image = ScreenImage("tb_image_{0:d}_{1:d}".format(row, col),
                                       tb_empty, self.tb_width, self.tb_height,
                                       True)
                tb_image.position = (corner_x, corner_y)
                tb_image.tag = (row, col)
                tb_image.click_callback = self.thumbnail_image_click
                self.container_thumbnails.append(tb_image)
                self.thumbnails_images.append(tb_image)

                tb_status = ScreenImage(
                    "tb_status_{0:d}_{1:d}".format(row, col), default_status,
                    self.tb_width, self.tb_progress_h)
                tb_status.position = (corner_x, tb_image.get_bottom() +
                                      self.tb_inner_margin)
                self.container_thumbnails.append(tb_status)
                self.thumbnails_status.append(tb_status)

                tb_label = ScreenLabel("tb_label_{0:d}_{1:d}".format(row, col),
                                       "<IMAGE FILE NAME>",
                                       12,
                                       self.tb_width,
                                       centered=1)
                tb_label.position = (corner_x, tb_status.get_bottom() +
                                     self.tb_inner_margin)
                tb_label.background = self.container_thumbnails.back_color
                tb_label.set_color(self.text_color)
                self.container_thumbnails.append(tb_label)
                self.thumbnails_labels.append(tb_label)

        # the paginator ...
        self.paginator = ScreenPaginator("tb_paginator",
                                         16,
                                         self.tb_grid_cols * self.tb_grid_rows,
                                         len(self.chart_image_list),
                                         self.container_thumbnails.width,
                                         text_color=button_text_color,
                                         back_color=button_back_color)
        self.paginator.position = (self.container_thumbnails.get_left(),
                                   self.container_thumbnails.get_bottom() + 20)
        self.paginator.page_selected_callback = self.load_page
        self.elements.append(self.paginator)

        img_container_width = self.width - 45 - self.container_thumbnails.width

        self.lbl_page_descriptor = ScreenLabel(
            "lbl_page_descriptor", "Page XXX of YYY (ZZZZZ elements)", 16,
            img_container_width)
        self.lbl_page_descriptor.position = (
            self.container_thumbnails.get_right() + 15,
            self.container_thumbnails.get_top())
        self.lbl_page_descriptor.background = self.general_background
        self.lbl_page_descriptor.set_color(self.text_color)
        self.elements.append(self.lbl_page_descriptor)

        img_container_height = int(img_container_width) + 100
        self.container_image_info = ScreenContainer(
            "container_image_info",
            (img_container_width, img_container_height),
            back_color=(15, 30, 50))
        self.container_image_info.position = (
            self.lbl_page_descriptor.get_left(),
            self.lbl_page_descriptor.get_bottom() + 10)
        self.elements.append(self.container_image_info)

        base_img = np.zeros((2, 2, 3), np.uint8)
        self.img_display = ScreenImage("img_display", base_img,
                                       self.container_image_info.width - 20,
                                       self.container_image_info.width - 20,
                                       True)
        self.img_display.position = (10, 10)
        self.container_image_info.append(self.img_display)

        self.img_title = ScreenLabel("img_title", "<Image File Name>" * 10, 16,
                                     self.img_display.width)
        self.img_title.position = (10, self.img_display.get_bottom() + 10)
        self.img_title.background = self.container_image_info.back_color
        self.img_title.set_color(self.text_color)
        self.container_image_info.append(self.img_title)

        self.btn_label_image = ScreenButton("btn_label_image",
                                            "Annotate",
                                            18,
                                            100,
                                            text_color=button_text_color,
                                            back_color=button_back_color)
        self.btn_label_image.position = (int(
            (self.container_image_info.width - self.btn_label_image.width) /
            2), self.img_title.get_bottom() + 10)
        self.btn_label_image.click_callback = self.btn_annotate_click
        self.container_image_info.append(self.btn_label_image)

        self.btn_prev_image = ScreenButton("btn_prev_image",
                                           "Previous",
                                           18,
                                           100,
                                           text_color=button_text_color,
                                           back_color=button_back_color)
        self.btn_prev_image.position = (self.btn_label_image.get_left() -
                                        self.btn_prev_image.width - 20,
                                        self.img_title.get_bottom() + 10)
        self.btn_prev_image.click_callback = self.btn_prev_image_click
        self.container_image_info.append(self.btn_prev_image)

        self.btn_next_image = ScreenButton("btn_next_image",
                                           "Next",
                                           18,
                                           100,
                                           text_color=button_text_color,
                                           back_color=button_back_color)
        self.btn_next_image.position = (self.btn_label_image.get_right() + 20,
                                        self.img_title.get_bottom() + 10)
        self.btn_next_image.click_callback = self.btn_next_image_click
        self.container_image_info.append(self.btn_next_image)

        self.img_title.set_text("[Select Image]")

        # ... general ...
        self.btn_exit = ScreenButton("btn_exit",
                                     "Exit",
                                     18,
                                     100,
                                     text_color=button_text_color,
                                     back_color=button_back_color)
        self.btn_exit.position = (self.width - 15 - self.btn_exit.width,
                                  self.height - 15 - self.btn_exit.height)
        self.btn_exit.click_callback = self.btn_exit_click
        self.elements.append(self.btn_exit)

    def gen_status_image(self, all_status, width, height, border):
        result = np.zeros((height, width, 3), np.uint8)

        approx_width = (width -
                        (len(all_status) + 1) * border) / len(all_status)
        last_x = 0
        for idx, value in enumerate(all_status):
            left = last_x + border
            right = left + approx_width
            if value == 2:
                # labeled and verified
                color = (128, 255, 128)
            elif value == 1:
                # labeled but unverified
                color = (192, 192, 64)
            else:
                # not labeled or verified
                color = (192, 64, 64)

            result[border:-border, int(left):int(right), :] = color
            last_x = right

        return result

    def btn_exit_click(self, button):
        # Just exit
        self.return_screen = None
        print("APPLICATION FINISHED")

    def refresh_page(self):
        self.load_page(None, self.current_page, True)

    def load_page(self, paginator, new_page, refresh=False):
        if new_page == self.current_page and not refresh:
            # page not changed ...
            return

        self.current_page = new_page

        page_size = len(self.thumbnails_images)

        current_elements = self.chart_image_list[self.current_page *
                                                 page_size:(self.current_page +
                                                            1) * page_size]

        for idx in range(page_size):
            row = int(idx / self.tb_grid_cols)
            col = idx % self.tb_grid_cols

            if idx < len(current_elements):
                # update image ...
                chart_path = current_elements[idx]
                current_img = cv2.imread(self.chart_dir + chart_path)
                current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2RGB)
                self.thumbnails_images[idx].set_image(current_img,
                                                      self.tb_width,
                                                      self.tb_height,
                                                      keep_aspect=True)

                # center new image
                corner_x = self.tb_outer_margin + self.tb_col_width * col
                corner_y = self.tb_outer_margin + self.tb_row_height * row
                self.center_image_in_box(self.thumbnails_images[idx], corner_x,
                                         corner_y, self.tb_width,
                                         self.tb_height)

                # find annotation path ...
                relative_dir, img_filename = os.path.split(chart_path)
                img_base, ext = os.path.splitext(img_filename)
                # output dir
                output_dir = self.annotation_dir + relative_dir
                annotation_filename = output_dir + "/" + img_base + ".xml"

                # default : No annotations ...
                if os.path.exists(annotation_filename):
                    # read the annotation ...
                    print(annotation_filename)
                    image_info = ImageInfo.FromXML(annotation_filename,
                                                   current_img)

                    status_ints = ImageInfo.GetAllStatuses(image_info)
                else:
                    status_ints = ImageInfo.GetAllStatuses(None)

                status = self.gen_status_image(status_ints, self.tb_width,
                                               self.tb_progress_h,
                                               self.tb_progress_border)
                self.thumbnails_status[idx].set_image(status, self.tb_width,
                                                      self.tb_progress_h)

                self.thumbnails_labels[idx].set_text(chart_path[1:])

                self.thumbnails_images[idx].visible = True
                self.thumbnails_status[idx].visible = True
                self.thumbnails_labels[idx].visible = True
            else:
                # special case for the last page ...
                self.thumbnails_images[idx].visible = False
                self.thumbnails_status[idx].visible = False
                self.thumbnails_labels[idx].visible = False

        msg = "Page {0:d} of {1:d} ({2:d} elements)".format(
            self.current_page + 1, self.paginator.total_pages,
            len(self.chart_image_list))
        self.lbl_page_descriptor.set_text(msg)

        if not refresh:
            self.selected_element = None
            self.update_selected_image(0)

    def update_selected_image(self, new_index):
        if new_index == self.selected_element:
            # no change in selection ...
            return

        self.selected_element = new_index

        page_size = len(self.thumbnails_images)
        for idx in range(page_size):
            if idx == self.selected_element:
                self.thumbnails_images[idx].border_width = 2
                self.thumbnails_images[idx].border_color = (255, 0, 0)

                self.thumbnails_labels[idx].set_color((255, 0, 0))

                # update image ....
                self.img_display.set_image(
                    self.thumbnails_images[idx].original_image,
                    self.container_image_info.width - 20,
                    self.container_image_info.width - 20, True)
                self.center_image_in_box(self.img_display, 10, 10,
                                         self.container_image_info.width - 20,
                                         self.container_image_info.width - 20)
                # update image name ...
                self.img_title.set_text(self.thumbnails_labels[idx].text)
            else:
                self.thumbnails_images[idx].border_width = 0
                self.thumbnails_images[idx].border_color = (0, 0, 0)

                self.thumbnails_labels[idx].set_color(self.text_color)

    def center_image_in_box(self, screen_image, x, y, width, height):
        assert isinstance(screen_image, ScreenImage)

        delta_x = int((width - screen_image.width) / 2)
        delta_y = int((height - screen_image.height) / 2)
        screen_image.position = (x + delta_x, y + delta_y)

    def thumbnail_image_click(self, thumbnail_image):
        # print((thumbnail_image.name, thumbnail_image.tag))
        row, col = thumbnail_image.tag
        idx = row * self.tb_grid_cols + col

        self.update_selected_image(idx)

    def btn_annotate_click(self, button):
        # find image name based on current page
        page_size = len(self.thumbnails_images)
        current_elements = self.chart_image_list[self.current_page *
                                                 page_size:(self.current_page +
                                                            1) * page_size]
        chart_path = current_elements[self.selected_element]

        # create the child sub-menu ....
        image_annotator = ChartImageAnnotator(self.size, self.chart_dir,
                                              self.annotation_dir, chart_path,
                                              self, self.admin_mode)
        image_annotator.prepare_screen()

        self.return_screen = image_annotator

    def btn_next_image_click(self, button):
        page_size = len(self.thumbnails_images)
        current_elements = self.chart_image_list[self.current_page *
                                                 page_size:(self.current_page +
                                                            1) * page_size]

        if self.selected_element + 1 < len(current_elements):
            # move to next within same page ...
            self.update_selected_image(self.selected_element + 1)
        else:
            if self.current_page + 1 < self.paginator.total_pages:
                self.paginator.set_current_page(self.current_page + 1)
                self.load_page(self.paginator, self.current_page + 1, True)
                self.update_selected_image(0)
            else:
                print("Current element is the last on the list of images")

    def btn_prev_image_click(self, button):
        page_size = len(self.thumbnails_images)

        if self.selected_element > 0:
            # move to the previous within the same page ...
            self.update_selected_image(self.selected_element - 1)
        else:
            if self.current_page > 0:
                self.paginator.set_current_page(self.current_page - 1)
                self.load_page(self.paginator, self.current_page - 1, True)
                self.update_selected_image(page_size - 1)
            else:
                print("Current element is the first on the list of images")
class LineChartAnnotator(Screen):
    ModeNavigate = 0
    ModeNumberEdit = 1
    ModeLineSelect = 2
    ModeLineEdit = 3
    ModePointAdd = 4
    ModePointEdit = 5
    ModeConfirmNumberOverwrite = 6
    ModeConfirmExit = 7

    ViewModeRawData = 0
    ViewModeGrayData = 1
    ViewModeRawNoData = 2
    ViewModeGrayNoData = 3

    DoubleClickMaxPointDistance = 5

    def __init__(self, size, panel_image, panel_info, parent_screen):
        Screen.__init__(self, "Line Chart Ground Truth Annotation Interface",
                        size)

        self.panel_image = panel_image
        self.panel_gray = np.zeros(self.panel_image.shape,
                                   self.panel_image.dtype)
        self.panel_gray[:, :, 0] = cv2.cvtColor(self.panel_image,
                                                cv2.COLOR_RGB2GRAY)
        self.panel_gray[:, :, 1] = self.panel_gray[:, :, 0].copy()
        self.panel_gray[:, :, 2] = self.panel_gray[:, :, 0].copy()

        self.panel_info = panel_info

        if self.panel_info.data is None:
            # create default Line chart data ...
            self.data = LineData.CreateDefault(self.panel_info)
            self.data_changed = True
        else:
            # make a copy ...
            self.data = LineData.Copy(self.panel_info.data)
            self.data_changed = False

        self.parent_screen = parent_screen

        self.general_background = (150, 190, 20)
        self.text_color = (255, 255, 255)

        self.elements.back_color = self.general_background
        self.edition_mode = None

        self.tempo_line_index = None
        self.tempo_point_index = None
        self.tempo_line_values = None

        self.view_mode = LineChartAnnotator.ViewModeRawData
        self.view_scale = 1.0

        self.label_title = None

        self.container_view_buttons = None
        self.lbl_zoom = None
        self.btn_zoom_reduce = None
        self.btn_zoom_increase = None
        self.btn_zoom_zero = None

        self.btn_view_raw_data = None
        self.btn_view_gray_data = None
        self.btn_view_raw_clear = None
        self.btn_view_gray_clear = None

        self.container_confirm_buttons = None
        self.lbl_confirm_message = None
        self.btn_confirm_cancel = None
        self.btn_confirm_accept = None

        self.container_annotation_buttons = None
        self.lbl_edit_title = None
        self.btn_edit_number = None
        self.btn_edit_data = None
        self.btn_return_accept = None
        self.btn_return_cancel = None

        self.container_number_buttons = None
        self.lbl_number_title = None
        self.lbl_number_series_title = None
        self.lbx_number_series_values = None
        self.btn_number_series_add = None
        self.btn_number_series_remove = None
        self.btn_number_return = None

        self.container_data_buttons = None
        self.lbl_data_title = None
        self.lbx_data_series_values = None
        self.btn_data_series_edit = None
        self.btn_data_return = None

        self.container_line_buttons = None
        self.lbl_line_title = None
        self.lbl_line_name = None
        self.lbx_line_points = None
        self.btn_line_point_edit = None
        self.btn_line_point_delete = None
        self.btn_line_point_add = None
        self.btn_line_return_accept = None
        self.btn_line_return_cancel = None

        self.container_images = None
        self.canvas_display = None
        self.img_main = None

        self.create_controllers()

        # get the view ...
        self.update_current_view(True)

    def create_controllers(self):
        # add elements....
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # Main Title
        self.label_title = ScreenLabel(
            "title",
            "Chart Image Annotation Tool - Line Chart Data Annotation", 28)
        self.label_title.background = self.general_background
        self.label_title.position = (int(
            (self.width - self.label_title.width) / 2), 20)
        self.label_title.set_color(self.text_color)
        self.elements.append(self.label_title)

        container_top = 10 + self.label_title.get_bottom()
        container_width = 300

        button_width = 190
        button_left = (container_width - button_width) / 2

        button_2_width = 130
        button_2_left = int(container_width * 0.25) - button_2_width / 2
        button_2_right = int(container_width * 0.75) - button_2_width / 2

        # ===========================
        # View Options Panel

        # View panel with view control buttons
        self.container_view_buttons = ScreenContainer(
            "container_view_buttons", (container_width, 160),
            back_color=self.general_background)
        self.container_view_buttons.position = (
            self.width - self.container_view_buttons.width - 10, container_top)
        self.elements.append(self.container_view_buttons)

        # zoom ....
        self.lbl_zoom = ScreenLabel("lbl_zoom", "Zoom: 100%", 21, 290, 1)
        self.lbl_zoom.position = (5, 5)
        self.lbl_zoom.set_background(self.general_background)
        self.lbl_zoom.set_color(self.text_color)
        self.container_view_buttons.append(self.lbl_zoom)

        self.btn_zoom_reduce = ScreenButton("btn_zoom_reduce", "[ - ]", 21, 90)
        self.btn_zoom_reduce.set_colors(button_text_color, button_back_color)
        self.btn_zoom_reduce.position = (10, self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_reduce.click_callback = self.btn_zoom_reduce_click
        self.container_view_buttons.append(self.btn_zoom_reduce)

        self.btn_zoom_increase = ScreenButton("btn_zoom_increase", "[ + ]", 21,
                                              90)
        self.btn_zoom_increase.set_colors(button_text_color, button_back_color)
        self.btn_zoom_increase.position = (self.container_view_buttons.width -
                                           self.btn_zoom_increase.width - 10,
                                           self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_increase.click_callback = self.btn_zoom_increase_click
        self.container_view_buttons.append(self.btn_zoom_increase)

        self.btn_zoom_zero = ScreenButton("btn_zoom_zero", "100%", 21, 90)
        self.btn_zoom_zero.set_colors(button_text_color, button_back_color)
        self.btn_zoom_zero.position = (
            (self.container_view_buttons.width - self.btn_zoom_zero.width) / 2,
            self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_zero.click_callback = self.btn_zoom_zero_click
        self.container_view_buttons.append(self.btn_zoom_zero)

        self.btn_view_raw_data = ScreenButton("btn_view_raw_data", "RGB Data",
                                              21, button_2_width)
        self.btn_view_raw_data.set_colors(button_text_color, button_back_color)
        self.btn_view_raw_data.position = (button_2_left,
                                           self.btn_zoom_zero.get_bottom() +
                                           10)
        self.btn_view_raw_data.click_callback = self.btn_view_raw_data_click
        self.container_view_buttons.append(self.btn_view_raw_data)

        self.btn_view_gray_data = ScreenButton("btn_view_gray", "Gray Data",
                                               21, button_2_width)
        self.btn_view_gray_data.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_gray_data.position = (button_2_right,
                                            self.btn_zoom_zero.get_bottom() +
                                            10)
        self.btn_view_gray_data.click_callback = self.btn_view_gray_data_click
        self.container_view_buttons.append(self.btn_view_gray_data)

        self.btn_view_raw_clear = ScreenButton("btn_view_raw_clear",
                                               "RGB Clear", 21, button_2_width)
        self.btn_view_raw_clear.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_raw_clear.position = (
            button_2_left, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_raw_clear.click_callback = self.btn_view_raw_clear_click
        self.container_view_buttons.append(self.btn_view_raw_clear)

        self.btn_view_gray_clear = ScreenButton("btn_view_gray_clear",
                                                "Gray Clear", 21,
                                                button_2_width)
        self.btn_view_gray_clear.set_colors(button_text_color,
                                            button_back_color)
        self.btn_view_gray_clear.position = (
            button_2_right, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_gray_clear.click_callback = self.btn_view_gray_clear_click
        self.container_view_buttons.append(self.btn_view_gray_clear)

        # ===========================
        # confirmation panel
        self.container_confirm_buttons = ScreenContainer(
            "container_confirm_buttons", (container_width, 70),
            back_color=self.general_background)
        self.container_confirm_buttons.position = (
            self.width - self.container_confirm_buttons.width - 10,
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_confirm_buttons)
        self.container_confirm_buttons.visible = False

        self.lbl_confirm_message = ScreenLabel(
            "lbl_confirm_message", "Confirmation message goes here?", 21, 290,
            1)
        self.lbl_confirm_message.position = (5, 5)
        self.lbl_confirm_message.set_background(self.general_background)
        self.lbl_confirm_message.set_color(self.text_color)
        self.container_confirm_buttons.append(self.lbl_confirm_message)

        self.btn_confirm_cancel = ScreenButton("btn_confirm_cancel", "Cancel",
                                               21, 130)
        self.btn_confirm_cancel.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_cancel.position = (
            10, self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_cancel.click_callback = self.btn_confirm_cancel_click
        self.container_confirm_buttons.append(self.btn_confirm_cancel)

        self.btn_confirm_accept = ScreenButton("btn_confirm_accept", "Accept",
                                               21, 130)
        self.btn_confirm_accept.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_accept.position = (
            self.container_confirm_buttons.width -
            self.btn_confirm_accept.width - 10,
            self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_accept.click_callback = self.btn_confirm_accept_click
        self.container_confirm_buttons.append(self.btn_confirm_accept)

        # ==============================
        # main annotation options
        darker_background = (100, 130, 15)

        self.container_annotation_buttons = ScreenContainer(
            "container_annotation_buttons", (container_width, 180),
            back_color=darker_background)
        self.container_annotation_buttons.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_annotation_buttons)

        self.lbl_edit_title = ScreenLabel("lbl_edit_title",
                                          "Line Chart Annotation Options", 21,
                                          290, 1)
        self.lbl_edit_title.position = (5, 5)
        self.lbl_edit_title.set_background(darker_background)
        self.lbl_edit_title.set_color(self.text_color)
        self.container_annotation_buttons.append(self.lbl_edit_title)

        self.btn_edit_number = ScreenButton("btn_edit_number",
                                            "Edit Number of Lines", 21,
                                            button_width)
        self.btn_edit_number.set_colors(button_text_color, button_back_color)
        self.btn_edit_number.position = (button_left,
                                         self.lbl_edit_title.get_bottom() + 10)
        self.btn_edit_number.click_callback = self.btn_edit_number_click
        self.container_annotation_buttons.append(self.btn_edit_number)

        self.btn_edit_data = ScreenButton("btn_edit_data", "Edit Line Data",
                                          21, button_width)
        self.btn_edit_data.set_colors(button_text_color, button_back_color)
        self.btn_edit_data.position = (button_left,
                                       self.btn_edit_number.get_bottom() + 10)
        self.btn_edit_data.click_callback = self.btn_edit_data_click
        self.container_annotation_buttons.append(self.btn_edit_data)

        self.btn_return_accept = ScreenButton("btn_return_accept", "Accept",
                                              21, button_2_width)
        return_top = self.container_annotation_buttons.height - self.btn_return_accept.height - 10
        self.btn_return_accept.set_colors(button_text_color, button_back_color)
        self.btn_return_accept.position = (button_2_left, return_top)
        self.btn_return_accept.click_callback = self.btn_return_accept_click
        self.container_annotation_buttons.append(self.btn_return_accept)

        self.btn_return_cancel = ScreenButton("btn_return_cancel", "Cancel",
                                              21, button_2_width)
        self.btn_return_cancel.set_colors(button_text_color, button_back_color)
        self.btn_return_cancel.position = (button_2_right, return_top)
        self.btn_return_cancel.click_callback = self.btn_return_cancel_click
        self.container_annotation_buttons.append(self.btn_return_cancel)

        # ==================================
        # - options to define the total number of lines in the chart ....
        self.container_number_buttons = ScreenContainer(
            "container_number_buttons", (container_width, 530),
            back_color=darker_background)
        self.container_number_buttons.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 15)
        self.elements.append(self.container_number_buttons)

        self.lbl_number_title = ScreenLabel("lbl_number_title ",
                                            "Lines in Chart: [0]", 25, 290, 1)
        self.lbl_number_title.position = (5, 5)
        self.lbl_number_title.set_background(darker_background)
        self.lbl_number_title.set_color(self.text_color)
        self.container_number_buttons.append(self.lbl_number_title)

        self.lbl_number_series_title = ScreenLabel("lbl_number_series_title",
                                                   "Data Series", 21, 290, 1)
        self.lbl_number_series_title.position = (
            5, self.lbl_number_title.get_bottom() + 10)
        self.lbl_number_series_title.set_background(darker_background)
        self.lbl_number_series_title.set_color(self.text_color)
        self.container_number_buttons.append(self.lbl_number_series_title)

        self.lbx_number_series_values = ScreenTextlist(
            "lbx_number_series_values", (container_width - 20, 290),
            18,
            back_color=(255, 255, 255),
            option_color=(0, 0, 0),
            selected_back=(120, 80, 50),
            selected_color=(255, 255, 255))
        self.lbx_number_series_values.position = (
            10, self.lbl_number_series_title.get_bottom() + 10)
        # self.lbx_number_categories_values.selected_value_change_callback = self.lbx_number_categories_values_changed
        self.container_number_buttons.append(self.lbx_number_series_values)

        self.btn_number_series_add = ScreenButton("btn_number_series_add",
                                                  "Add", 21, button_2_width)
        self.btn_number_series_add.set_colors(button_text_color,
                                              button_back_color)
        self.btn_number_series_add.position = (
            button_2_left, self.lbx_number_series_values.get_bottom() + 10)
        self.btn_number_series_add.click_callback = self.btn_number_series_add_click
        self.container_number_buttons.append(self.btn_number_series_add)

        self.btn_number_series_remove = ScreenButton(
            "btn_number_series_remove", "Remove", 21, button_2_width)
        self.btn_number_series_remove.set_colors(button_text_color,
                                                 button_back_color)
        self.btn_number_series_remove.position = (
            button_2_right, self.lbx_number_series_values.get_bottom() + 10)
        self.btn_number_series_remove.click_callback = self.btn_number_series_remove_click
        self.container_number_buttons.append(self.btn_number_series_remove)

        self.btn_number_return = ScreenButton("btn_number_return", "Return",
                                              21, button_width)
        self.btn_number_return.set_colors(button_text_color, button_back_color)
        self.btn_number_return.position = (
            button_left, self.btn_number_series_add.get_bottom() + 15)
        self.btn_number_return.click_callback = self.btn_number_return_click
        self.container_number_buttons.append(self.btn_number_return)
        self.container_number_buttons.visible = False

        # ==============================
        # data annotation options
        self.container_data_buttons = ScreenContainer(
            "container_data_buttons", (container_width, 380),
            back_color=darker_background)
        self.container_data_buttons.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_data_buttons)

        self.lbl_data_title = ScreenLabel("lbl_data_title ", "Lines in Chart",
                                          25, 290, 1)
        self.lbl_data_title.position = (5, 5)
        self.lbl_data_title.set_background(darker_background)
        self.lbl_data_title.set_color(self.text_color)
        self.container_data_buttons.append(self.lbl_data_title)

        self.lbx_data_series_values = ScreenTextlist(
            "lbx_data_series_values", (container_width - 20, 210),
            18,
            back_color=(255, 255, 255),
            option_color=(0, 0, 0),
            selected_back=(120, 80, 50),
            selected_color=(255, 255, 255))
        self.lbx_data_series_values.position = (
            10, self.lbl_data_title.get_bottom() + 20)
        self.container_data_buttons.append(self.lbx_data_series_values)

        self.btn_data_series_edit = ScreenButton("btn_data_series_edit",
                                                 "Edit Points", 21,
                                                 button_width)
        self.btn_data_series_edit.set_colors(button_text_color,
                                             button_back_color)
        self.btn_data_series_edit.position = (
            button_left, self.lbx_data_series_values.get_bottom() + 10)
        self.btn_data_series_edit.click_callback = self.btn_data_series_edit_click
        self.container_data_buttons.append(self.btn_data_series_edit)

        self.btn_data_return = ScreenButton("btn_data_return", "Return", 21,
                                            button_width)
        self.btn_data_return.set_colors(button_text_color, button_back_color)
        self.btn_data_return.position = (
            button_left, self.btn_data_series_edit.get_bottom() + 20)
        self.btn_data_return.click_callback = self.btn_data_return_click
        self.container_data_buttons.append(self.btn_data_return)

        self.container_data_buttons.visible = False

        # ==============================
        # line annotation options
        self.container_line_buttons = ScreenContainer(
            "container_line_buttons", (container_width, 450),
            back_color=darker_background)
        self.container_line_buttons.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_line_buttons)

        self.lbl_line_title = ScreenLabel("lbl_line_title", "Points in Line",
                                          25, 290, 1)
        self.lbl_line_title.position = (5, 5)
        self.lbl_line_title.set_background(darker_background)
        self.lbl_line_title.set_color(self.text_color)
        self.container_line_buttons.append(self.lbl_line_title)

        self.lbl_line_name = ScreenLabel("lbl_line_name", "[Data series]", 18,
                                         290, 1)
        self.lbl_line_name.position = (5,
                                       self.lbl_line_title.get_bottom() + 20)
        self.lbl_line_name.set_background(darker_background)
        self.lbl_line_name.set_color(self.text_color)
        self.container_line_buttons.append(self.lbl_line_name)

        self.lbx_line_points = ScreenTextlist("lbx_line_points",
                                              (container_width - 20, 210),
                                              18,
                                              back_color=(255, 255, 255),
                                              option_color=(0, 0, 0),
                                              selected_back=(120, 80, 50),
                                              selected_color=(255, 255, 255))
        self.lbx_line_points.position = (10,
                                         self.lbl_line_name.get_bottom() + 30)
        self.lbx_line_points.selected_value_change_callback = self.lbx_line_points_value_changed
        self.container_line_buttons.append(self.lbx_line_points)

        self.btn_line_point_edit = ScreenButton("btn_line_point_edit",
                                                "Edit Point", 21,
                                                button_2_width)
        self.btn_line_point_edit.set_colors(button_text_color,
                                            button_back_color)
        self.btn_line_point_edit.position = (
            button_2_left, self.lbx_line_points.get_bottom() + 10)
        self.btn_line_point_edit.click_callback = self.btn_line_point_edit_click
        self.container_line_buttons.append(self.btn_line_point_edit)

        self.btn_line_point_delete = ScreenButton("btn_line_point_delete",
                                                  "Remove Point", 21,
                                                  button_2_width)
        self.btn_line_point_delete.set_colors(button_text_color,
                                              button_back_color)
        self.btn_line_point_delete.position = (
            button_2_right, self.lbx_line_points.get_bottom() + 10)
        self.btn_line_point_delete.click_callback = self.btn_line_point_delete_click
        self.container_line_buttons.append(self.btn_line_point_delete)

        self.btn_line_point_add = ScreenButton("btn_line_point_add",
                                               "Add Points", 21, button_width)
        self.btn_line_point_add.set_colors(button_text_color,
                                           button_back_color)
        self.btn_line_point_add.position = (
            button_left, self.btn_line_point_edit.get_bottom() + 10)
        self.btn_line_point_add.click_callback = self.btn_line_point_add_click
        self.container_line_buttons.append(self.btn_line_point_add)

        self.btn_line_return_accept = ScreenButton("btn_line_return_accept",
                                                   "Accept", 21,
                                                   button_2_width)
        self.btn_line_return_accept.set_colors(button_text_color,
                                               button_back_color)
        self.btn_line_return_accept.position = (
            button_2_left, self.btn_line_point_add.get_bottom() + 20)
        self.btn_line_return_accept.click_callback = self.btn_line_return_accept_click
        self.container_line_buttons.append(self.btn_line_return_accept)

        self.btn_line_return_cancel = ScreenButton("btn_line_return_cancel",
                                                   "Cancel", 21,
                                                   button_2_width)
        self.btn_line_return_cancel.set_colors(button_text_color,
                                               button_back_color)
        self.btn_line_return_cancel.position = (
            button_2_right, self.btn_line_point_add.get_bottom() + 20)
        self.btn_line_return_cancel.click_callback = self.btn_line_return_cancel_click
        self.container_line_buttons.append(self.btn_line_return_cancel)

        self.container_line_buttons.visible = False

        # ======================================
        # visuals
        # ===========================
        # Image

        image_width = self.width - self.container_view_buttons.width - 30
        image_height = self.height - container_top - 10
        self.container_images = ScreenContainer("container_images",
                                                (image_width, image_height),
                                                back_color=(0, 0, 0))
        self.container_images.position = (10, container_top)
        self.elements.append(self.container_images)

        # ... image objects ...
        tempo_blank = np.zeros((50, 50, 3), np.uint8)
        tempo_blank[:, :, :] = 255
        self.img_main = ScreenImage("img_main", tempo_blank, 0, 0, True,
                                    cv2.INTER_NEAREST)
        self.img_main.position = (0, 0)
        self.img_main.mouse_button_down_callback = self.img_mouse_down
        self.img_main.double_click_callback = self.img_mouse_double_click
        self.container_images.append(self.img_main)

        self.canvas_display = ScreenCanvas("canvas_display", 100, 100)
        self.canvas_display.position = (0, 0)
        self.canvas_display.locked = True
        self.container_images.append(self.canvas_display)

        self.prepare_number_controls()

        self.set_editor_mode(LineChartAnnotator.ModeNavigate)

    def btn_zoom_reduce_click(self, button):
        self.update_view_scale(self.view_scale - 0.25)

    def btn_zoom_increase_click(self, button):
        self.update_view_scale(self.view_scale + 0.25)

    def btn_zoom_zero_click(self, button):
        self.update_view_scale(1.0)

    def btn_view_raw_data_click(self, button):
        self.view_mode = LineChartAnnotator.ViewModeRawData
        self.update_current_view()

    def btn_view_gray_data_click(self, button):
        self.view_mode = LineChartAnnotator.ViewModeGrayData
        self.update_current_view()

    def btn_view_raw_clear_click(self, button):
        self.view_mode = LineChartAnnotator.ViewModeRawNoData
        self.update_current_view()

    def btn_view_gray_clear_click(self, button):
        self.view_mode = LineChartAnnotator.ViewModeGrayNoData
        self.update_current_view()

    def update_view_scale(self, new_scale):
        prev_scale = self.view_scale

        if 0.25 <= new_scale <= 4.0:
            self.view_scale = new_scale
        else:
            return

        # keep previous offsets ...
        scroll_offset_y = self.container_images.v_scroll.value if self.container_images.v_scroll.active else 0
        scroll_offset_x = self.container_images.h_scroll.value if self.container_images.h_scroll.active else 0

        prev_center_y = scroll_offset_y + self.container_images.height / 2
        prev_center_x = scroll_offset_x + self.container_images.width / 2

        # compute new scroll box offsets
        scale_factor = (new_scale / prev_scale)
        new_off_y = prev_center_y * scale_factor - self.container_images.height / 2
        new_off_x = prev_center_x * scale_factor - self.container_images.width / 2

        # update view ....
        self.update_current_view(True)

        # set offsets
        if self.container_images.v_scroll.active and 0 <= new_off_y <= self.container_images.v_scroll.max:
            self.container_images.v_scroll.value = new_off_y
        if self.container_images.h_scroll.active and 0 <= new_off_x <= self.container_images.h_scroll.max:
            self.container_images.h_scroll.value = new_off_x

        # re-scale objects from both canvas
        # ... display ...
        for polygon_name in self.canvas_display.elements:
            display_polygon = self.canvas_display.elements[polygon_name]
            display_polygon.points *= scale_factor

        # update scale text ...
        self.lbl_zoom.set_text("Zoom: " +
                               str(int(round(self.view_scale * 100, 0))) + "%")

    def update_current_view(self, resized=False):
        if self.view_mode in [
                LineChartAnnotator.ViewModeGrayData,
                LineChartAnnotator.ViewModeGrayNoData
        ]:
            # gray scale mode
            base_image = self.panel_gray
        else:
            base_image = self.panel_image

        h, w, c = base_image.shape

        modified_image = base_image.copy()

        if self.view_mode in [
                LineChartAnnotator.ViewModeRawData,
                LineChartAnnotator.ViewModeGrayData
        ]:
            # (for example, draw the polygons)
            self.canvas_display.visible = True

            x1, y1, x2, y2 = self.panel_info.axes.bounding_box
            x1 = int(x1)
            y1 = int(y1)
            x2 = int(x2)
            y2 = int(y2)

            # axes lines
            cv2.line(modified_image, (x1, y1), (x1, y2), (0, 255, 0),
                     thickness=1)  # y = green
            cv2.line(modified_image, (x1, y2), (x2, y2), (0, 0, 255),
                     thickness=1)  # x = blue
            # close the data area rectangle ?
            # cv2.line(modified_image, (x2, y1), (x2, y2), (0, 128, 0), thickness=1)
            # cv2.line(modified_image, (x1, y1), (x2, y1), (0, 0, 128), thickness=1)

            # check which lines will be drawn ...
            if self.edition_mode in [
                    LineChartAnnotator.ModeLineEdit,
                    LineChartAnnotator.ModePointAdd,
                    LineChartAnnotator.ModePointEdit
            ]:
                # Only draw the line being edited ... based on its temporary changes ...
                lines_to_drawing = [self.tempo_line_values]
            else:
                # draw everything ...
                lines_to_drawing = self.data.lines

            # for each line to drawn ...
            for idx, line_values in enumerate(lines_to_drawing):
                line_color = self.canvas_display.colors[idx % len(
                    self.canvas_display.colors)]

                all_transformed_points = []
                for p_idx in range(len(line_values.points)):
                    # transform current point from relative space to absolute pixel space
                    c_x, c_y = line_values.points[p_idx]
                    c_x += x1
                    c_y = y2 - c_y
                    current_point = (int(round(c_x)), int(round(c_y)))

                    all_transformed_points.append(current_point)

                    # Draw the points as small circles ...
                    if (self.edition_mode == LineChartAnnotator.ModeLineEdit
                            and self.lbx_line_points.selected_option_value
                            is not None
                            and int(self.lbx_line_points.selected_option_value)
                            == p_idx):
                        # empty large circle for selected option
                        cv2.circle(modified_image,
                                   current_point,
                                   5,
                                   line_color,
                                   thickness=2)
                    else:
                        # filled small circle
                        cv2.circle(modified_image,
                                   current_point,
                                   3,
                                   line_color,
                                   thickness=-1)

                # Draw the line ...
                all_transformed_points = np.array(
                    all_transformed_points).astype(np.int32)
                modified_image = cv2.polylines(modified_image,
                                               [all_transformed_points], False,
                                               line_color)

        else:
            self.canvas_display.visible = False

        # finally, resize ...
        modified_image = cv2.resize(
            modified_image,
            (int(w * self.view_scale), int(h * self.view_scale)),
            interpolation=cv2.INTER_NEAREST)

        # update canvas size ....
        self.canvas_display.height, self.canvas_display.width, _ = modified_image.shape

        # replace/update image
        self.img_main.set_image(modified_image, 0, 0, True, cv2.INTER_NEAREST)
        if resized:
            self.container_images.recalculate_size()

    def btn_confirm_cancel_click(self, button):
        if self.edition_mode in [LineChartAnnotator.ModeConfirmExit]:
            # return to navigation
            self.set_editor_mode(LineChartAnnotator.ModeNavigate)

        elif self.edition_mode in [
                LineChartAnnotator.ModePointAdd,
                LineChartAnnotator.ModePointEdit
        ]:
            # return to line edition mode ...
            self.update_points_list()
            self.set_editor_mode(LineChartAnnotator.ModeLineEdit)
        else:
            print(self.edition_mode)
            raise Exception("Not Implemented")

    def btn_confirm_accept_click(self, button):
        if self.edition_mode == LineChartAnnotator.ModeConfirmExit:
            print("-> Changes made to Line Data Annotations were lost")
            self.return_screen = self.parent_screen
        else:
            raise Exception("Not Implemented")

    def btn_edit_number_click(self, button):
        self.set_editor_mode(LineChartAnnotator.ModeNumberEdit)
        self.update_current_view()

    def btn_edit_data_click(self, button):
        self.fill_data_series_list(self.lbx_data_series_values)
        self.set_editor_mode(LineChartAnnotator.ModeLineSelect)
        self.update_current_view()

    def btn_return_accept_click(self, button):
        if self.data_changed:
            # overwrite existing Line data ...
            self.panel_info.data = LineData.Copy(self.data)
            self.parent_screen.subtool_completed(True)

        # return
        self.return_screen = self.parent_screen

    def btn_return_cancel_click(self, button):
        if self.data_changed:
            self.set_editor_mode(LineChartAnnotator.ModeConfirmExit)
        else:
            # simply return
            self.return_screen = self.parent_screen

    def numbers_update_GUI(self, data_series_changed):
        n_lines = self.data.total_lines()
        self.lbl_number_title.set_text("Lines in Chart: {0:d}".format(n_lines))

        if data_series_changed:
            self.fill_data_series_list(self.lbx_number_series_values)

        self.update_current_view()

    def btn_number_series_add_click(self, button):
        # add ... using general default values (based on axis info)
        p1, p2 = self.get_default_line_values()
        self.data.add_data_series(default_points=[p1, p2])

        self.data_changed = True

        # update GUI
        self.numbers_update_GUI(True)

    def btn_number_series_remove_click(self, button):
        if self.lbx_number_series_values.selected_option_value is None:
            print("Must select a data series")
            return

        option_idx = int(self.lbx_number_series_values.selected_option_value)

        # remove ...
        self.data.remove_data_series(option_idx)

        self.data_changed = True

        # update GUI
        self.numbers_update_GUI(True)

    def get_default_line_values(self):
        a_x1, a_y1, a_x2, a_y2 = self.panel_info.axes.bounding_box
        a_x1 = int(a_x1)
        a_y1 = int(a_y1)
        a_x2 = int(a_x2)
        a_y2 = int(a_y2)

        axis_range = a_y2 - a_y1
        axis_domain = a_x2 - a_x1

        line_y = 0.5 * axis_range

        return (0, line_y), (axis_domain, line_y)

    def btn_number_return_click(self, button):
        self.set_editor_mode(LineChartAnnotator.ModeNavigate)
        self.update_current_view()

    def from_pos_to_rel_click(self, pos):
        # click pos ...
        click_x, click_y = pos
        click_x /= self.view_scale
        click_y /= self.view_scale

        x1, y1, x2, y2 = self.panel_info.axes.bounding_box
        x1 = int(x1)
        y2 = int(y2)

        rel_x = click_x - x1
        rel_y = y2 - click_y

        return rel_x, rel_y

    def img_mouse_down(self, img_object, pos, button):
        if button == 1:
            if self.edition_mode in [
                    LineChartAnnotator.ModePointAdd,
                    LineChartAnnotator.ModePointEdit
            ]:
                # click pos ...
                rel_x, rel_y = self.from_pos_to_rel_click(pos)

                if self.edition_mode == LineChartAnnotator.ModePointEdit:
                    # set new position for point being edited ...
                    self.tempo_line_values.set_point(self.tempo_point_index,
                                                     rel_x, rel_y)
                    # go back to previous mode
                    self.update_points_list()
                    self.set_editor_mode(LineChartAnnotator.ModeLineEdit)
                elif self.edition_mode == LineChartAnnotator.ModePointAdd:
                    # Add the new point
                    self.tempo_line_values.add_point(rel_x, rel_y,
                                                     LineValues.InsertByXValue)
                    # .. and stay on current state until cancel is pressed.

                self.update_current_view()

    def prepare_number_controls(self):
        n_lines = self.data.total_lines()
        self.lbl_number_title.set_text("Lines in Chart: {0:d}".format(n_lines))

        self.fill_data_series_list(self.lbx_number_series_values)

    def fill_data_series_list(self, text_list):
        text_list.clear_options()
        for idx, current_text in enumerate(self.data.data_series):
            if current_text is None:
                display_value = str(idx + 1)
            else:
                display_value = "{0:d}: {1:s}".format(idx + 1,
                                                      current_text.value)

            text_list.add_option(str(idx), display_value)

    def set_editor_mode(self, new_mode):
        self.edition_mode = new_mode

        # Navigation mode ...
        self.container_annotation_buttons.visible = (
            self.edition_mode == LineChartAnnotator.ModeNavigate)

        # edit modes ...
        self.container_number_buttons.visible = (
            self.edition_mode == LineChartAnnotator.ModeNumberEdit)
        self.container_data_buttons.visible = (
            self.edition_mode == LineChartAnnotator.ModeLineSelect)
        self.container_line_buttons.visible = (
            self.edition_mode == LineChartAnnotator.ModeLineEdit)

        # Confirm panel and buttons  ...
        self.container_confirm_buttons.visible = self.edition_mode in [
            LineChartAnnotator.ModeConfirmNumberOverwrite,
            LineChartAnnotator.ModePointAdd, LineChartAnnotator.ModePointEdit,
            LineChartAnnotator.ModeConfirmExit
        ]

        if self.edition_mode == LineChartAnnotator.ModeConfirmNumberOverwrite:
            self.lbl_confirm_message.set_text("Discard Existing Line Data?")
        elif self.edition_mode == LineChartAnnotator.ModePointAdd:
            self.lbl_confirm_message.set_text("Click on New Point")
        elif self.edition_mode == LineChartAnnotator.ModePointEdit:
            self.lbl_confirm_message.set_text("Click on New Position")
        elif self.edition_mode == LineChartAnnotator.ModeConfirmExit:
            self.lbl_confirm_message.set_text("Discard Changes to Line Data?")

        # Do not show accept at these steps (they can be implicitly accepted, but need explicit cancel button only)
        self.btn_confirm_accept.visible = self.edition_mode not in [
            LineChartAnnotator.ModePointAdd, LineChartAnnotator.ModePointEdit
        ]

    def btn_data_series_edit_click(self, button):
        if self.lbx_data_series_values.selected_option_value is None:
            print("Must select a data series")
            return

        option_idx = int(self.lbx_data_series_values.selected_option_value)

        # prepare temporals
        self.tempo_line_index = option_idx
        self.tempo_line_values = LineValues.Copy(self.data.lines[option_idx])

        # series name ...
        display = self.lbx_data_series_values.option_display[
            self.lbx_data_series_values.selected_option_value]
        self.lbl_line_name.set_text(display)

        # ... list of points ...
        self.update_points_list()

        self.set_editor_mode(LineChartAnnotator.ModeLineEdit)
        self.update_current_view(False)

    def btn_data_return_click(self, button):
        self.set_editor_mode(LineChartAnnotator.ModeNavigate)

    def update_points_list(self):
        self.lbx_line_points.clear_options()
        for idx, (p_x, p_y) in enumerate(self.tempo_line_values.points):
            display_value = "{0:d}: ({1:.1f}, {2:.1f})".format(
                idx + 1, p_x, p_y)

            self.lbx_line_points.add_option(str(idx), display_value)

    def btn_line_point_edit_click(self, button):
        if self.lbx_line_points.selected_option_value is None:
            print("Must select a data point from list")
            return

        self.tempo_point_index = int(
            self.lbx_line_points.selected_option_value)
        self.set_editor_mode(LineChartAnnotator.ModePointEdit)

    def delete_tempo_line_point(self, del_idx):
        if self.tempo_line_values.remove_point(del_idx):
            # update GUI
            self.update_points_list()
            self.update_current_view()

    def btn_line_point_delete_click(self, button):
        if self.lbx_line_points.selected_option_value is None:
            print("Must select a data point from list")
            return

        # try delete
        del_idx = int(self.lbx_line_points.selected_option_value)
        self.delete_tempo_line_point(del_idx)

    def btn_line_point_add_click(self, button):
        self.set_editor_mode(LineChartAnnotator.ModePointAdd)

    def btn_line_return_accept_click(self, button):
        self.data.lines[self.tempo_line_index] = LineValues.Copy(
            self.tempo_line_values)
        self.data_changed = True

        self.set_editor_mode(LineChartAnnotator.ModeLineSelect)
        self.update_current_view()

    def btn_line_return_cancel_click(self, button):
        self.set_editor_mode(LineChartAnnotator.ModeLineSelect)
        self.update_current_view(False)

    def lbx_line_points_value_changed(self, new_value, old_value):
        self.update_current_view(False)

    def img_mouse_double_click(self, img, position, button):
        if self.edition_mode == LineChartAnnotator.ModeLineEdit:
            # click relative position
            rel_x, rel_y = self.from_pos_to_rel_click(position)

            # find closest point ...
            distance, point_idx = self.tempo_line_values.closest_point(
                rel_x, rel_y)

            if button == 1:
                # left click
                if point_idx is not None and distance < LineChartAnnotator.DoubleClickMaxPointDistance:
                    # ... edit...
                    self.tempo_point_index = point_idx
                    self.set_editor_mode(LineChartAnnotator.ModePointEdit)
                else:
                    # ... add point ...
                    self.tempo_line_values.add_point(rel_x, rel_y,
                                                     LineValues.InsertByXValue)
                    # update GUI
                    self.update_points_list()

                self.update_current_view(False)
            else:
                # right click ... delete ...
                if distance < LineChartAnnotator.DoubleClickMaxPointDistance:
                    self.delete_tempo_line_point(point_idx)
class ChartImageAnnotator(BaseImageAnnotator):
    ModeNavigate = 0
    ModeEditPanels = 1
    ModeSelectPanelSplit = 2
    ModeConfirmOverwritePanels = 3
    ModeEditClass = 4
    ModeConfirmOverwriteClass = 5
    ModeConfirmExit = 6

    SplitOperationXSplit = 0
    SplitOperationYSplit = 1
    SplitOperationMerge = 2

    WaitModeNone = 0
    WaitModeText = 1
    WaitModeLegend = 2
    WaitModeAxes = 3
    WaitModeData = 4

    def __init__(self, size, chart_dir, annotation_dir, relative_path,
                 parent_menu, admin_mode):
        BaseImageAnnotator.__init__(self,
                                    "Chart Ground Truth Annotation Interface",
                                    size)

        self.general_background = (20, 85, 50)
        self.text_color = (255, 255, 255)

        self.parent = parent_menu
        self.chart_dir = chart_dir
        self.annotation_dir = annotation_dir
        self.relative_path = relative_path
        self.admin_mode = admin_mode

        self.time_stats = TimeStats()
        self.in_menu_time_start = time.time()
        self.wait_mode = ChartImageAnnotator.WaitModeNone

        # find output path ...
        relative_dir, img_filename = os.path.split(self.relative_path)
        img_base, ext = os.path.splitext(img_filename)
        # output dir
        self.output_dir = self.annotation_dir + relative_dir
        self.annotation_filename = self.output_dir + "/" + img_base + ".xml"

        # first ... load image ....
        self.base_rgb_image = cv2.imread(self.chart_dir + self.relative_path)
        self.base_rgb_image = cv2.cvtColor(self.base_rgb_image,
                                           cv2.COLOR_BGR2RGB)
        # ... and cache the gray-scale version
        self.base_gray_image = np.zeros(self.base_rgb_image.shape,
                                        self.base_rgb_image.dtype)
        self.base_gray_image[:, :, 0] = cv2.cvtColor(self.base_rgb_image,
                                                     cv2.COLOR_RGB2GRAY)
        self.base_gray_image[:, :, 1] = self.base_gray_image[:, :, 0].copy()
        self.base_gray_image[:, :, 2] = self.base_gray_image[:, :, 0].copy()

        # load annotations for this image .... (if any)
        if os.path.exists(self.annotation_filename):
            # annotation found!
            self.image_info = ImageInfo.FromXML(self.annotation_filename,
                                                self.base_rgb_image)
        else:
            # create an empty annotation
            self.image_info = ImageInfo.CreateDefault(self.base_rgb_image)

        self.split_panel_operation = None
        self.tempo_panel_tree = None
        self.selected_panel = 0
        self.unsaved_changes = False

        self.elements.back_color = self.general_background

        self.label_title = None
        self.container_confirm_buttons = None
        self.lbl_confirm_message = None
        self.btn_confirm_cancel = None
        self.btn_confirm_accept = None

        self.container_panels_buttons = None
        self.lbl_panels_title = None
        self.btn_label_panels = None
        self.btn_panels_verify = None
        self.lbl_panels_current = None
        self.btn_panels_prev = None
        self.btn_panels_next = None

        self.container_annotation_buttons = None
        self.lbl_edit_title = None
        self.btn_edit_class = None
        self.btn_edit_text = None
        self.btn_edit_legend = None
        self.btn_edit_axis = None
        self.btn_edit_data = None

        self.btn_verify_class = None
        self.btn_verify_text = None
        self.btn_verify_legend = None
        self.btn_verify_axis = None
        self.btn_verify_data = None

        self.container_split_panels = None
        self.lbl_split_panel_title = None
        self.btn_split_panel_horizontal = None
        self.btn_split_panel_vertical = None
        self.btn_merge_panel = None
        self.btn_split_return = None

        self.container_classify_panels = None
        self.lbl_class_panel_title = None
        self.lbx_class_panel_class = None
        self.btn_class_panel_continue = None

        self.btn_save = None
        self.btn_auto_check = None
        self.btn_return = None

        # generate the interface!
        self.create_controllers()

        # get the view ...
        self.update_current_view(True)

    def create_controllers(self):
        # add elements....
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # Main Title
        self.label_title = ScreenLabel(
            "title",
            "Chart Image Annotation Tool - {0:s}".format(self.relative_path),
            28)
        self.label_title.background = self.general_background
        self.label_title.position = (int(
            (self.width - self.label_title.width) / 2), 20)
        self.label_title.set_color(self.text_color)

        self.elements.append(self.label_title)

        container_top = 10 + self.label_title.get_bottom()
        container_width = 300

        button_width = 190
        button_left = (container_width - button_width) / 2

        button_2_width = 130
        button_2_left = int(container_width * 0.25) - button_2_width / 2
        button_2_right = int(container_width * 0.75) - button_2_width / 2

        # ===========================
        # View Options Panel
        self.create_image_annotator_controls(container_top, container_width,
                                             self.general_background,
                                             self.text_color,
                                             button_text_color,
                                             button_back_color)

        # ===========================
        # confirmation panel
        self.container_confirm_buttons = ScreenContainer(
            "container_confirm_buttons", (container_width, 70),
            back_color=self.general_background)
        self.container_confirm_buttons.position = (
            self.width - self.container_confirm_buttons.width - 10,
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_confirm_buttons)
        self.container_confirm_buttons.visible = False

        self.lbl_confirm_message = ScreenLabel(
            "lbl_confirm_message", "Confirmation message goes here?", 21, 290,
            1)
        self.lbl_confirm_message.position = (5, 5)
        self.lbl_confirm_message.set_background(self.general_background)
        self.lbl_confirm_message.set_color(self.text_color)
        self.container_confirm_buttons.append(self.lbl_confirm_message)

        self.btn_confirm_cancel = ScreenButton("btn_confirm_cancel", "Cancel",
                                               21, 130)
        self.btn_confirm_cancel.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_cancel.position = (
            10, self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_cancel.click_callback = self.btn_confirm_cancel_click
        self.container_confirm_buttons.append(self.btn_confirm_cancel)

        self.btn_confirm_accept = ScreenButton("btn_confirm_accept", "Accept",
                                               21, 130)
        self.btn_confirm_accept.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_accept.position = (
            self.container_confirm_buttons.width -
            self.btn_confirm_accept.width - 10,
            self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_accept.click_callback = self.btn_confirm_accept_click
        self.container_confirm_buttons.append(self.btn_confirm_accept)

        # ===========================

        # panel for multi-panel figure options
        panel_buttons_bg = (10, 55, 25)
        self.container_panels_buttons = ScreenContainer(
            "container_panels_buttons", (container_width, 160),
            back_color=panel_buttons_bg)
        self.container_panels_buttons.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_panels_buttons)

        self.lbl_panels_title = ScreenLabel("lbl_panels_title",
                                            "Options for Multi-Panel Images",
                                            21, 290, 1)
        self.lbl_panels_title.position = (5, 5)
        self.lbl_panels_title.set_background(panel_buttons_bg)
        self.lbl_panels_title.set_color(self.text_color)
        self.container_panels_buttons.append(self.lbl_panels_title)

        if self.admin_mode:
            self.btn_label_panels = ScreenButton("btn_label_panels",
                                                 "Annotate", 21,
                                                 button_2_width)
            self.btn_label_panels.position = (
                button_2_left, self.lbl_panels_title.get_bottom() + 10)

            self.btn_panels_verify = ScreenButton("btn_panels_verify",
                                                  "Mark Verified", 21,
                                                  button_2_width)
            self.btn_panels_verify.position = (
                button_2_right, self.lbl_panels_title.get_bottom() + 10)
            self.btn_panels_verify.set_colors(button_text_color,
                                              button_back_color)
            self.btn_panels_verify.click_callback = self.btn_panels_verify_click
            self.container_panels_buttons.append(self.btn_panels_verify)
        else:
            self.btn_label_panels = ScreenButton("btn_label_panels",
                                                 "Annotate Panels", 21,
                                                 button_width)
            self.btn_label_panels.position = (
                button_left, self.lbl_panels_title.get_bottom() + 10)

            self.btn_panels_verify = None

        self.btn_label_panels.set_colors(button_text_color, button_back_color)
        self.btn_label_panels.click_callback = self.btn_label_panels_click
        self.container_panels_buttons.append(self.btn_label_panels)

        self.lbl_panels_current = ScreenLabel("lbl_panels_title",
                                              "Current Panel: 1 / 1", 21, 290,
                                              1)
        self.lbl_panels_current.position = (
            5, self.btn_label_panels.get_bottom() + 20)
        self.lbl_panels_current.set_background(panel_buttons_bg)
        self.lbl_panels_current.set_color(self.text_color)
        self.container_panels_buttons.append(self.lbl_panels_current)

        self.btn_panels_prev = ScreenButton("btn_panels_prev", "Previous", 21,
                                            130)
        self.btn_panels_prev.set_colors(button_text_color, button_back_color)
        self.btn_panels_prev.position = (10,
                                         self.lbl_panels_current.get_bottom() +
                                         10)
        self.btn_panels_prev.click_callback = self.btn_panels_prev_click
        self.container_panels_buttons.append(self.btn_panels_prev)

        self.btn_panels_next = ScreenButton("btn_panels_next", "Next", 21, 130)
        self.btn_panels_next.set_colors(button_text_color, button_back_color)
        self.btn_panels_next.position = (self.container_panels_buttons.width -
                                         self.btn_panels_next.width - 10,
                                         self.lbl_panels_current.get_bottom() +
                                         10)
        self.btn_panels_next.click_callback = self.btn_panels_next_click
        self.container_panels_buttons.append(self.btn_panels_next)

        # =================================================

        self.container_annotation_buttons = ScreenContainer(
            "container_annotation_buttons", (container_width, 380),
            back_color=panel_buttons_bg)
        self.container_annotation_buttons.position = (
            self.container_panels_buttons.get_left(),
            self.container_panels_buttons.get_bottom() + 20)
        self.elements.append(self.container_annotation_buttons)

        self.lbl_edit_title = ScreenLabel("lbl_edit_title",
                                          "Options for Current Panel", 21, 290,
                                          1)
        self.lbl_edit_title.position = (5, 5)
        self.lbl_edit_title.set_background(panel_buttons_bg)
        self.lbl_edit_title.set_color(self.text_color)
        self.container_annotation_buttons.append(self.lbl_edit_title)

        if self.admin_mode:
            edit_button_left = button_2_left
            edit_button_width = 180
            verify_button_left = edit_button_left + edit_button_width + 10
            verify_button_width = 90
        else:
            edit_button_left = button_left
            edit_button_width = button_width
            verify_button_left = 0
            verify_button_width = 0

        self.btn_edit_class = ScreenButton("btn_edit_class",
                                           "Edit Classification", 21,
                                           edit_button_width)
        self.btn_edit_class.set_colors(button_text_color, button_back_color)
        self.btn_edit_class.position = (edit_button_left,
                                        self.lbl_edit_title.get_bottom() + 10)
        self.btn_edit_class.click_callback = self.btn_edit_class_click
        self.container_annotation_buttons.append(self.btn_edit_class)

        self.btn_edit_text = ScreenButton("btn_edit_text", "Edit Text", 21,
                                          edit_button_width)
        self.btn_edit_text.set_colors(button_text_color, button_back_color)
        self.btn_edit_text.position = (edit_button_left,
                                       self.btn_edit_class.get_bottom() + 10)
        self.btn_edit_text.click_callback = self.btn_edit_text_click
        self.container_annotation_buttons.append(self.btn_edit_text)

        self.btn_edit_legend = ScreenButton("btn_edit_legend", "Edit Legend",
                                            21, edit_button_width)
        self.btn_edit_legend.set_colors(button_text_color, button_back_color)
        self.btn_edit_legend.position = (edit_button_left,
                                         self.btn_edit_text.get_bottom() + 10)
        self.btn_edit_legend.click_callback = self.btn_edit_legend_click
        self.container_annotation_buttons.append(self.btn_edit_legend)

        self.btn_edit_axis = ScreenButton("btn_edit_axis", "Edit Axis", 21,
                                          edit_button_width)
        self.btn_edit_axis.set_colors(button_text_color, button_back_color)
        self.btn_edit_axis.position = (edit_button_left,
                                       self.btn_edit_legend.get_bottom() + 10)
        self.btn_edit_axis.click_callback = self.btn_edit_axis_click
        self.container_annotation_buttons.append(self.btn_edit_axis)

        self.btn_edit_data = ScreenButton("btn_edit_data", "Edit Data", 21,
                                          edit_button_width)
        self.btn_edit_data.set_colors(button_text_color, button_back_color)
        self.btn_edit_data.position = (edit_button_left,
                                       self.btn_edit_axis.get_bottom() + 10)
        self.btn_edit_data.click_callback = self.btn_edit_data_click
        self.container_annotation_buttons.append(self.btn_edit_data)

        self.btn_auto_check = ScreenButton("btn_auto_check", "Auto Check", 21,
                                           button_2_width)
        self.btn_auto_check.set_colors(button_text_color, button_back_color)
        self.btn_auto_check.position = (button_2_left,
                                        self.btn_edit_data.get_bottom() + 30)
        self.btn_auto_check.click_callback = self.btn_auto_check_click
        self.container_annotation_buttons.append(self.btn_auto_check)

        self.btn_save = ScreenButton("btn_save", "Save", 21, button_2_width)
        self.btn_save.set_colors(button_text_color, button_back_color)
        self.btn_save.position = (button_2_right,
                                  self.btn_edit_data.get_bottom() + 30)
        self.btn_save.click_callback = self.btn_save_click
        self.container_annotation_buttons.append(self.btn_save)

        self.btn_return = ScreenButton("btn_return", "Return", 21,
                                       button_width)
        self.btn_return.set_colors(button_text_color, button_back_color)
        self.btn_return.position = (button_left,
                                    self.container_annotation_buttons.height -
                                    self.btn_return.height - 10)
        self.btn_return.click_callback = self.btn_return_click
        self.container_annotation_buttons.append(self.btn_return)

        if self.admin_mode:
            self.btn_verify_class = ScreenButton("btn_verify_class",
                                                 "Verified", 21,
                                                 verify_button_width)
            self.btn_verify_class.set_colors(button_text_color,
                                             button_back_color)
            self.btn_verify_class.position = (verify_button_left,
                                              self.btn_edit_class.get_top())
            self.btn_verify_class.click_callback = self.btn_verify_class_click
            self.container_annotation_buttons.append(self.btn_verify_class)

            self.btn_verify_text = ScreenButton("btn_verify_text", "Verified",
                                                21, verify_button_width)
            self.btn_verify_text.set_colors(button_text_color,
                                            button_back_color)
            self.btn_verify_text.position = (verify_button_left,
                                             self.btn_edit_text.get_top())
            self.btn_verify_text.click_callback = self.btn_verify_text_click
            self.container_annotation_buttons.append(self.btn_verify_text)

            self.btn_verify_legend = ScreenButton("btn_verify_legend",
                                                  "Verified", 21,
                                                  verify_button_width)
            self.btn_verify_legend.set_colors(button_text_color,
                                              button_back_color)
            self.btn_verify_legend.position = (verify_button_left,
                                               self.btn_edit_legend.get_top())
            self.btn_verify_legend.click_callback = self.btn_verify_legend_click
            self.container_annotation_buttons.append(self.btn_verify_legend)

            self.btn_verify_axis = ScreenButton("btn_verify_axis", "Verified",
                                                21, verify_button_width)
            self.btn_verify_axis.set_colors(button_text_color,
                                            button_back_color)
            self.btn_verify_axis.position = (verify_button_left,
                                             self.btn_edit_axis.get_top())
            self.btn_verify_axis.click_callback = self.btn_verify_axis_click
            self.container_annotation_buttons.append(self.btn_verify_axis)

            self.btn_verify_data = ScreenButton("btn_verify_data", "Verified",
                                                21, verify_button_width)
            self.btn_verify_data.set_colors(button_text_color,
                                            button_back_color)
            self.btn_verify_data.position = (verify_button_left,
                                             self.btn_edit_data.get_top())
            self.btn_verify_data.click_callback = self.btn_verify_data_click
            self.container_annotation_buttons.append(self.btn_verify_data)

        # =======

        self.container_split_panels = ScreenContainer(
            "container_split_panels", (container_width, 220),
            back_color=panel_buttons_bg)
        self.container_split_panels.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_split_panels)

        self.lbl_split_panel_title = ScreenLabel("lbl_split_panel_title",
                                                 "Edit Panels Options", 21,
                                                 290, 1)
        self.lbl_split_panel_title.position = (5, 5)
        self.lbl_split_panel_title.set_background(panel_buttons_bg)
        self.lbl_split_panel_title.set_color(self.text_color)
        self.container_split_panels.append(self.lbl_split_panel_title)

        self.btn_split_panel_horizontal = ScreenButton(
            "btn_split_panel_horizontal", "Y - Split", 21, button_width)
        self.btn_split_panel_horizontal.set_colors(button_text_color,
                                                   button_back_color)
        self.btn_split_panel_horizontal.position = (
            button_left, self.lbl_split_panel_title.get_bottom() + 10)
        self.btn_split_panel_horizontal.click_callback = self.btn_split_panel_horizontal_click
        self.container_split_panels.append(self.btn_split_panel_horizontal)

        self.btn_split_panel_vertical = ScreenButton(
            "btn_split_panel_vertical", "X - Split", 21, button_width)
        self.btn_split_panel_vertical.set_colors(button_text_color,
                                                 button_back_color)
        self.btn_split_panel_vertical.position = (
            button_left, self.btn_split_panel_horizontal.get_bottom() + 10)
        self.btn_split_panel_vertical.click_callback = self.btn_split_panel_vertical_click
        self.container_split_panels.append(self.btn_split_panel_vertical)

        self.btn_merge_panel = ScreenButton("btn_merge_panel", "Merge Panels",
                                            21, button_width)
        self.btn_merge_panel.set_colors(button_text_color, button_back_color)
        self.btn_merge_panel.position = (
            button_left, self.btn_split_panel_vertical.get_bottom() + 10)
        self.btn_merge_panel.click_callback = self.btn_merge_panel_click
        self.container_split_panels.append(self.btn_merge_panel)

        self.btn_split_return = ScreenButton("btn_split_return", "Return", 21,
                                             button_width)
        self.btn_split_return.set_colors(button_text_color, button_back_color)
        self.btn_split_return.position = (button_left,
                                          self.btn_merge_panel.get_bottom() +
                                          10)
        self.btn_split_return.click_callback = self.btn_split_return_click
        self.container_split_panels.append(self.btn_split_return)

        self.container_split_panels.visible = False

        # =====================================
        self.container_classify_panels = ScreenContainer(
            "container_classify_panels", (container_width, 420),
            back_color=panel_buttons_bg)
        self.container_classify_panels.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_classify_panels)

        self.lbl_class_panel_title = ScreenLabel("lbl_class_panel_title",
                                                 "Classify Panel", 21, 290, 1)
        self.lbl_class_panel_title.position = (5, 5)
        self.lbl_class_panel_title.set_background(panel_buttons_bg)
        self.lbl_class_panel_title.set_color(self.text_color)
        self.container_classify_panels.append(self.lbl_class_panel_title)

        self.lbx_class_panel_class = ScreenTextlist(
            "lbx_class_panel_class", (container_width - 20, 320), 22)
        self.lbx_class_panel_class.position = (
            10, self.lbl_class_panel_title.get_bottom() + 10)
        self.container_classify_panels.append(self.lbx_class_panel_class)
        self.add_chart_types()

        self.btn_class_panel_continue = ScreenButton(
            "btn_class_panel_continue", "Continue", 21, button_width)
        self.btn_class_panel_continue.set_colors(button_text_color,
                                                 button_back_color)
        self.btn_class_panel_continue.position = (
            button_left, self.container_classify_panels.height -
            self.btn_class_panel_continue.height - 10)
        self.btn_class_panel_continue.click_callback = self.btn_class_panel_continue_click
        self.container_classify_panels.append(self.btn_class_panel_continue)

        self.container_classify_panels.visible = False

        # =======================================

        self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
        self.update_panel_info()

    def add_chart_types(self):
        self.lbx_class_panel_class.add_option(str(ChartInfo.TypeNonChart),
                                              "Non-Chart")
        self.lbx_class_panel_class.add_option(str(ChartInfo.TypeLine),
                                              "Line Chart")
        self.lbx_class_panel_class.add_option(str(ChartInfo.TypeScatter),
                                              "Scatter Chart")

        self.lbx_class_panel_class.add_option(
            str(ChartInfo.TypeBar) + "-" +
            str(ChartInfo.OrientationHorizontal), "Bar Chart (Horizontal)")
        self.lbx_class_panel_class.add_option(
            str(ChartInfo.TypeBar) + "-" + str(ChartInfo.OrientationVertical),
            "Bar Chart (Vertical)")

        self.lbx_class_panel_class.add_option(
            str(ChartInfo.TypeBox) + "-" +
            str(ChartInfo.OrientationHorizontal), "Box Chart (Horizontal)")
        self.lbx_class_panel_class.add_option(
            str(ChartInfo.TypeBox) + "-" + str(ChartInfo.OrientationVertical),
            "Box Chart (Vertical)")

    def prepare_screen(self):
        if self.wait_mode != ChartImageAnnotator.WaitModeNone:
            # get delta of time on previous screen ... prepare to count towards this menu
            delta = self.get_reset_time_delta()
            # add delta on the corresponding process...
            if self.wait_mode == ChartImageAnnotator.WaitModeText:
                self.time_stats.time_text += delta
            elif self.wait_mode == ChartImageAnnotator.WaitModeLegend:
                self.time_stats.time_legend += delta
            elif self.wait_mode == ChartImageAnnotator.WaitModeAxes:
                self.time_stats.time_axes += delta
            elif self.wait_mode == ChartImageAnnotator.WaitModeData:
                self.time_stats.time_data += delta

            self.wait_mode = ChartImageAnnotator.WaitModeNone

        # call parent prepare screen ...
        Screen.prepare_screen(self)

    def btn_confirm_accept_click(self, button):
        if self.edition_mode == ChartImageAnnotator.ModeConfirmOverwritePanels:
            # commit changes ....
            # ... create an empty annotation ...
            self.image_info = ImageInfo.CreateDefault(self.base_rgb_image)
            # copy the panel structure ...
            self.image_info.panel_tree = PanelTree.Copy(self.tempo_panel_tree)
            # ... get the empty annotation for each panel ...
            self.image_info.reset_panels_info()

            # go back to navigation mode ...
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
            self.update_current_view(False)
            self.selected_panel = 0
            self.update_panel_info()

            self.unsaved_changes = True

        elif self.edition_mode == ChartImageAnnotator.ModeConfirmOverwriteClass:
            # commit changes ....
            # ... get selected class ...
            if "-" in self.lbx_class_panel_class.selected_option_value:
                # Chart type with orientation
                type_str, orientation_str = self.lbx_class_panel_class.selected_option_value.split(
                    "-")
                type_value = int(type_str)
                orientation = int(orientation_str)
            else:
                # No orientation
                type_value = int(
                    self.lbx_class_panel_class.selected_option_value)
                orientation = None

            if self.admin_mode:
                overwrite = input("Discard Chart Info (y/n)? ").lower() in [
                    "y", "yes", "1", "true"
                ]
            else:
                overwrite = True

            if overwrite:
                # ... create an empty panel annotation ...
                self.image_info.panels[self.selected_panel] = ChartInfo(
                    type_value, orientation)
            else:
                # ... admin request simple overwrite of values which might lead to inconsistencies ...
                self.image_info.panels[self.selected_panel].type = type_value
                self.image_info.panels[
                    self.selected_panel].orientation = orientation

            # go back to navigation mode ...
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
            self.update_current_view(False)

        elif self.edition_mode == ChartImageAnnotator.ModeConfirmExit:
            # return with unsaved changes lost
            print("Unsaved changes on " + self.relative_path + " were lost")

            delta = self.get_reset_time_delta()
            self.time_stats.time_main += delta
            self.parent.update_annotation_times(self.time_stats)

            self.parent.refresh_page()
            self.return_screen = self.parent

    def btn_confirm_cancel_click(self, button):
        if self.edition_mode == ChartImageAnnotator.ModeSelectPanelSplit:
            # simply go back ...
            self.set_editor_mode(ChartImageAnnotator.ModeEditPanels)
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmOverwritePanels:
            # go back to navigation mode ... no changes commited
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
            self.update_current_view(False)
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmOverwriteClass:
            # go back to navigation mode ... no changes commited
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
            self.update_current_view(False)
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmExit:
            # go back to navigation mode ... no changes commited
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
            self.update_current_view(False)

    def get_reset_time_delta(self):
        new_time = time.time()
        delta = new_time - self.in_menu_time_start
        self.in_menu_time_start = new_time

        return delta

    def btn_label_panels_click(self, button):
        self.time_stats.time_main += self.get_reset_time_delta()
        self.tempo_panel_tree = PanelTree.Copy(self.image_info.panel_tree)
        self.set_editor_mode(ChartImageAnnotator.ModeEditPanels)

    def btn_panels_prev_click(self, button):
        if self.selected_panel > 0:
            self.selected_panel -= 1
            self.update_current_view(False)
            self.update_panel_info()

    def btn_panels_next_click(self, button):
        if self.selected_panel + 1 < len(self.image_info.panels):
            self.selected_panel += 1
            self.update_current_view(False)
            self.update_panel_info()

    def btn_return_click(self, button):
        if not self.unsaved_changes:
            print("Edition for " + self.relative_path + " completed")

            delta = self.get_reset_time_delta()
            self.time_stats.time_main += delta
            self.parent.update_annotation_times(self.time_stats)

            self.parent.refresh_page()
            self.return_screen = self.parent

        else:
            # go into confirmation mode
            self.set_editor_mode(ChartImageAnnotator.ModeConfirmExit)

    def btn_save_click(self, button):
        # create dirs (if they don't exist)
        os.makedirs(self.output_dir, exist_ok=True)

        xml_str = self.image_info.to_XML()
        with open(self.annotation_filename, 'w', encoding="utf-8") as out_file:
            out_file.write(xml_str)

        print("Data saved to: " + self.annotation_filename)
        self.unsaved_changes = False

    def save_json_file(self, json_content, json_filename):
        with open(json_filename, "w") as tempo_file:
            tempo_str = json.dumps(json_content, indent="\t")
            tempo_file.write(tempo_str)

    def btn_auto_check_click(self, button):
        status = ImageInfo.GetAllStatuses(self.image_info)

        false_status = [(2 if val > 0 else 0) for val in status]
        panel_info = self.image_info.panels[self.selected_panel]

        self.unsaved_changes = True
        panel_info.properties["auto_check_passed"] = 0

        # class ...
        if status[1] >= 1:
            try:
                tempo_json = ChartJSON_Exporter.prepare_chart_image_json(
                    panel_info, false_status, 1, False)
                print("Task 1: Annotation Seems Okay!")
                self.save_json_file(tempo_json, "TEMPO_VALID_JSON.json")
            except Exception as e:
                print("Errors on Task 1 data (Chart Image Classification): " +
                      str(e))
                return

        # text detection, classification and recognition ...
        if status[2] >= 1:
            try:
                tempo_json = ChartJSON_Exporter.prepare_chart_image_json(
                    panel_info, false_status, 3, False)
                print("Tasks 2 and 3: Annotation Seems Okay!")
                self.save_json_file(tempo_json, "TEMPO_VALID_JSON.json")
            except Exception as e:
                print(
                    "Errors on Task 2 or 3 (Text Detection, Recognition, and Classification): "
                    + str(e))
                return

        if status[3] >= 1 and status[4] >= 1:
            try:
                tempo_json = ChartJSON_Exporter.prepare_chart_image_json(
                    panel_info, false_status, 5, False)
                print("Tasks 4 and 5: Annotation Seems Okay!")
                self.save_json_file(tempo_json, "TEMPO_VALID_JSON.json")
            except Exception as e:
                print(
                    "Errors on Task 4 and/or 5 (Axes and Legend Recognition): "
                    + str(e))
                return

        if status[5] >= 1:
            try:
                tempo_json = ChartJSON_Exporter.prepare_chart_image_json(
                    panel_info, false_status, 7, False)
                print("Tasks 6a/6b: Annotation Seems Okay!")
                self.save_json_file(tempo_json, "TEMPO_VALID_JSON.json")
            except Exception as e:
                print("Errors on Task 6a/6b (Data Extraction): " + str(e))
                return

        panel_info.properties["auto_check_passed"] = 1
        print("Chart meets all annotation requirements!")

    def btn_edit_class_click(self, button):
        prev_key = self.get_image_class_key()
        self.lbx_class_panel_class.change_option_selected(prev_key)

        delta = self.get_reset_time_delta()
        self.time_stats.time_main += delta
        self.set_editor_mode(ChartImageAnnotator.ModeEditClass)

    def btn_edit_text_click(self, button):
        # prepare time count for text ...
        delta = self.get_reset_time_delta()
        self.time_stats.time_main += delta
        self.wait_mode = ChartImageAnnotator.WaitModeText

        panel_image = self.image_info.get_panel_image(self.selected_panel)

        text_annotator = ChartTextAnnotator(
            self.size, panel_image,
            self.image_info.panels[self.selected_panel], self, self.admin_mode)
        text_annotator.prepare_screen()
        # text_annotator.copy_view(self)

        self.return_screen = text_annotator

    def btn_edit_legend_click(self, button):
        # prepare time count for legend ...
        delta = self.get_reset_time_delta()
        self.time_stats.time_main += delta
        self.wait_mode = ChartImageAnnotator.WaitModeLegend

        panel_image = self.image_info.get_panel_image(self.selected_panel)

        legend_annotator = ChartLegendAnnotator(
            self.size, panel_image,
            self.image_info.panels[self.selected_panel], self)
        legend_annotator.prepare_screen()

        self.return_screen = legend_annotator

    def btn_edit_axis_click(self, button):
        # prepare time count for axis ...
        delta = self.get_reset_time_delta()
        self.time_stats.time_main += delta
        self.wait_mode = ChartImageAnnotator.WaitModeAxes

        panel_image = self.image_info.get_panel_image(self.selected_panel)

        axes_annotator = ChartAxesAnnotator(
            self.size, panel_image,
            self.image_info.panels[self.selected_panel], self)
        axes_annotator.prepare_screen()

        self.return_screen = axes_annotator

    def btn_edit_data_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]

        if not current_panel.check_classes():
            print("Must select a valid chart type to annotate its data!")
            return

        if not current_panel.check_text():
            print("Text must be annotated first!")
            return

        if not current_panel.check_axes():
            print("Axes must be annotated first!")
            return

        panel_image = self.image_info.get_panel_image(self.selected_panel)

        if self.image_info.panels[
                self.selected_panel].type == ChartInfo.TypeBar:
            data_annotator = BarChartAnnotator(self.size, panel_image,
                                               current_panel, self)
        elif self.image_info.panels[
                self.selected_panel].type == ChartInfo.TypeBox:
            data_annotator = BoxChartAnnotator(self.size, panel_image,
                                               current_panel, self)
        elif self.image_info.panels[
                self.selected_panel].type == ChartInfo.TypeLine:
            data_annotator = LineChartAnnotator(self.size, panel_image,
                                                current_panel, self)
        elif self.image_info.panels[
                self.selected_panel].type == ChartInfo.TypeScatter:
            data_annotator = ScatterChartAnnotator(self.size, panel_image,
                                                   current_panel, self)
        else:
            raise Exception("Not implemented!!")

        # prepare time count for data ...
        delta = self.get_reset_time_delta()
        self.time_stats.time_main += delta
        self.wait_mode = ChartImageAnnotator.WaitModeData

        data_annotator.prepare_screen()

        self.return_screen = data_annotator

    def custom_view_update(self, modified_image):
        if self.edition_mode in [
                ChartImageAnnotator.ModeEditPanels,
                ChartImageAnnotator.ModeSelectPanelSplit,
                ChartImageAnnotator.ModeConfirmOverwritePanels
        ]:
            # get panels from temporary tree
            panel_nodes = self.tempo_panel_tree.root.get_leaves()
            temporary_tree = True
        else:
            # get from current tree
            panel_nodes = self.image_info.panel_tree.root.get_leaves()
            temporary_tree = False

        for idx, panel_node in enumerate(panel_nodes):
            # mark the panel boundaries ...
            if idx == self.selected_panel and self.edition_mode == ChartImageAnnotator.ModeNavigate:
                border_color = (0, 255, 0)
            else:
                border_color = (255, 0, 0)

            if not temporary_tree:
                # show the chart type (and orientation)
                chart_type, chart_orientation = self.image_info.panels[
                    idx].get_description()
                chart_desc = "{0:s} ({1:s})".format(chart_type,
                                                    chart_orientation)
                font_scale = 2
                font_face = cv2.FONT_HERSHEY_PLAIN
                font_thickness = 1
                font_padding = 2

                text_size, font_baseline = cv2.getTextSize(
                    chart_desc, font_face, font_scale, font_thickness)

                modified_image[panel_node.y1:panel_node.y1 + text_size[1] + 10,
                               panel_node.x1:panel_node.x1 + text_size[0] +
                               10] = 255

                vertical_displacement = font_padding + text_size[1]
                text_loc = (panel_node.x1 + font_padding,
                            panel_node.y1 + vertical_displacement)
                shadow_loc = (text_loc[0] + 2, text_loc[1] + 1)
                cv2.putText(modified_image,
                            chart_desc,
                            shadow_loc,
                            font_face,
                            font_scale, (0, 0, 0),
                            thickness=font_thickness,
                            lineType=cv2.LINE_AA)

                cv2.putText(modified_image,
                            chart_desc,
                            text_loc,
                            font_face,
                            font_scale,
                            border_color,
                            thickness=font_thickness,
                            lineType=cv2.LINE_AA)

            cv2.rectangle(modified_image, (panel_node.x1, panel_node.y1),
                          (panel_node.x2, panel_node.y2),
                          border_color,
                          thickness=2)

        # TODO: show here any relevant annotations on the modified image ...

    def btn_split_panel_horizontal_click(self, button):
        self.split_panel_operation = ChartImageAnnotator.SplitOperationYSplit
        self.set_editor_mode(ChartImageAnnotator.ModeSelectPanelSplit)

    def btn_split_panel_vertical_click(self, button):
        self.split_panel_operation = ChartImageAnnotator.SplitOperationXSplit
        self.set_editor_mode(ChartImageAnnotator.ModeSelectPanelSplit)

    def btn_merge_panel_click(self, button):
        self.split_panel_operation = ChartImageAnnotator.SplitOperationMerge
        self.set_editor_mode(ChartImageAnnotator.ModeSelectPanelSplit)

    def btn_split_return_click(self, button):
        # add time delta for panels ... from here, confirm or not will count towards main screen time
        delta = self.get_reset_time_delta()
        self.time_stats.time_panels += delta

        if self.tempo_panel_tree == self.image_info.panel_tree:
            # nothing changed ...
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)
        else:
            self.set_editor_mode(
                ChartImageAnnotator.ModeConfirmOverwritePanels)

    def set_editor_mode(self, new_mode):
        self.edition_mode = new_mode

        # Navigation mode ...
        self.container_panels_buttons.visible = (
            self.edition_mode == ChartImageAnnotator.ModeNavigate)
        self.container_annotation_buttons.visible = (
            self.edition_mode == ChartImageAnnotator.ModeNavigate)

        # Edit panels ...
        self.container_split_panels.visible = (
            self.edition_mode == ChartImageAnnotator.ModeEditPanels)
        self.container_classify_panels.visible = (
            self.edition_mode == ChartImageAnnotator.ModeEditClass)

        # Confirm panel and buttons  ...
        self.container_confirm_buttons.visible = self.edition_mode in [
            ChartImageAnnotator.ModeSelectPanelSplit,
            ChartImageAnnotator.ModeConfirmOverwritePanels,
            ChartImageAnnotator.ModeConfirmOverwriteClass,
            ChartImageAnnotator.ModeConfirmExit
        ]

        if self.edition_mode == ChartImageAnnotator.ModeSelectPanelSplit:
            if self.split_panel_operation == ChartImageAnnotator.SplitOperationYSplit:
                self.lbl_confirm_message.set_text("Select Point for Y - Split")
            elif self.split_panel_operation == ChartImageAnnotator.SplitOperationXSplit:
                self.lbl_confirm_message.set_text("Select Point for X - Split")
            elif self.split_panel_operation == ChartImageAnnotator.SplitOperationMerge:
                self.lbl_confirm_message.set_text("Select a panel to Merge")
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmOverwritePanels:
            self.lbl_confirm_message.set_text(
                "Discard Previous Image Annotations?")
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmOverwriteClass:
            self.lbl_confirm_message.set_text(
                "Discard Previous Panel Annotations?")
        elif self.edition_mode == ChartImageAnnotator.ModeConfirmExit:
            self.lbl_confirm_message.set_text("Discard unsaved changes?")

        self.btn_confirm_accept.visible = (
            self.container_confirm_buttons.visible
            and self.edition_mode != ChartImageAnnotator.ModeSelectPanelSplit)

    def img_main_mouse_button_down(self, img, pos, button):
        if self.edition_mode == ChartImageAnnotator.ModeSelectPanelSplit:
            scaled_x, scaled_y = pos
            click_x = int(scaled_x / self.view_scale)
            click_y = int(scaled_y / self.view_scale)

            selected_nodes = self.tempo_panel_tree.root.find_point_containers(
                click_x, click_y, False)
            if len(selected_nodes) == 1:
                current_node = selected_nodes[0]
                if self.split_panel_operation == ChartImageAnnotator.SplitOperationYSplit:
                    current_node.horizontal_split(click_y)
                elif self.split_panel_operation == ChartImageAnnotator.SplitOperationXSplit:
                    current_node.vertical_split(click_x)
                elif self.split_panel_operation == ChartImageAnnotator.SplitOperationMerge:
                    current_node.merge_with_parent()

                self.set_editor_mode(ChartImageAnnotator.ModeEditPanels)
                self.update_current_view(False)

    def update_panel_info(self):
        msg = "Current Panel: {0:d} / {1:d}".format(
            self.selected_panel + 1, len(self.image_info.panels))
        self.lbl_panels_current.set_text(msg)

    def btn_class_panel_continue_click(self, button):
        # finish time for classification ... from here, next time will count towards main menu ...
        delta = self.get_reset_time_delta()
        self.time_stats.time_classification += delta

        prev_key = self.get_image_class_key()
        if prev_key != self.lbx_class_panel_class.selected_option_value:
            self.set_editor_mode(ChartImageAnnotator.ModeConfirmOverwriteClass)
        else:
            # nothing was changed ...
            self.set_editor_mode(ChartImageAnnotator.ModeNavigate)

    def get_image_class_key(self):
        current_panel = self.image_info.panels[self.selected_panel]
        class_key = str(current_panel.type)
        if current_panel.type in [ChartInfo.TypeBar, ChartInfo.TypeBox]:
            class_key += "-" + str(current_panel.orientation)

        return class_key

    def subtool_completed(self, data_changed):
        if data_changed:
            self.unsaved_changes = True

    def btn_panels_verify_click(self, button):
        self.image_info.properties["VERIFIED_01_PANELS"] = time.time()
        self.unsaved_changes = True
        print("Image Panels have been marked as verified!!!")

    def btn_verify_class_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]
        current_panel.set_classes_verified(True)
        self.unsaved_changes = True

        print("Current Panels Classification has been marked as verified!!!")

    def btn_verify_text_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]
        current_panel.set_text_verified(True)
        self.unsaved_changes = True

        print("Current Panels Text has been marked as verified!!!")

    def btn_verify_legend_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]
        current_panel.set_legend_verified(True)
        self.unsaved_changes = True

        print("Current Panels Legend has been marked as verified!!!")

    def btn_verify_axis_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]
        current_panel.set_axes_verified(True)
        self.unsaved_changes = True

        print("Current Panels Axes have been marked as verified!!!")

    def btn_verify_data_click(self, button):
        current_panel = self.image_info.panels[self.selected_panel]
        current_panel.set_data_verified(True)
        self.unsaved_changes = True

        print("Current Panels Data has been marked as verified!!!")
Example #4
0
class ChartLegendAnnotator(BaseImageAnnotator):
    ModeNavigate = 0
    ModeRectangleSelect = 1
    ModeRectangleEdit = 2
    ModeConfirmExit = 3

    def __init__(self, size, panel_image, panel_info, parent_screen):
        BaseImageAnnotator.__init__(self, "Chart Legend Ground Truth Annotation Interface", size)

        self.base_rgb_image = panel_image
        self.base_gray_image = np.zeros(self.base_rgb_image.shape, self.base_rgb_image.dtype)
        self.base_gray_image[:, :, 0] = cv2.cvtColor(self.base_rgb_image, cv2.COLOR_RGB2GRAY)
        self.base_gray_image[:, :, 1] = self.base_gray_image[:, :, 0].copy()
        self.base_gray_image[:, :, 2] = self.base_gray_image[:, :, 0].copy()

        self.panel_info = panel_info

        if self.panel_info.legend is None:
            # create a new legend info
            self.legend = LegendInfo(self.panel_info.get_all_text(TextInfo.TypeLegendLabel))
            # try to automatically find legend elements ...
            self.estimate_legend_boxes()
            self.data_changed = True
        else:
            # make a copy
            self.legend = LegendInfo.Copy(self.panel_info.legend)
            self.data_changed = False

        self.parent_screen = parent_screen

        self.general_background = (80, 70, 150)
        self.text_color = (255, 255, 255)

        self.elements.back_color = self.general_background

        self.edition_mode = None
        self.tempo_text_id = None

        self.label_title = None

        self.container_confirm_buttons = None
        self.lbl_confirm_message = None
        self.btn_confirm_cancel = None
        self.btn_confirm_accept = None

        self.container_legend_options = None
        self.lbl_legend_title = None
        self.lbx_legend_list = None
        self.btn_legend_edit = None
        self.btn_legend_delete = None
        self.btn_return_accept = None
        self.btn_return_cancel = None

        self.create_controllers()

        # get the view ...
        self.update_current_view(True)

    def get_best_assignments(self, cost_matrix, n_ccs, max_distance):
        # use the Munkres algorithm to find the optimal assignments
        m = Munkres()
        assignments = m.compute(cost_matrix.copy())

        # validate the assignments
        valid_assignments = []
        raw_cost = 0
        valid_cost = 0
        for text_idx, cc_idx in assignments:
            # check if the assignment is valid ...
            if (text_idx < len(self.legend.text_labels) and cc_idx < n_ccs and
                cost_matrix[text_idx, cc_idx] < max_distance):
                # valid ... add to list of assignments ....
                valid_assignments.append((text_idx, cc_idx))
                # add to valid cost ....
                valid_cost += cost_matrix[text_idx, cc_idx]

            # add to raw cost
            raw_cost += cost_matrix[text_idx, cc_idx]

        return raw_cost, valid_cost, valid_assignments

    def get_intervals_IOU(self, int_1_min, int_1_max, int_2_min, int_2_max):
        if int_1_min <= int_2_max and int_2_min <= int_1_max:
            # intersection ...
            int_min = max(int_1_min, int_2_min)
            int_max = min(int_1_max, int_2_max)
            int_size = int_max - int_min
            # union ...
            union_min = min(int_1_min, int_2_min)
            union_max = max(int_1_max, int_2_max)
            union_size = union_max - union_min

            # print((int_1_min, int_1_max, int_2_min, int_2_max, int_min, int_max, int_size, union_size))

            return int_size / union_size
        else:
            # no intersection ...
            return 0.0

    def estimate_legend_boxes(self):
        legend_orientation = self.legend.get_legend_orientation()
        if legend_orientation == LegendInfo.OrientationMixed:
            # hard to auto-validate, do not create an estimation
            return

        otsu_t, binarized = cv2.threshold(self.base_gray_image[:, :, 0], 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        binarized = 255 - binarized

        # subtract the all text  ...
        for txt_label in self.panel_info.get_all_text():
            min_x, min_y, max_x, max_y = txt_label.get_axis_aligned_rectangle()

            binarized[int(min_y):int(max_y), int(min_x):int(max_x)] = 0

        # get the CC ...
        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binarized)
        # ... get CC stats ...
        cc_boxes = np.zeros((num_labels - 1, 4), dtype=np.int32)
        for cc_idx in range(1, num_labels):
            cc_min_x = stats[cc_idx, cv2.CC_STAT_LEFT]
            cc_max_x = cc_min_x + stats[cc_idx, cv2.CC_STAT_WIDTH]
            cc_min_y = stats[cc_idx, cv2.CC_STAT_TOP]
            cc_max_y = cc_min_y + stats[cc_idx, cv2.CC_STAT_HEIGHT]

            cc_boxes[cc_idx - 1] = cc_min_x, cc_max_x, cc_min_y, cc_max_y

        # find distances between each CC candidate and each legend text ...
        align_size = max(cc_boxes.shape[0], len(self.legend.text_labels))
        left_distances = np.zeros((align_size, align_size), dtype=np.int32)
        right_distances = np.zeros((align_size, align_size), dtype=np.int32)
        # max distance is the maximum width
        max_distance = labels.shape[1]

        left_distances[:, :] = max_distance
        right_distances[:, :] = max_distance
        # ... for each text label in the legend
        for text_idx, txt_label in enumerate(self.legend.text_labels):
            l_min_x, l_min_y, l_max_x, l_max_y = txt_label.get_axis_aligned_rectangle()

            # .... for each CC in the binarized image
            for cc_idx, (cc_min_x, cc_max_x, cc_min_y, cc_max_y) in enumerate(cc_boxes):
                # check area, confirm that this is not a large CC overlapping in range with the text region
                text_region = (l_max_x - l_min_x + 1) * (l_max_y - l_min_y + 1)
                cc_region = (cc_max_x - cc_min_x + 1) * (cc_max_y - cc_min_y + 1)
                if cc_region < text_region * 2:
                    # they should overlap vertically ...
                    if cc_min_y <= l_max_y and l_min_y <= cc_max_y:
                        # both legend text and the CC intersect vertically, check distance
                        if cc_min_x <= l_max_x and l_min_x <= cc_max_x:
                            # they also intersect horizontally
                            if cc_min_x < l_min_x:
                                # CC is the left-most
                                left_distances[text_idx, cc_idx] = 0
                            else:
                                # CC is the right-most
                                right_distances[text_idx, cc_idx] = 0
                        else:
                            # no horizontal intersection, find distance between edges
                            if cc_max_x < l_min_x:
                                # CC is on the left side of this legend element ...
                                left_distances[text_idx, cc_idx] = l_min_x - cc_max_x
                            else:
                                # CC is on the right side of this legend element ...
                                right_distances[text_idx, cc_idx] = cc_min_x - l_max_x
                    else:
                        # no vertical overlap, most legends should be connected to elements on the left or right
                        pass
                else:
                    # the CC is too big ... keep default of max distance
                    pass

        # running munkress on both sides ....
        n_ccs = cc_boxes.shape[0]
        l_raw_cost, l_valid_cost, l_assignments = self.get_best_assignments(left_distances, n_ccs, max_distance)
        r_raw_cost, r_valid_cost, r_assignments=  self.get_best_assignments(right_distances, n_ccs, max_distance)

        if l_raw_cost < r_raw_cost:
            # assume left alignment ...
            title = "try_left"
            assignments = l_assignments
        else:
            # use right alignment ..
            title = "try_right"
            assignments = r_assignments

        # cv2.imshow(title, binarized)

        # discard candidates if pairwise IOU is inconsistent ..
        min_overlap = 0.8
        for pair_idx, (text_idx_1, cc_idx_1) in enumerate(assignments[:-1]):
            cc_1_min_x, cc_1_max_x, cc_1_min_y, cc_1_max_y = cc_boxes[cc_idx_1]

            for text_idx_2, cc_idx_2 in assignments[pair_idx + 1:]:
                cc_2_min_x, cc_2_max_x, cc_2_min_y, cc_2_max_y = cc_boxes[cc_idx_2]

                if legend_orientation == LegendInfo.OrientationVertical:
                    # they should overlap on X ...
                    IOU = self.get_intervals_IOU(cc_1_min_x, cc_1_max_x, cc_2_min_x, cc_2_max_x)

                else:
                    # they should overlap on Y ...
                    IOU = self.get_intervals_IOU(cc_1_min_y, cc_1_max_y, cc_2_min_y, cc_2_max_y)

                # print((pair_idx, text_idx_1, text_idx_2, IOU))
                if IOU < min_overlap:
                    # do not create the default boxes
                    return

        # create the default bboxes for valid assignments ...
        for text_idx, cc_idx in assignments:
            text_id = self.legend.text_labels[text_idx].id

            # create default legend bbox using this element ...
            cc_min_x, cc_max_x, cc_min_y, cc_max_y = cc_boxes[cc_idx]
            points = np.array([[cc_min_x - 1, cc_min_y - 1],
                               [cc_max_x + 1, cc_min_y - 1],
                               [cc_max_x + 1, cc_max_y + 1],
                               [cc_min_x - 1, cc_max_y + 1]], dtype=np.float64)
            self.legend.marker_per_label[text_id] = points
            # print((text_idx, text_id, cc_idx, distances[text_idx, cc_idx]))

        # cv2.imshow("-", binarized)
        # cv2.waitKey()

    def create_controllers(self):
        # add elements....
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # Main Title
        self.label_title = ScreenLabel("title", "Chart Image Annotation Tool - Legend Markers Annotation", 28)
        self.label_title.background = self.general_background
        self.label_title.position = (int((self.width - self.label_title.width) / 2), 20)
        self.label_title.set_color(self.text_color)
        self.elements.append(self.label_title)

        container_top = 10 + self.label_title.get_bottom()
        container_width = 300

        button_width = 190
        button_left = (container_width - button_width) / 2

        button_2_width = 130
        button_2_left = int(container_width * 0.25) - button_2_width / 2
        button_2_right = int(container_width * 0.75) - button_2_width / 2

        # ===========================
        # View Options Panel

        # View panel with view control buttons
        self.create_image_annotator_controls(container_top, container_width, self.general_background, self.text_color,
                                             button_text_color, button_back_color)

        # ===========================
        # confirmation panel
        self.container_confirm_buttons = ScreenContainer("container_confirm_buttons", (container_width, 70),
                                                         back_color=self.general_background)
        self.container_confirm_buttons.position = (
            self.width - self.container_confirm_buttons.width - 10, self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_confirm_buttons)
        self.container_confirm_buttons.visible = False

        self.lbl_confirm_message = ScreenLabel("lbl_confirm_message", "Confirmation message goes here?", 21, 290, 1)
        self.lbl_confirm_message.position = (5, 5)
        self.lbl_confirm_message.set_background(self.general_background)
        self.lbl_confirm_message.set_color(self.text_color)
        self.container_confirm_buttons.append(self.lbl_confirm_message)

        self.btn_confirm_cancel = ScreenButton("btn_confirm_cancel", "Cancel", 21, 130)
        self.btn_confirm_cancel.set_colors(button_text_color, button_back_color)
        self.btn_confirm_cancel.position = (10, self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_cancel.click_callback = self.btn_confirm_cancel_click
        self.container_confirm_buttons.append(self.btn_confirm_cancel)

        self.btn_confirm_accept = ScreenButton("btn_confirm_accept", "Accept", 21, 130)
        self.btn_confirm_accept.set_colors(button_text_color, button_back_color)
        self.btn_confirm_accept.position = (self.container_confirm_buttons.width - self.btn_confirm_accept.width - 10,
                                            self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_accept.click_callback = self.btn_confirm_accept_click
        self.container_confirm_buttons.append(self.btn_confirm_accept)

        # ======================================
        # legend options
        darker_background = (55, 45, 100)
        self.container_legend_options = ScreenContainer("container_legend_options", (container_width, 550),
                                                        back_color=darker_background)
        self.container_legend_options.position = (self.container_view_buttons.get_left(),
                                                  self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_legend_options)

        self.lbl_legend_title = ScreenLabel("lbl_legend_title", "Legend Marker Annotation", 21, 290, 1)
        self.lbl_legend_title.position = (5, 5)
        self.lbl_legend_title.set_background(darker_background)
        self.lbl_legend_title.set_color(self.text_color)
        self.container_legend_options.append(self.lbl_legend_title)

        self.lbx_legend_list = ScreenTextlist("lbx_legend_list", (container_width - 20, 350), 18,
                                              back_color=(255,255,255), option_color=(0, 0, 0),
                                              selected_back=(120, 80, 50), selected_color=(255, 255, 255))
        self.lbx_legend_list.position = (10, self.lbl_legend_title.get_bottom() + 10)
        self.lbx_legend_list.selected_value_change_callback = self.lbx_legend_list_changed
        self.container_legend_options.append(self.lbx_legend_list)

        self.btn_legend_edit = ScreenButton("btn_legend_edit", "Edit", 21, button_2_width)
        self.btn_legend_edit.set_colors(button_text_color, button_back_color)
        self.btn_legend_edit.position = (button_2_left, self.lbx_legend_list.get_bottom() + 10)
        self.btn_legend_edit.click_callback = self.btn_legend_edit_click
        self.container_legend_options.append(self.btn_legend_edit)

        self.btn_legend_delete = ScreenButton("btn_legend_delete", "Delete", 21, button_2_width)
        self.btn_legend_delete.set_colors(button_text_color, button_back_color)
        self.btn_legend_delete.position = (button_2_right, self.lbx_legend_list.get_bottom() + 10)
        self.btn_legend_delete.click_callback = self.btn_legend_delete_click
        self.container_legend_options.append(self.btn_legend_delete)

        self.btn_return_accept = ScreenButton("btn_return_accept", "Accept", 21, button_2_width)
        return_top = self.container_legend_options.height - self.btn_return_accept.height - 10
        self.btn_return_accept.set_colors(button_text_color, button_back_color)
        self.btn_return_accept.position = (button_2_left, return_top)
        self.btn_return_accept.click_callback = self.btn_return_accept_click
        self.container_legend_options.append(self.btn_return_accept)

        self.btn_return_cancel = ScreenButton("btn_return_cancel", "Cancel", 21, button_2_width)
        self.btn_return_cancel.set_colors(button_text_color, button_back_color)
        self.btn_return_cancel.position = (button_2_right, return_top)
        self.btn_return_cancel.click_callback = self.btn_return_cancel_click
        self.container_legend_options.append(self.btn_return_cancel)

        # ======================================
        # legend entry edition options ....
        darker_background = (55, 45, 100)
        self.container_legend_edit  = ScreenContainer("container_legend_edit", (container_width, 150),
                                                        back_color=darker_background)
        self.container_legend_edit.position = (self.container_confirm_buttons.get_left(),
                                                  self.container_confirm_buttons.get_bottom() + 20)
        self.elements.append(self.container_legend_edit)
        self.container_legend_edit.visible = False

        self.btn_edit_force_box = ScreenButton("btn_edit_force_box", "Force Box", 21, button_width)
        self.btn_edit_force_box.set_colors(button_text_color, button_back_color)
        self.btn_edit_force_box.position = (button_left, 10)
        self.btn_edit_force_box.click_callback = self.btn_edit_force_box_click
        self.container_legend_edit.append(self.btn_edit_force_box)


        # ======================================
        # visuals
        # ===========================

        self.add_text_regions()

        self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)

    def get_color_display_info(self, text_id):
        main_color = self.canvas_display.colors[text_id % len(self.canvas_display.colors)]
        sel_color = self.canvas_display.sel_colors[text_id % len(self.canvas_display.sel_colors)]

        return main_color, sel_color

    def get_text_display_info(self, text):
        return "{0:d} - {1:s}".format(text.id, text.value)

    def add_text_regions(self):
        # populate the list-box using existing text regions (if any)
        for text in self.legend.text_labels:
            text_desc = self.get_text_display_info(text)
            main_color, sel_color = self.get_color_display_info(text.id)

            self.lbx_legend_list.add_option(str(text.id), text_desc)

            self.canvas_display.add_polygon_element(str(text.id), text.position_polygon.copy(), main_color, sel_color)

            if self.legend.marker_per_label[text.id] is not None:
                marker_pos = self.legend.marker_per_label[text.id].copy()
                self.canvas_display.add_polygon_element(str(text.id) + "-mark", marker_pos, main_color, sel_color)

    def custom_view_update(self, modified_image):
        pass

    def set_editor_mode(self, new_mode):
        self.edition_mode = new_mode

        # Navigation mode ...
        self.container_legend_options.visible = (self.edition_mode == ChartLegendAnnotator.ModeNavigate)

        # Confirm panel and buttons  ...
        self.container_confirm_buttons.visible = self.edition_mode in [ChartLegendAnnotator.ModeRectangleSelect,
                                                                       ChartLegendAnnotator.ModeRectangleEdit,
                                                                       ChartLegendAnnotator.ModeConfirmExit]

        self.container_legend_edit.visible = (self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit)

        if self.edition_mode == ChartLegendAnnotator.ModeRectangleSelect:
            self.lbl_confirm_message.set_text("Select Legend Mark Location")
        elif self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
            self.lbl_confirm_message.set_text("Editing Legend Mark Location")
        elif self.edition_mode == ChartLegendAnnotator.ModeConfirmExit:
            self.lbl_confirm_message.set_text("Discard Changes to Legend?")

        # Do not show accept at these steps (they can be implicitly accepted, but need explicit cancel button only)
        self.btn_confirm_accept.visible = self.edition_mode != ChartLegendAnnotator.ModeRectangleSelect

        if new_mode in [ChartLegendAnnotator.ModeRectangleEdit]:
            # show polygon
            self.canvas_select.locked = False
            self.canvas_select.elements["selection_polygon"].visible = True
        else:
            # for every other mode
            self.canvas_select.locked = True
            self.canvas_select.elements["selection_polygon"].visible = False

    def btn_confirm_cancel_click(self, button):
        if self.edition_mode in [ChartLegendAnnotator.ModeRectangleEdit,
                                 ChartLegendAnnotator.ModeRectangleSelect,
                                 ChartLegendAnnotator.ModeConfirmExit]:

            if self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
                polygon_name = str(self.tempo_text_id) + "-mark"

                if polygon_name in self.canvas_display.elements:
                    self.canvas_display.elements[polygon_name].visible = True

            # return to navigation
            self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)
        else:
            print(self.edition_mode)
            raise Exception("Not Implemented")

    def btn_confirm_accept_click(self, button):
        if self.edition_mode == ChartLegendAnnotator.ModeConfirmExit:
            print("-> Changes made to Legend Annotations were lost")
            self.return_screen = self.parent_screen
        elif self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
            # get polygon from GUI
            raw_polygon = self.canvas_select.elements["selection_polygon"].points.copy()
            mark_polygon = raw_polygon / self.view_scale
            # update on GUI ...
            polygon_name = str(self.tempo_text_id) + "-mark"
            if self.legend.marker_per_label[self.tempo_text_id] is None:
                # add to the display canvas ...
                main_color, sel_color = self.get_color_display_info(self.tempo_text_id)
                self.canvas_display.add_polygon_element(polygon_name, raw_polygon, main_color, sel_color)
            else:
                # update the display canvas ...
                self.canvas_display.update_polygon_element(polygon_name, raw_polygon, True)

            # update on DATA
            self.legend.marker_per_label[self.tempo_text_id] = mark_polygon.copy()
            self.data_changed = True

            # return ...
            self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)
        else:
            raise Exception("Not Implemented")

    def get_next_axis_aligned_box(self, click_x, click_y):

        last_known_polygon = None
        for text_id in self.legend.marker_per_label:
            if self.legend.marker_per_label[text_id] is not None:
                last_known_polygon = self.legend.marker_per_label[text_id]
                break

        if last_known_polygon is None:
            # default small rectangle
            rect_w, rect_h = 40, 20
        else:
            # axis aligned container rectangle from last bbox
            min_x = last_known_polygon[:, 0].min()
            max_x = last_known_polygon[:, 0].max()
            min_y = last_known_polygon[:, 1].min()
            max_y = last_known_polygon[:, 1].max()

            rect_w = (max_x - min_x) * self.view_scale
            rect_h = (max_y - min_y) * self.view_scale

        points = np.array([[click_x, click_y], [click_x + rect_w, click_y],
                           [click_x + rect_w, click_y + rect_h], [click_x, click_y + rect_h]])

        return points

    def img_main_mouse_button_down(self, img_object, pos, button):
        if button == 1:
            if self.edition_mode == ChartLegendAnnotator.ModeRectangleSelect:
                click_x, click_y = pos
                points = self.get_next_axis_aligned_box(click_x, click_y)
                self.canvas_select.elements["selection_polygon"].update(points)

                self.set_editor_mode(ChartLegendAnnotator.ModeRectangleEdit)

    def lbx_legend_list_changed(self, new_value, old_value):
        self.canvas_display.change_selected_element(new_value)

    def btn_legend_edit_click(self, button):
        if self.lbx_legend_list.selected_option_value is None:
            print("Must select a legend label from the list")
            return

        # check if new or edit
        self.tempo_text_id = int(self.lbx_legend_list.selected_option_value)
        if self.legend.marker_per_label[self.tempo_text_id] is None:
            # new ...
            self.set_editor_mode(ChartLegendAnnotator.ModeRectangleSelect)
        else:
            # edit existing ...
            # ... copy points to selection canvas ...
            polygon = self.legend.marker_per_label[self.tempo_text_id].copy() * self.view_scale
            self.canvas_select.update_polygon_element("selection_polygon", polygon, True)

            self.canvas_display.elements[str(self.tempo_text_id) + "-mark"].visible = False

            self.set_editor_mode(ChartLegendAnnotator.ModeRectangleEdit)

    def btn_legend_delete_click(self, button):
        if self.lbx_legend_list.selected_option_value is None:
            print("Must select a legend label from the list")
            return

        self.tempo_text_id = int(self.lbx_legend_list.selected_option_value)

        # delete from GUI ...
        if self.legend.marker_per_label[self.tempo_text_id] is not None:
            polygon_name = str(self.tempo_text_id) + "-mark"
            self.canvas_display.remove_element(polygon_name)

        # delete from Data ...
        self.legend.marker_per_label[self.tempo_text_id] = None

        self.data_changed = True

    def btn_return_accept_click(self, button):
        if self.data_changed:
            # overwrite existing legend data ...
            self.panel_info.legend = LegendInfo.Copy(self.legend)
            self.parent_screen.subtool_completed(True)

        # return
        self.return_screen = self.parent_screen

    def btn_return_cancel_click(self, button):
        if self.data_changed:
            self.set_editor_mode(ChartLegendAnnotator.ModeConfirmExit)
        else:
            # simply return
            self.return_screen = self.parent_screen

    def btn_edit_force_box_click(self, button):
        legend_entry_polygon = self.canvas_select.elements["selection_polygon"].points / self.view_scale

        poly = Polygon(legend_entry_polygon)
        minx, miny, maxx, maxy = poly.bounds

        legend_entry_polygon = np.array([[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]])
        self.canvas_select.elements["selection_polygon"].update(legend_entry_polygon * self.view_scale)

    def img_main_mouse_double_click(self, element, pos, button):
        if button == 1:
            # double left click ...
            if self.edition_mode == ChartLegendAnnotator.ModeNavigate:
                click_x, click_y = pos

                # scale the view ....
                click_x /= self.view_scale
                click_y /= self.view_scale

                # find if a given element was clicked ....
                for text_region in self.legend.text_labels:
                    # check ...
                    if text_region.area_contains_point(click_x, click_y):
                        # clicked on a text region ...
                        self.lbx_legend_list.change_option_selected(str(text_region.id))
                        # simulate click on edit button ....
                        self.btn_legend_edit_click(None)
                        break
class ChartLegendAnnotator(Screen):
    ModeNavigate = 0
    ModeRectangleSelect = 1
    ModeRectangleEdit = 2
    ModeConfirmExit = 3

    ViewModeRawData = 0
    ViewModeGrayData = 1
    ViewModeRawNoData = 2
    ViewModeGrayNoData = 3

    def __init__(self, size, panel_image, panel_info, parent_screen):
        Screen.__init__(self, "Chart Legend Ground Truth Annotation Interface",
                        size)

        self.panel_image = panel_image
        self.panel_gray = np.zeros(self.panel_image.shape,
                                   self.panel_image.dtype)
        self.panel_gray[:, :, 0] = cv2.cvtColor(self.panel_image,
                                                cv2.COLOR_RGB2GRAY)
        self.panel_gray[:, :, 1] = self.panel_gray[:, :, 0].copy()
        self.panel_gray[:, :, 2] = self.panel_gray[:, :, 0].copy()

        self.panel_info = panel_info

        if self.panel_info.legend is None:
            # create a new legend info
            self.legend = LegendInfo(
                self.panel_info.get_all_text(TextInfo.TypeLegendLabel))
            self.data_changed = True
        else:
            # make a copy
            self.legend = LegendInfo.Copy(self.panel_info.legend)
            self.data_changed = False

        self.parent_screen = parent_screen

        self.general_background = (80, 70, 150)
        self.text_color = (255, 255, 255)

        self.elements.back_color = self.general_background

        self.edition_mode = None
        self.tempo_text_id = None
        self.view_mode = ChartLegendAnnotator.ViewModeRawData
        self.view_scale = 1.0

        self.label_title = None

        self.container_view_buttons = None
        self.lbl_zoom = None
        self.btn_zoom_reduce = None
        self.btn_zoom_increase = None
        self.btn_zoom_zero = None

        self.btn_view_raw_data = None
        self.btn_view_gray_data = None
        self.btn_view_raw_clear = None
        self.btn_view_gray_clear = None

        self.container_confirm_buttons = None
        self.lbl_confirm_message = None
        self.btn_confirm_cancel = None
        self.btn_confirm_accept = None

        self.container_legend_options = None
        self.lbl_legend_title = None
        self.lbx_legend_list = None
        self.btn_legend_edit = None
        self.btn_legend_delete = None
        self.btn_return_accept = None
        self.btn_return_cancel = None

        self.container_images = None
        self.canvas_select = None
        self.canvas_display = None
        self.img_main = None

        self.create_controllers()

        # get the view ...
        self.update_current_view(True)

    def create_controllers(self):
        # add elements....
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # Main Title
        self.label_title = ScreenLabel(
            "title", "Chart Image Annotation Tool - Legend Markers Annotation",
            28)
        self.label_title.background = self.general_background
        self.label_title.position = (int(
            (self.width - self.label_title.width) / 2), 20)
        self.label_title.set_color(self.text_color)
        self.elements.append(self.label_title)

        container_top = 10 + self.label_title.get_bottom()
        container_width = 300

        button_width = 190
        button_left = (container_width - button_width) / 2

        button_2_width = 130
        button_2_left = int(container_width * 0.25) - button_2_width / 2
        button_2_right = int(container_width * 0.75) - button_2_width / 2

        # ===========================
        # View Options Panel

        # View panel with view control buttons
        self.container_view_buttons = ScreenContainer(
            "container_view_buttons", (container_width, 160),
            back_color=self.general_background)
        self.container_view_buttons.position = (
            self.width - self.container_view_buttons.width - 10, container_top)
        self.elements.append(self.container_view_buttons)

        # zoom ....
        self.lbl_zoom = ScreenLabel("lbl_zoom", "Zoom: 100%", 21, 290, 1)
        self.lbl_zoom.position = (5, 5)
        self.lbl_zoom.set_background(self.general_background)
        self.lbl_zoom.set_color(self.text_color)
        self.container_view_buttons.append(self.lbl_zoom)

        self.btn_zoom_reduce = ScreenButton("btn_zoom_reduce", "[ - ]", 21, 90)
        self.btn_zoom_reduce.set_colors(button_text_color, button_back_color)
        self.btn_zoom_reduce.position = (10, self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_reduce.click_callback = self.btn_zoom_reduce_click
        self.container_view_buttons.append(self.btn_zoom_reduce)

        self.btn_zoom_increase = ScreenButton("btn_zoom_increase", "[ + ]", 21,
                                              90)
        self.btn_zoom_increase.set_colors(button_text_color, button_back_color)
        self.btn_zoom_increase.position = (self.container_view_buttons.width -
                                           self.btn_zoom_increase.width - 10,
                                           self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_increase.click_callback = self.btn_zoom_increase_click
        self.container_view_buttons.append(self.btn_zoom_increase)

        self.btn_zoom_zero = ScreenButton("btn_zoom_zero", "100%", 21, 90)
        self.btn_zoom_zero.set_colors(button_text_color, button_back_color)
        self.btn_zoom_zero.position = (
            (self.container_view_buttons.width - self.btn_zoom_zero.width) / 2,
            self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_zero.click_callback = self.btn_zoom_zero_click
        self.container_view_buttons.append(self.btn_zoom_zero)

        self.btn_view_raw_data = ScreenButton("btn_view_raw_data", "RGB Data",
                                              21, button_2_width)
        self.btn_view_raw_data.set_colors(button_text_color, button_back_color)
        self.btn_view_raw_data.position = (button_2_left,
                                           self.btn_zoom_zero.get_bottom() +
                                           10)
        self.btn_view_raw_data.click_callback = self.btn_view_raw_data_click
        self.container_view_buttons.append(self.btn_view_raw_data)

        self.btn_view_gray_data = ScreenButton("btn_view_gray", "Gray Data",
                                               21, button_2_width)
        self.btn_view_gray_data.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_gray_data.position = (button_2_right,
                                            self.btn_zoom_zero.get_bottom() +
                                            10)
        self.btn_view_gray_data.click_callback = self.btn_view_gray_data_click
        self.container_view_buttons.append(self.btn_view_gray_data)

        self.btn_view_raw_clear = ScreenButton("btn_view_raw_clear",
                                               "RGB Clear", 21, button_2_width)
        self.btn_view_raw_clear.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_raw_clear.position = (
            button_2_left, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_raw_clear.click_callback = self.btn_view_raw_clear_click
        self.container_view_buttons.append(self.btn_view_raw_clear)

        self.btn_view_gray_clear = ScreenButton("btn_view_gray_clear",
                                                "Gray Clear", 21,
                                                button_2_width)
        self.btn_view_gray_clear.set_colors(button_text_color,
                                            button_back_color)
        self.btn_view_gray_clear.position = (
            button_2_right, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_gray_clear.click_callback = self.btn_view_gray_clear_click
        self.container_view_buttons.append(self.btn_view_gray_clear)

        # ===========================
        # confirmation panel
        self.container_confirm_buttons = ScreenContainer(
            "container_confirm_buttons", (container_width, 70),
            back_color=self.general_background)
        self.container_confirm_buttons.position = (
            self.width - self.container_confirm_buttons.width - 10,
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_confirm_buttons)
        self.container_confirm_buttons.visible = False

        self.lbl_confirm_message = ScreenLabel(
            "lbl_confirm_message", "Confirmation message goes here?", 21, 290,
            1)
        self.lbl_confirm_message.position = (5, 5)
        self.lbl_confirm_message.set_background(self.general_background)
        self.lbl_confirm_message.set_color(self.text_color)
        self.container_confirm_buttons.append(self.lbl_confirm_message)

        self.btn_confirm_cancel = ScreenButton("btn_confirm_cancel", "Cancel",
                                               21, 130)
        self.btn_confirm_cancel.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_cancel.position = (
            10, self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_cancel.click_callback = self.btn_confirm_cancel_click
        self.container_confirm_buttons.append(self.btn_confirm_cancel)

        self.btn_confirm_accept = ScreenButton("btn_confirm_accept", "Accept",
                                               21, 130)
        self.btn_confirm_accept.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_accept.position = (
            self.container_confirm_buttons.width -
            self.btn_confirm_accept.width - 10,
            self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_accept.click_callback = self.btn_confirm_accept_click
        self.container_confirm_buttons.append(self.btn_confirm_accept)

        # ======================================
        # legend options
        darker_background = (55, 45, 100)
        self.container_legend_options = ScreenContainer(
            "container_legend_options", (container_width, 550),
            back_color=darker_background)
        self.container_legend_options.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_legend_options)

        self.lbl_legend_title = ScreenLabel("lbl_legend_title",
                                            "Legend Marker Annotation", 21,
                                            290, 1)
        self.lbl_legend_title.position = (5, 5)
        self.lbl_legend_title.set_background(darker_background)
        self.lbl_legend_title.set_color(self.text_color)
        self.container_legend_options.append(self.lbl_legend_title)

        self.lbx_legend_list = ScreenTextlist("lbx_legend_list",
                                              (container_width - 20, 350),
                                              18,
                                              back_color=(255, 255, 255),
                                              option_color=(0, 0, 0),
                                              selected_back=(120, 80, 50),
                                              selected_color=(255, 255, 255))
        self.lbx_legend_list.position = (10,
                                         self.lbl_legend_title.get_bottom() +
                                         10)
        self.lbx_legend_list.selected_value_change_callback = self.lbx_legend_list_changed
        self.container_legend_options.append(self.lbx_legend_list)

        self.btn_legend_edit = ScreenButton("btn_legend_edit", "Edit", 21,
                                            button_2_width)
        self.btn_legend_edit.set_colors(button_text_color, button_back_color)
        self.btn_legend_edit.position = (button_2_left,
                                         self.lbx_legend_list.get_bottom() +
                                         10)
        self.btn_legend_edit.click_callback = self.btn_legend_edit_click
        self.container_legend_options.append(self.btn_legend_edit)

        self.btn_legend_delete = ScreenButton("btn_legend_delete", "Delete",
                                              21, button_2_width)
        self.btn_legend_delete.set_colors(button_text_color, button_back_color)
        self.btn_legend_delete.position = (button_2_right,
                                           self.lbx_legend_list.get_bottom() +
                                           10)
        self.btn_legend_delete.click_callback = self.btn_legend_delete_click
        self.container_legend_options.append(self.btn_legend_delete)

        self.btn_return_accept = ScreenButton("btn_return_accept", "Accept",
                                              21, button_2_width)
        return_top = self.container_legend_options.height - self.btn_return_accept.height - 10
        self.btn_return_accept.set_colors(button_text_color, button_back_color)
        self.btn_return_accept.position = (button_2_left, return_top)
        self.btn_return_accept.click_callback = self.btn_return_accept_click
        self.container_legend_options.append(self.btn_return_accept)

        self.btn_return_cancel = ScreenButton("btn_return_cancel", "Cancel",
                                              21, button_2_width)
        self.btn_return_cancel.set_colors(button_text_color, button_back_color)
        self.btn_return_cancel.position = (button_2_right, return_top)
        self.btn_return_cancel.click_callback = self.btn_return_cancel_click
        self.container_legend_options.append(self.btn_return_cancel)

        # ======================================
        # visuals
        # ===========================
        # Image

        image_width = self.width - self.container_view_buttons.width - 30
        image_height = self.height - container_top - 10
        self.container_images = ScreenContainer("container_images",
                                                (image_width, image_height),
                                                back_color=(0, 0, 0))
        self.container_images.position = (10, container_top)
        self.elements.append(self.container_images)

        # ... image objects ...
        tempo_blank = np.zeros((50, 50, 3), np.uint8)
        tempo_blank[:, :, :] = 255
        self.img_main = ScreenImage("img_main", tempo_blank, 0, 0, True,
                                    cv2.INTER_NEAREST)
        self.img_main.position = (0, 0)
        self.img_main.mouse_button_down_callback = self.img_mouse_down
        self.container_images.append(self.img_main)

        self.canvas_select = ScreenCanvas("canvas_select", 100, 100)
        self.canvas_select.position = (0, 0)
        self.canvas_select.locked = True
        # self.canvas_select.object_edited_callback = self.canvas_object_edited
        # self.canvas_select.object_selected_callback = self.canvas_selection_changed
        self.container_images.append(self.canvas_select)

        base_points = np.array([[10, 10], [20, 10], [20, 20], [10, 20]],
                               dtype=np.float64)
        self.canvas_select.add_polygon_element("selection_polygon",
                                               base_points)
        self.canvas_select.elements["selection_polygon"].visible = False

        self.canvas_display = ScreenCanvas("canvas_display", 100, 100)
        self.canvas_display.position = (0, 0)
        self.canvas_display.locked = True
        self.container_images.append(self.canvas_display)

        self.add_text_regions()

        self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)

    def get_color_display_info(self, text_id):
        main_color = self.canvas_display.colors[text_id % len(
            self.canvas_display.colors)]
        sel_color = self.canvas_display.sel_colors[text_id % len(
            self.canvas_display.sel_colors)]

        return main_color, sel_color

    def get_text_display_info(self, text):
        return "{0:d} - {1:s}".format(text.id, text.value)

    def add_text_regions(self):
        # populate the list-box using existing text regions (if any)
        for text in self.legend.text_labels:
            text_desc = self.get_text_display_info(text)
            main_color, sel_color = self.get_color_display_info(text.id)

            self.lbx_legend_list.add_option(str(text.id), text_desc)

            self.canvas_display.add_polygon_element(
                str(text.id), text.position_polygon.copy(), main_color,
                sel_color)

            if self.legend.marker_per_label[text.id] is not None:
                marker_pos = self.legend.marker_per_label[text.id].copy()
                self.canvas_display.add_polygon_element(
                    str(text.id) + "-mark", marker_pos, main_color, sel_color)

    def btn_zoom_reduce_click(self, button):
        self.update_view_scale(self.view_scale - 0.25)

    def btn_zoom_increase_click(self, button):
        self.update_view_scale(self.view_scale + 0.25)

    def btn_zoom_zero_click(self, button):
        self.update_view_scale(1.0)

    def btn_view_raw_data_click(self, button):
        self.view_mode = ChartLegendAnnotator.ViewModeRawData
        self.update_current_view()

    def btn_view_gray_data_click(self, button):
        self.view_mode = ChartLegendAnnotator.ViewModeGrayData
        self.update_current_view()

    def btn_view_raw_clear_click(self, button):
        self.view_mode = ChartLegendAnnotator.ViewModeRawNoData
        self.update_current_view()

    def btn_view_gray_clear_click(self, button):
        self.view_mode = ChartLegendAnnotator.ViewModeGrayNoData
        self.update_current_view()

    def update_current_view(self, resized=False):
        if self.view_mode in [
                ChartLegendAnnotator.ViewModeGrayData,
                ChartLegendAnnotator.ViewModeGrayNoData
        ]:
            # gray scale mode
            base_image = self.panel_gray
        else:
            base_image = self.panel_image

        h, w, c = base_image.shape

        modified_image = base_image.copy()

        if self.view_mode in [
                ChartLegendAnnotator.ViewModeRawData,
                ChartLegendAnnotator.ViewModeGrayData
        ]:
            # TODO: show here any relevant annotations on the modified image ...
            # (for example, draw the polygons)
            self.canvas_display.visible = True
        else:
            self.canvas_display.visible = False

        # finally, resize ...
        modified_image = cv2.resize(
            modified_image,
            (int(w * self.view_scale), int(h * self.view_scale)),
            interpolation=cv2.INTER_NEAREST)

        # update canvas size ....
        self.canvas_select.height, self.canvas_select.width, _ = modified_image.shape
        self.canvas_display.height, self.canvas_display.width, _ = modified_image.shape

        # replace/update image
        self.img_main.set_image(modified_image, 0, 0, True, cv2.INTER_NEAREST)
        if resized:
            self.container_images.recalculate_size()

    def update_view_scale(self, new_scale):
        prev_scale = self.view_scale

        if 0.25 <= new_scale <= 4.0:
            self.view_scale = new_scale
        else:
            return

        # keep previous offsets ...
        scroll_offset_y = self.container_images.v_scroll.value if self.container_images.v_scroll.active else 0
        scroll_offset_x = self.container_images.h_scroll.value if self.container_images.h_scroll.active else 0

        prev_center_y = scroll_offset_y + self.container_images.height / 2
        prev_center_x = scroll_offset_x + self.container_images.width / 2

        # compute new scroll bar offsets
        scale_factor = (new_scale / prev_scale)
        new_off_y = prev_center_y * scale_factor - self.container_images.height / 2
        new_off_x = prev_center_x * scale_factor - self.container_images.width / 2

        # update view ....
        self.update_current_view(True)

        # set offsets
        if self.container_images.v_scroll.active and 0 <= new_off_y <= self.container_images.v_scroll.max:
            self.container_images.v_scroll.value = new_off_y
        if self.container_images.h_scroll.active and 0 <= new_off_x <= self.container_images.h_scroll.max:
            self.container_images.h_scroll.value = new_off_x

        # re-scale objects from both canvas
        # ... selection ...
        selection_polygon = self.canvas_select.elements["selection_polygon"]
        selection_polygon.points *= scale_factor
        # ... display ...
        for polygon_name in self.canvas_display.elements:
            display_polygon = self.canvas_display.elements[polygon_name]
            display_polygon.points *= scale_factor

        # update scale text ...
        self.lbl_zoom.set_text("Zoom: " +
                               str(int(round(self.view_scale * 100, 0))) + "%")

    def set_editor_mode(self, new_mode):
        self.edition_mode = new_mode

        # Navigation mode ...
        self.container_legend_options.visible = (
            self.edition_mode == ChartLegendAnnotator.ModeNavigate)

        # Confirm panel and buttons  ...
        self.container_confirm_buttons.visible = self.edition_mode in [
            ChartLegendAnnotator.ModeRectangleSelect,
            ChartLegendAnnotator.ModeRectangleEdit,
            ChartLegendAnnotator.ModeConfirmExit
        ]

        if self.edition_mode == ChartLegendAnnotator.ModeRectangleSelect:
            self.lbl_confirm_message.set_text("Select Legend Mark Location")
        elif self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
            self.lbl_confirm_message.set_text("Editing Legend Mark Location")
        elif self.edition_mode == ChartLegendAnnotator.ModeConfirmExit:
            self.lbl_confirm_message.set_text("Discard Changes to Legend?")

        # Do not show accept at these steps (they can be implicitly accepted, but need explicit cancel button only)
        self.btn_confirm_accept.visible = self.edition_mode != ChartLegendAnnotator.ModeRectangleSelect

        if new_mode in [ChartLegendAnnotator.ModeRectangleEdit]:
            # show polygon
            self.canvas_select.locked = False
            self.canvas_select.elements["selection_polygon"].visible = True
        else:
            # for every other mode
            self.canvas_select.locked = True
            self.canvas_select.elements["selection_polygon"].visible = False

    def btn_confirm_cancel_click(self, button):
        if self.edition_mode in [
                ChartLegendAnnotator.ModeRectangleEdit,
                ChartLegendAnnotator.ModeRectangleSelect,
                ChartLegendAnnotator.ModeConfirmExit
        ]:

            if self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
                polygon_name = str(self.tempo_text_id) + "-mark"

                if polygon_name in self.canvas_display.elements:
                    self.canvas_display.elements[polygon_name].visible = True

            # return to navigation
            self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)
        else:
            print(self.edition_mode)
            raise Exception("Not Implemented")

    def btn_confirm_accept_click(self, button):
        if self.edition_mode == ChartLegendAnnotator.ModeConfirmExit:
            print("-> Changes made to Legend Annotations were lost")
            self.return_screen = self.parent_screen
        elif self.edition_mode == ChartLegendAnnotator.ModeRectangleEdit:
            # get polygon from GUI
            raw_polygon = self.canvas_select.elements[
                "selection_polygon"].points.copy()
            mark_polygon = raw_polygon / self.view_scale
            # update on GUI ...
            polygon_name = str(self.tempo_text_id) + "-mark"
            if self.legend.marker_per_label[self.tempo_text_id] is None:
                # add to the display canvas ...
                main_color, sel_color = self.get_color_display_info(
                    self.tempo_text_id)
                self.canvas_display.add_polygon_element(
                    polygon_name, raw_polygon, main_color, sel_color)
            else:
                # update the display canvas ...
                self.canvas_display.update_polygon_element(
                    polygon_name, raw_polygon, True)

            # update on DATA
            self.legend.marker_per_label[
                self.tempo_text_id] = mark_polygon.copy()
            self.data_changed = True

            # return ...
            self.set_editor_mode(ChartLegendAnnotator.ModeNavigate)
        else:
            raise Exception("Not Implemented")

    def get_next_axis_aligned_box(self, click_x, click_y):

        last_known_polygon = None
        for text_id in self.legend.marker_per_label:
            if self.legend.marker_per_label[text_id] is not None:
                last_known_polygon = self.legend.marker_per_label[text_id]
                break

        if last_known_polygon is None:
            # default small rectangle
            rect_w, rect_h = 40, 20
        else:
            # axis aligned container rectangle from last bbox
            min_x = last_known_polygon[:, 0].min()
            max_x = last_known_polygon[:, 0].max()
            min_y = last_known_polygon[:, 1].min()
            max_y = last_known_polygon[:, 1].max()

            rect_w = (max_x - min_x) * self.view_scale
            rect_h = (max_y - min_y) * self.view_scale

        points = np.array([[click_x, click_y], [click_x + rect_w, click_y],
                           [click_x + rect_w, click_y + rect_h],
                           [click_x, click_y + rect_h]])

        return points

    def img_mouse_down(self, img_object, pos, button):
        if button == 1:
            if self.edition_mode == ChartLegendAnnotator.ModeRectangleSelect:
                click_x, click_y = pos
                points = self.get_next_axis_aligned_box(click_x, click_y)
                self.canvas_select.elements["selection_polygon"].update(points)

                self.set_editor_mode(ChartLegendAnnotator.ModeRectangleEdit)

    def lbx_legend_list_changed(self, new_value, old_value):
        self.canvas_display.change_selected_element(new_value)

    def btn_legend_edit_click(self, button):
        if self.lbx_legend_list.selected_option_value is None:
            print("Must select a legend label from the list")
            return

        # check if new or edit
        self.tempo_text_id = int(self.lbx_legend_list.selected_option_value)
        if self.legend.marker_per_label[self.tempo_text_id] is None:
            # new ...
            self.set_editor_mode(ChartLegendAnnotator.ModeRectangleSelect)
        else:
            # edit existing ...
            # ... copy points to selection canvas ...
            polygon = self.legend.marker_per_label[
                self.tempo_text_id].copy() * self.view_scale
            self.canvas_select.update_polygon_element("selection_polygon",
                                                      polygon, True)

            self.canvas_display.elements[str(self.tempo_text_id) +
                                         "-mark"].visible = False

            self.set_editor_mode(ChartLegendAnnotator.ModeRectangleEdit)

    def btn_legend_delete_click(self, button):
        if self.lbx_legend_list.selected_option_value is None:
            print("Must select a legend label from the list")
            return

        self.tempo_text_id = int(self.lbx_legend_list.selected_option_value)

        # delete from GUI ...
        if self.legend.marker_per_label[self.tempo_text_id] is not None:
            polygon_name = str(self.tempo_text_id) + "-mark"
            self.canvas_display.remove_element(polygon_name)

        # delete from Data ...
        self.legend.marker_per_label[self.tempo_text_id] = None

        self.data_changed = True

    def btn_return_accept_click(self, button):
        if self.data_changed:
            # overwrite existing legend data ...
            self.panel_info.legend = LegendInfo.Copy(self.legend)
            self.parent_screen.subtool_completed(True)

        # return
        self.return_screen = self.parent_screen

    def btn_return_cancel_click(self, button):
        if self.data_changed:
            self.set_editor_mode(ChartLegendAnnotator.ModeConfirmExit)
        else:
            # simply return
            self.return_screen = self.parent_screen
Example #6
0
class ChartTextAnnotator(Screen):
    ModeNavigate = 0
    ModeAddingTextSelect = 1
    ModeAddingTextEdit = 2
    ModeEditingText = 3
    ModeConfirmDeleteText = 4
    ModeConfirmExit = 5
    ModeConfirmOverwrite = 6

    ViewModeRawData = 0
    ViewModeGrayData = 1
    ViewModeRawNoData = 2
    ViewModeGrayNoData = 3

    RotationAuto = 0
    Rotation0 = 1
    Rotation90 = 2
    Rotation180 = 3
    Rotation270 = 4

    def __init__(self, size, panel_image, panel_info, parent_screen,
                 admin_mode):
        Screen.__init__(self, "Chart Text Ground Truth Annotation Interface",
                        size)

        self.panel_image = panel_image
        self.panel_gray = np.zeros(self.panel_image.shape,
                                   self.panel_image.dtype)
        self.panel_gray[:, :, 0] = cv2.cvtColor(self.panel_image,
                                                cv2.COLOR_RGB2GRAY)
        self.panel_gray[:, :, 1] = self.panel_gray[:, :, 0].copy()
        self.panel_gray[:, :, 2] = self.panel_gray[:, :, 0].copy()

        self.panel_info = panel_info

        # working copy ...
        self.text_regions = [
            TextInfo.Copy(text_info) for text_info in self.panel_info.text
        ]

        self.parent_screen = parent_screen
        self.admin_mode = admin_mode

        self.general_background = (150, 100, 60)
        self.text_color = (255, 255, 255)
        self.canvas_colors = {
            TextInfo.TypeChartTitle: (255, 0, 0),
            TextInfo.TypeAxisTitle: (0, 255, 0),
            TextInfo.TypeTickLabel: (0, 0, 255),
            TextInfo.TypeLegendTitle: (255, 255, 0),
            TextInfo.TypeLegendLabel: (255, 0, 255),
            TextInfo.TypeValueLabel: (0, 255, 255),
            TextInfo.TypeOther: (128, 0, 0),
        }
        self.canvas_sel_colors = {
            TextInfo.TypeChartTitle: (128, 0, 0),
            TextInfo.TypeAxisTitle: (0, 128, 0),
            TextInfo.TypeTickLabel: (0, 0, 128),
            TextInfo.TypeLegendTitle: (128, 128, 0),
            TextInfo.TypeLegendLabel: (128, 0, 128),
            TextInfo.TypeValueLabel: (0, 128, 128),
            TextInfo.TypeOther: (64, 0, 0),
        }

        self.elements.back_color = self.general_background

        self.edition_mode = None
        self.view_mode = ChartTextAnnotator.ViewModeRawData
        self.view_scale = 1.0

        self.data_changed = False
        self.tempo_edit_text = None

        self.label_title = None

        self.container_view_buttons = None
        self.lbl_zoom = None
        self.btn_zoom_reduce = None
        self.btn_zoom_increase = None
        self.btn_zoom_zero = None

        self.btn_view_raw_data = None
        self.btn_view_gray_data = None
        self.btn_view_raw_clear = None
        self.btn_view_gray_clear = None

        self.container_confirm_buttons = None
        self.lbl_confirm_message = None
        self.btn_confirm_cancel = None
        self.btn_confirm_accept = None

        self.container_text_options = None
        self.lbl_text_title = None
        self.lbx_text_list = None
        self.btn_text_add = None
        self.btn_text_edit = None
        self.btn_text_delete = None
        self.btn_return_accept = None
        self.btn_return_cancel = None

        self.container_edit_options = None
        self.lbl_edit_title = None
        self.lbx_edit_type = None
        self.lbl_edit_text = None
        self.btn_edit_OCR_0 = None
        self.btn_edit_OCR_180 = None
        self.btn_edit_OCR_any = None
        self.btn_edit_OCR_270 = None
        self.btn_edit_OCR_90 = None
        self.txt_edit_text = None
        self.btn_edit_cancel = None
        self.btn_edit_accept = None

        self.container_images = None
        self.canvas_select = None
        self.canvas_display = None
        self.img_main = None

        self.create_controllers()

        # get the view ...
        self.update_current_view(True)

    def create_controllers(self):
        # add elements....
        button_text_color = (35, 50, 20)
        button_back_color = (228, 228, 228)

        # Main Title
        self.label_title = ScreenLabel(
            "title", "Chart Image Annotation Tool - Panel Text Annotation", 28)
        self.label_title.background = self.general_background
        self.label_title.position = (int(
            (self.width - self.label_title.width) / 2), 20)
        self.label_title.set_color(self.text_color)
        self.elements.append(self.label_title)

        container_top = 10 + self.label_title.get_bottom()
        container_width = 300

        button_width = 190
        button_left = (container_width - button_width) / 2

        button_2_width = 130
        button_2_left = int(container_width * 0.25) - button_2_width / 2
        button_2_right = int(container_width * 0.75) - button_2_width / 2

        # ===========================
        # View Options Panel

        # View panel with view control buttons
        self.container_view_buttons = ScreenContainer(
            "container_view_buttons", (container_width, 160),
            back_color=self.general_background)
        self.container_view_buttons.position = (
            self.width - self.container_view_buttons.width - 10, container_top)
        self.elements.append(self.container_view_buttons)

        # zoom ....
        self.lbl_zoom = ScreenLabel("lbl_zoom", "Zoom: 100%", 21, 290, 1)
        self.lbl_zoom.position = (5, 5)
        self.lbl_zoom.set_background(self.general_background)
        self.lbl_zoom.set_color(self.text_color)
        self.container_view_buttons.append(self.lbl_zoom)

        self.btn_zoom_reduce = ScreenButton("btn_zoom_reduce", "[ - ]", 21, 90)
        self.btn_zoom_reduce.set_colors(button_text_color, button_back_color)
        self.btn_zoom_reduce.position = (10, self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_reduce.click_callback = self.btn_zoom_reduce_click
        self.container_view_buttons.append(self.btn_zoom_reduce)

        self.btn_zoom_increase = ScreenButton("btn_zoom_increase", "[ + ]", 21,
                                              90)
        self.btn_zoom_increase.set_colors(button_text_color, button_back_color)
        self.btn_zoom_increase.position = (self.container_view_buttons.width -
                                           self.btn_zoom_increase.width - 10,
                                           self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_increase.click_callback = self.btn_zoom_increase_click
        self.container_view_buttons.append(self.btn_zoom_increase)

        self.btn_zoom_zero = ScreenButton("btn_zoom_zero", "100%", 21, 90)
        self.btn_zoom_zero.set_colors(button_text_color, button_back_color)
        self.btn_zoom_zero.position = (
            (self.container_view_buttons.width - self.btn_zoom_zero.width) / 2,
            self.lbl_zoom.get_bottom() + 10)
        self.btn_zoom_zero.click_callback = self.btn_zoom_zero_click
        self.container_view_buttons.append(self.btn_zoom_zero)

        self.btn_view_raw_data = ScreenButton("btn_view_raw_data", "RGB Data",
                                              21, button_2_width)
        self.btn_view_raw_data.set_colors(button_text_color, button_back_color)
        self.btn_view_raw_data.position = (button_2_left,
                                           self.btn_zoom_zero.get_bottom() +
                                           10)
        self.btn_view_raw_data.click_callback = self.btn_view_raw_data_click
        self.container_view_buttons.append(self.btn_view_raw_data)

        self.btn_view_gray_data = ScreenButton("btn_view_gray", "Gray Data",
                                               21, button_2_width)
        self.btn_view_gray_data.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_gray_data.position = (button_2_right,
                                            self.btn_zoom_zero.get_bottom() +
                                            10)
        self.btn_view_gray_data.click_callback = self.btn_view_gray_data_click
        self.container_view_buttons.append(self.btn_view_gray_data)

        self.btn_view_raw_clear = ScreenButton("btn_view_raw_clear",
                                               "RGB Clear", 21, button_2_width)
        self.btn_view_raw_clear.set_colors(button_text_color,
                                           button_back_color)
        self.btn_view_raw_clear.position = (
            button_2_left, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_raw_clear.click_callback = self.btn_view_raw_clear_click
        self.container_view_buttons.append(self.btn_view_raw_clear)

        self.btn_view_gray_clear = ScreenButton("btn_view_gray_clear",
                                                "Gray Clear", 21,
                                                button_2_width)
        self.btn_view_gray_clear.set_colors(button_text_color,
                                            button_back_color)
        self.btn_view_gray_clear.position = (
            button_2_right, self.btn_view_raw_data.get_bottom() + 10)
        self.btn_view_gray_clear.click_callback = self.btn_view_gray_clear_click
        self.container_view_buttons.append(self.btn_view_gray_clear)

        # ===========================
        # confirmation panel
        self.container_confirm_buttons = ScreenContainer(
            "container_confirm_buttons", (container_width, 70),
            back_color=self.general_background)
        self.container_confirm_buttons.position = (
            self.width - self.container_confirm_buttons.width - 10,
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_confirm_buttons)
        self.container_confirm_buttons.visible = False

        self.lbl_confirm_message = ScreenLabel(
            "lbl_confirm_message", "Confirmation message goes here?", 21, 290,
            1)
        self.lbl_confirm_message.position = (5, 5)
        self.lbl_confirm_message.set_background(self.general_background)
        self.lbl_confirm_message.set_color(self.text_color)
        self.container_confirm_buttons.append(self.lbl_confirm_message)

        self.btn_confirm_cancel = ScreenButton("btn_confirm_cancel", "Cancel",
                                               21, 130)
        self.btn_confirm_cancel.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_cancel.position = (
            10, self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_cancel.click_callback = self.btn_confirm_cancel_click
        self.container_confirm_buttons.append(self.btn_confirm_cancel)

        self.btn_confirm_accept = ScreenButton("btn_confirm_accept", "Accept",
                                               21, 130)
        self.btn_confirm_accept.set_colors(button_text_color,
                                           button_back_color)
        self.btn_confirm_accept.position = (
            self.container_confirm_buttons.width -
            self.btn_confirm_accept.width - 10,
            self.lbl_confirm_message.get_bottom() + 10)
        self.btn_confirm_accept.click_callback = self.btn_confirm_accept_click
        self.container_confirm_buttons.append(self.btn_confirm_accept)

        # ===========================
        # general edition options
        darker_background = (100, 66, 30)
        self.container_text_options = ScreenContainer(
            "container_text_options", (container_width, 550),
            back_color=darker_background)
        self.container_text_options.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_text_options)

        self.lbl_text_title = ScreenLabel("lbl_text_title",
                                          "Options for Text Annotation", 21,
                                          290, 1)
        self.lbl_text_title.position = (5, 5)
        self.lbl_text_title.set_background(darker_background)
        self.lbl_text_title.set_color(self.text_color)
        self.container_text_options.append(self.lbl_text_title)

        self.lbx_text_list = ScreenTextlist("lbx_text_list",
                                            (container_width - 20, 350),
                                            18,
                                            back_color=(255, 255, 255),
                                            option_color=(0, 0, 0),
                                            selected_back=(120, 80, 50),
                                            selected_color=(255, 255, 255))
        self.lbx_text_list.position = (10,
                                       self.lbl_text_title.get_bottom() + 10)
        self.lbx_text_list.selected_value_change_callback = self.lbx_text_list_option_changed
        self.container_text_options.append(self.lbx_text_list)

        self.btn_text_add = ScreenButton("btn_text_add", "Add", 21, 90)
        self.btn_text_add.set_colors(button_text_color, button_back_color)
        self.btn_text_add.position = (10, self.lbx_text_list.get_bottom() + 10)
        self.btn_text_add.click_callback = self.btn_text_add_click
        self.container_text_options.append(self.btn_text_add)

        self.btn_text_edit = ScreenButton("btn_text_edit", "Edit", 21, 90)
        self.btn_text_edit.set_colors(button_text_color, button_back_color)
        self.btn_text_edit.position = (
            (self.container_text_options.width - self.btn_text_edit.width) / 2,
            self.lbx_text_list.get_bottom() + 10)
        self.btn_text_edit.click_callback = self.btn_text_edit_click
        self.container_text_options.append(self.btn_text_edit)

        self.btn_text_delete = ScreenButton("btn_text_delete", "Delete", 21,
                                            90)
        self.btn_text_delete.set_colors(button_text_color, button_back_color)
        self.btn_text_delete.position = (self.container_text_options.width -
                                         self.btn_text_delete.width - 10,
                                         self.lbx_text_list.get_bottom() + 10)
        self.btn_text_delete.click_callback = self.btn_text_delete_click
        self.container_text_options.append(self.btn_text_delete)

        self.btn_return_accept = ScreenButton("btn_return_accept", "Accept",
                                              21, button_2_width)
        self.btn_return_accept.set_colors(button_text_color, button_back_color)
        self.btn_return_accept.position = (button_2_left,
                                           self.container_text_options.height -
                                           self.btn_return_accept.height - 10)
        self.btn_return_accept.click_callback = self.btn_return_accept_click
        self.container_text_options.append(self.btn_return_accept)

        self.btn_return_cancel = ScreenButton("btn_return_cancel", "Cancel",
                                              21, button_2_width)
        self.btn_return_cancel.set_colors(button_text_color, button_back_color)
        self.btn_return_cancel.position = (button_2_right,
                                           self.container_text_options.height -
                                           self.btn_return_cancel.height - 10)
        self.btn_return_cancel.click_callback = self.btn_return_cancel_click
        self.container_text_options.append(self.btn_return_cancel)

        # =============================================================
        # options for editing a given text

        self.container_edit_options = ScreenContainer(
            "container_edit_options", (container_width, 550),
            back_color=darker_background)
        self.container_edit_options.position = (
            self.container_view_buttons.get_left(),
            self.container_view_buttons.get_bottom() + 20)
        self.elements.append(self.container_edit_options)

        self.lbl_edit_title = ScreenLabel("lbl_edit_title", "Editing Text", 21,
                                          290, 1)
        self.lbl_edit_title.position = (5, 5)
        self.lbl_edit_title.set_background(darker_background)
        self.lbl_edit_title.set_color(self.text_color)
        self.container_edit_options.append(self.lbl_edit_title)

        self.lbx_edit_type = ScreenTextlist("lbx_edit_type",
                                            (container_width - 20, 280),
                                            18,
                                            back_color=(255, 255, 255),
                                            option_color=(0, 0, 0),
                                            selected_back=(120, 80, 50),
                                            selected_color=(255, 255, 255))
        self.lbx_edit_type.position = (10,
                                       self.lbl_edit_title.get_bottom() + 10)
        self.container_edit_options.append(self.lbx_edit_type)
        self.add_text_types()

        self.lbl_edit_text = ScreenLabel("lbl_edit_text", "Transcription", 21,
                                         button_width, 1)
        self.lbl_edit_text.position = (button_left,
                                       self.lbx_edit_type.get_bottom() + 20)
        self.lbl_edit_text.set_background(darker_background)
        self.lbl_edit_text.set_color(self.text_color)
        self.container_edit_options.append(self.lbl_edit_text)

        self.btn_edit_OCR_0 = ScreenButton("btn_edit_OCR_0", "OCR R+0", 21,
                                           button_2_width)
        self.btn_edit_OCR_0.set_colors(button_text_color, button_back_color)
        self.btn_edit_OCR_0.position = (button_2_left,
                                        self.lbl_edit_text.get_bottom() + 10)
        self.btn_edit_OCR_0.click_callback = self.btn_edit_OCR_0_click
        self.container_edit_options.append(self.btn_edit_OCR_0)

        self.btn_edit_OCR_any = ScreenButton("btn_edit_OCR_any", "OCR R+ANY",
                                             21, button_2_width)
        self.btn_edit_OCR_any.set_colors(button_text_color, button_back_color)
        self.btn_edit_OCR_any.position = (button_2_right,
                                          self.lbl_edit_text.get_bottom() + 10)
        self.btn_edit_OCR_any.click_callback = self.btn_edit_OCR_any_click
        self.container_edit_options.append(self.btn_edit_OCR_any)
        """
        self.btn_edit_OCR_180 = ScreenButton("btn_edit_OCR_180", "OCR R+180", 21, button_2_width)
        self.btn_edit_OCR_180.set_colors(button_text_color, button_back_color)
        self.btn_edit_OCR_180.position = (button_2_right, self.lbl_edit_text.get_bottom() + 10)
        self.btn_edit_OCR_180.click_callback = self.btn_edit_OCR_180_click
        self.container_edit_options.append(self.btn_edit_OCR_180)
        """

        self.btn_edit_OCR_270 = ScreenButton("btn_edit_OCR_270", "OCR R-90",
                                             21, button_2_width)
        self.btn_edit_OCR_270.set_colors(button_text_color, button_back_color)
        self.btn_edit_OCR_270.position = (button_2_left,
                                          self.btn_edit_OCR_0.get_bottom() +
                                          10)
        self.btn_edit_OCR_270.click_callback = self.btn_edit_OCR_270_click
        self.container_edit_options.append(self.btn_edit_OCR_270)

        self.btn_edit_OCR_90 = ScreenButton("btn_edit_OCR_90", "OCR R+90", 21,
                                            button_2_width)
        self.btn_edit_OCR_90.set_colors(button_text_color, button_back_color)
        self.btn_edit_OCR_90.position = (button_2_right,
                                         self.btn_edit_OCR_0.get_bottom() + 10)
        self.btn_edit_OCR_90.click_callback = self.btn_edit_OCR_90_click
        self.container_edit_options.append(self.btn_edit_OCR_90)

        self.txt_edit_text = ScreenTextbox("txt_edit_text",
                                           "",
                                           24,
                                           container_width - 20,
                                           text_color=(255, 255, 255),
                                           back_color=(0, 0, 0))
        self.txt_edit_text.position = (5,
                                       self.btn_edit_OCR_270.get_bottom() + 10)
        self.txt_edit_text.capture_EOL = True  # allow multi-line elements
        self.container_edit_options.append(self.txt_edit_text)

        self.btn_edit_cancel = ScreenButton("btn_edit_cancel", "Cancel", 21,
                                            button_2_width)
        self.btn_edit_cancel.set_colors(button_text_color, button_back_color)
        self.btn_edit_cancel.position = (button_2_right,
                                         self.container_edit_options.height -
                                         self.btn_edit_cancel.height - 10)
        self.btn_edit_cancel.click_callback = self.btn_edit_cancel_click
        self.container_edit_options.append(self.btn_edit_cancel)

        self.btn_edit_accept = ScreenButton("btn_return_accept", "Accept", 21,
                                            button_2_width)
        self.btn_edit_accept.set_colors(button_text_color, button_back_color)
        self.btn_edit_accept.position = (button_2_left,
                                         self.container_edit_options.height -
                                         self.btn_edit_accept.height - 10)
        self.btn_edit_accept.click_callback = self.btn_edit_accept_click
        self.container_edit_options.append(self.btn_edit_accept)
        self.container_edit_options.visible = False

        # ===========================
        # Image

        image_width = self.width - self.container_view_buttons.width - 30
        image_height = self.height - container_top - 10
        self.container_images = ScreenContainer("container_images",
                                                (image_width, image_height),
                                                back_color=(0, 0, 0))
        self.container_images.position = (10, container_top)
        self.elements.append(self.container_images)

        # ... image objects ...
        tempo_blank = np.zeros((50, 50, 3), np.uint8)
        tempo_blank[:, :, :] = 255
        self.img_main = ScreenImage("img_main", tempo_blank, 0, 0, True,
                                    cv2.INTER_NEAREST)
        self.img_main.position = (0, 0)
        self.img_main.double_click_callback = self.img_mouse_double_click
        self.img_main.mouse_button_down_callback = self.img_mouse_down
        self.container_images.append(self.img_main)

        self.canvas_select = ScreenCanvas("canvas_select", 100, 100)
        self.canvas_select.position = (0, 0)
        self.canvas_select.locked = True
        # self.canvas_select.object_edited_callback = self.canvas_object_edited
        # self.canvas_select.object_selected_callback = self.canvas_selection_changed
        self.container_images.append(self.canvas_select)

        base_points = np.array([[10, 10], [20, 10], [20, 20], [10, 20]],
                               dtype=np.float64)
        self.canvas_select.add_polygon_element("selection_polygon",
                                               base_points)
        self.canvas_select.elements["selection_polygon"].visible = False

        self.canvas_display = ScreenCanvas("canvas_display", 100, 100)
        self.canvas_display.position = (0, 0)
        self.canvas_display.locked = True
        self.container_images.append(self.canvas_display)

        self.add_text_regions()

        self.set_editor_mode(ChartTextAnnotator.ModeNavigate)

    def add_text_types(self):
        self.lbx_edit_type.add_option(str(TextInfo.TypeChartTitle),
                                      "Chart Title")
        self.lbx_edit_type.add_option(str(TextInfo.TypeAxisTitle),
                                      "Axis Title")
        self.lbx_edit_type.add_option(str(TextInfo.TypeTickLabel),
                                      "Tick Label")
        self.lbx_edit_type.add_option(str(TextInfo.TypeLegendTitle),
                                      "Legend Title")
        self.lbx_edit_type.add_option(str(TextInfo.TypeLegendLabel),
                                      "Legend Label")
        self.lbx_edit_type.add_option(str(TextInfo.TypeValueLabel),
                                      "Value Label")
        self.lbx_edit_type.add_option(str(TextInfo.TypeOther), "Other")

    def get_text_display(self, text):
        return "{0:d} - {1:s}: {2:s}".format(text.id,
                                             text.get_type_description(),
                                             text.value)

    def add_text_regions(self):
        # populate the list-box using existing text regions (if any)
        for text in self.text_regions:
            self.lbx_text_list.add_option(str(text.id),
                                          self.get_text_display(text))

            self.canvas_display.add_polygon_element(
                str(text.id), text.position_polygon.copy(),
                self.canvas_colors[text.type],
                self.canvas_sel_colors[text.type])

    def btn_zoom_reduce_click(self, button):
        self.update_view_scale(self.view_scale - 0.25)

    def btn_zoom_increase_click(self, button):
        self.update_view_scale(self.view_scale + 0.25)

    def btn_zoom_zero_click(self, button):
        self.update_view_scale(1.0)

    def btn_view_raw_data_click(self, button):
        self.view_mode = ChartTextAnnotator.ViewModeRawData
        self.update_current_view()

    def btn_view_gray_data_click(self, button):
        self.view_mode = ChartTextAnnotator.ViewModeGrayData
        self.update_current_view()

    def btn_view_raw_clear_click(self, button):
        self.view_mode = ChartTextAnnotator.ViewModeRawNoData
        self.update_current_view()

    def btn_view_gray_clear_click(self, button):
        self.view_mode = ChartTextAnnotator.ViewModeGrayNoData
        self.update_current_view()

    def update_current_view(self, resized=False):
        if self.view_mode in [
                ChartTextAnnotator.ViewModeGrayData,
                ChartTextAnnotator.ViewModeGrayNoData
        ]:
            # gray scale mode
            base_image = self.panel_gray
        else:
            base_image = self.panel_image

        h, w, c = base_image.shape

        modified_image = base_image.copy()

        if self.view_mode in [
                ChartTextAnnotator.ViewModeRawData,
                ChartTextAnnotator.ViewModeGrayData
        ]:
            # TODO: show here any relevant annotations on the modified image ...
            # (for example, draw the polygons)
            self.canvas_display.visible = True
        else:
            self.canvas_display.visible = False

        # finally, resize ...
        modified_image = cv2.resize(
            modified_image,
            (int(w * self.view_scale), int(h * self.view_scale)),
            interpolation=cv2.INTER_NEAREST)

        # update canvas size ....
        self.canvas_select.height, self.canvas_select.width, _ = modified_image.shape
        self.canvas_display.height, self.canvas_display.width, _ = modified_image.shape

        # replace/update image
        self.img_main.set_image(modified_image, 0, 0, True, cv2.INTER_NEAREST)
        if resized:
            self.container_images.recalculate_size()

    def update_view_scale(self, new_scale):
        prev_scale = self.view_scale

        if 0.25 <= new_scale <= 4.0:
            self.view_scale = new_scale
        else:
            return

        # keep previous offsets ...
        scroll_offset_y = self.container_images.v_scroll.value if self.container_images.v_scroll.active else 0
        scroll_offset_x = self.container_images.h_scroll.value if self.container_images.h_scroll.active else 0

        prev_center_y = scroll_offset_y + self.container_images.height / 2
        prev_center_x = scroll_offset_x + self.container_images.width / 2

        # compute new scroll bar offsets
        scale_factor = (new_scale / prev_scale)
        new_off_y = prev_center_y * scale_factor - self.container_images.height / 2
        new_off_x = prev_center_x * scale_factor - self.container_images.width / 2

        # update view ....
        self.update_current_view(True)

        # set offsets
        if self.container_images.v_scroll.active and 0 <= new_off_y <= self.container_images.v_scroll.max:
            self.container_images.v_scroll.value = new_off_y
        if self.container_images.h_scroll.active and 0 <= new_off_x <= self.container_images.h_scroll.max:
            self.container_images.h_scroll.value = new_off_x

        # re-scale objects from both canvas
        # ... selection ...
        selection_polygon = self.canvas_select.elements["selection_polygon"]
        selection_polygon.points *= scale_factor
        # ... display ...
        for polygon_name in self.canvas_display.elements:
            display_polygon = self.canvas_display.elements[polygon_name]
            display_polygon.points *= scale_factor

        # update scale text ...
        self.lbl_zoom.set_text("Zoom: " +
                               str(int(round(self.view_scale * 100, 0))) + "%")

    def set_editor_mode(self, new_mode):
        self.edition_mode = new_mode

        # Navigation mode ...
        self.container_text_options.visible = (
            self.edition_mode == ChartTextAnnotator.ModeNavigate)

        # Edit panels ...
        self.container_edit_options.visible = self.edition_mode in [
            ChartTextAnnotator.ModeAddingTextEdit,
            ChartTextAnnotator.ModeEditingText
        ]

        # Confirm panel and buttons  ...
        self.container_confirm_buttons.visible = self.edition_mode in [
            ChartTextAnnotator.ModeAddingTextSelect,
            ChartTextAnnotator.ModeConfirmDeleteText,
            ChartTextAnnotator.ModeConfirmExit,
            ChartTextAnnotator.ModeConfirmOverwrite
        ]

        if self.edition_mode == ChartTextAnnotator.ModeAddingTextSelect:
            self.lbl_confirm_message.set_text("Select Text Location")
        elif self.edition_mode == ChartTextAnnotator.ModeAddingTextEdit:
            self.lbl_confirm_message.set_text("Adding Text Region")
        elif self.edition_mode == ChartTextAnnotator.ModeConfirmDeleteText:
            self.lbl_confirm_message.set_text("Delete Text Region?")
        elif self.edition_mode == ChartTextAnnotator.ModeConfirmExit:
            self.lbl_confirm_message.set_text("Discard Changes to Text?")
        elif self.edition_mode == ChartTextAnnotator.ModeConfirmOverwrite:
            self.lbl_confirm_message.set_text(
                "Discarding Legend/Axis data, Proceed?")

        # Do not show accept at these steps (they can be implicitly accepted, but need explicit cancel button only)
        self.btn_confirm_accept.visible = self.edition_mode != ChartTextAnnotator.ModeAddingTextSelect

        if new_mode in [
                ChartTextAnnotator.ModeAddingTextEdit,
                ChartTextAnnotator.ModeEditingText
        ]:
            # show polygon
            self.canvas_select.locked = False
            self.canvas_select.elements["selection_polygon"].visible = True
        else:
            # for every other mode
            self.canvas_select.locked = True
            self.canvas_select.elements["selection_polygon"].visible = False

        if new_mode == ChartTextAnnotator.ModeAddingTextEdit:
            # prepare empty input ...
            self.lbx_text_list.change_option_selected(None)
            self.txt_edit_text.updateText("")

    def btn_confirm_accept_click(self, button):
        if self.edition_mode == ChartTextAnnotator.ModeConfirmExit:
            print("-> Changes made to Text Annotations were lost")
            self.return_screen = self.parent_screen
        elif self.edition_mode == ChartTextAnnotator.ModeConfirmDeleteText:
            # delete tempo region ...
            # remove from text regions
            self.text_regions.remove(self.tempo_edit_text)

            # remove from the GUI
            self.lbx_text_list.remove_option(str(self.tempo_edit_text.id))
            self.canvas_display.remove_element(str(self.tempo_edit_text.id))

            self.data_changed = True

            # return  ...
            self.set_editor_mode(ChartTextAnnotator.ModeNavigate)
        elif self.edition_mode == ChartTextAnnotator.ModeConfirmOverwrite:
            # overwrite text data ...
            if self.admin_mode:
                overwrite = input(
                    "Discard Axis and/or Legend Info (y/n)? ").lower() in [
                        "y", "yes", "1", "true"
                    ]
            else:
                # for all other users, existing data will be discarded to ensure consistency
                overwrite = True

            self.panel_info.overwrite_text(self.text_regions, overwrite)
            self.parent_screen.subtool_completed(True)
            # return
            self.return_screen = self.parent_screen
        else:
            raise Exception("Not Implemented")

    def btn_confirm_cancel_click(self, button):
        if self.edition_mode in [
                ChartTextAnnotator.ModeAddingTextSelect,
                ChartTextAnnotator.ModeConfirmDeleteText,
                ChartTextAnnotator.ModeConfirmExit,
                ChartTextAnnotator.ModeConfirmOverwrite
        ]:
            # return to navigation
            self.set_editor_mode(ChartTextAnnotator.ModeNavigate)
        else:
            print(self.edition_mode)
            raise Exception("Not Implemented")

    def get_next_polygon(self, click_x, click_y):

        if len(
                self.text_regions
        ) > 0 and self.text_regions[-1].axis_aligned_rectangle_ratio() < 0.8:
            # simply copy as a polygon centered on the click point

            # copy and scale to current view ...
            points = self.text_regions[-1].position_polygon.copy(
            ) * self.view_scale

            # find scaled center ... compute and apply translation to get a polygon aligned with the click point
            old_cx = points[:, 0].mean()
            old_cy = points[:, 1].mean()

            delta_x = click_x - old_cx
            delta_y = click_y - old_cy

            points[:, 0] += delta_x
            points[:, 1] += delta_y
        else:
            if len(self.text_regions) == 0:
                # default small rectangle
                rect_w, rect_h = 40, 20
            else:
                # axis aligned container rectangle from last bbox
                min_x, min_y, max_x, max_y = self.text_regions[
                    -1].get_axis_aligned_rectangle()

                rect_w = (max_x - min_x) * self.view_scale
                rect_h = (max_y - min_y) * self.view_scale

            points = np.array([[click_x, click_y], [click_x + rect_w, click_y],
                               [click_x + rect_w, click_y + rect_h],
                               [click_x, click_y + rect_h]])

        return points

    def img_mouse_double_click(self, element, pos, button):
        if button == 1:
            # double left click ...
            if self.edition_mode == ChartTextAnnotator.ModeNavigate:
                click_x, click_y = pos
                points = self.get_next_polygon(click_x, click_y)
                self.canvas_select.elements["selection_polygon"].update(points)

                self.set_editor_mode(ChartTextAnnotator.ModeAddingTextEdit)

    def btn_text_add_click(self, button):
        self.set_editor_mode(ChartTextAnnotator.ModeAddingTextSelect)

    def btn_text_edit_click(self, button):
        if self.lbx_text_list.selected_option_value is None:
            print("Must select an option to edit")
            return

        for text in self.text_regions:
            if text.id == int(self.lbx_text_list.selected_option_value):
                # option found
                # copy
                self.tempo_edit_text = text

                # ... copy points to selection canvas ...
                polygon = self.tempo_edit_text.position_polygon.copy(
                ) * self.view_scale
                self.canvas_select.update_polygon_element(
                    "selection_polygon", polygon, True)
                # ... copy type to list of types
                self.lbx_edit_type.change_option_selected(
                    str(self.tempo_edit_text.type))
                # ... copy transcript to textbox
                self.txt_edit_text.updateText(self.tempo_edit_text.value)

                self.canvas_display.elements[str(
                    self.tempo_edit_text.id)].visible = False

                self.set_editor_mode(ChartTextAnnotator.ModeEditingText)
                break

    def btn_text_delete_click(self, button):
        if self.lbx_text_list.selected_option_value is None:
            print("Must select an option to edit")
            return

        for text in self.text_regions:
            if text.id == int(self.lbx_text_list.selected_option_value):
                # option found
                # copy
                self.tempo_edit_text = text

                self.set_editor_mode(ChartTextAnnotator.ModeConfirmDeleteText)
                break

    def btn_return_accept_click(self, button):
        if self.data_changed:
            if self.panel_info.legend is not None or self.panel_info.axes is not None:
                # confirm if return ... might overwrite existing data
                self.set_editor_mode(ChartTextAnnotator.ModeConfirmOverwrite)
            else:
                # overwrite text data ... both legend  and axes are None
                self.panel_info.overwrite_text(self.text_regions, False)
                # return
                self.return_screen = self.parent_screen
        else:
            # Nothing changed just return
            self.return_screen = self.parent_screen

    def btn_return_cancel_click(self, button):
        if self.data_changed:
            # confirm if return ...
            self.set_editor_mode(ChartTextAnnotator.ModeConfirmExit)
        else:
            # just return
            self.return_screen = self.parent_screen

    def find_next_id(self):
        if len(self.text_regions) == 0:
            return 0
        else:
            return max([text.id for text in self.text_regions]) + 1

    def btn_edit_accept_click(self, button):
        if self.lbx_edit_type.selected_option_value is None:
            print("-> Must Select text type")
            return

        if len(self.txt_edit_text.text.strip()) == 0:
            print("-> Must Provide Transcription")
            return

        text_polygon = self.canvas_select.elements[
            "selection_polygon"].points / self.view_scale
        text_type = int(self.lbx_edit_type.selected_option_value)
        text_value = self.txt_edit_text.text.strip()

        if self.edition_mode == ChartTextAnnotator.ModeAddingTextEdit:
            # new text will be added
            next_id = self.find_next_id()
            text = TextInfo(next_id, text_polygon, text_type, text_value)
            self.text_regions.append(text)
            # add to the GUI
            display = self.get_text_display(text)
            self.lbx_text_list.add_option(str(next_id), display)
            self.canvas_display.add_polygon_element(
                str(text.id),
                text.position_polygon.copy() * self.view_scale,
                self.canvas_colors[text.type],
                self.canvas_sel_colors[text.type])
            self.data_changed = True
            # return  ...
            self.set_editor_mode(ChartTextAnnotator.ModeNavigate)
        elif self.edition_mode == ChartTextAnnotator.ModeEditingText:
            # existing text should be over-written

            self.tempo_edit_text.position_polygon = text_polygon
            self.tempo_edit_text.type = text_type
            self.tempo_edit_text.value = text_value

            key_str = str(self.tempo_edit_text.id)
            display = self.get_text_display(self.tempo_edit_text)

            # update list
            self.lbx_text_list.update_option_display(key_str, display)
            # update and make canvas representation of the text region visible again ..
            canvas_points = self.tempo_edit_text.position_polygon.copy(
            ) * self.view_scale
            self.canvas_display.update_polygon_element(key_str, canvas_points,
                                                       True)
            self.canvas_display.update_custom_colors(
                key_str, self.canvas_colors[self.tempo_edit_text.type],
                self.canvas_sel_colors[self.tempo_edit_text.type])

            # remove temporal reference
            self.tempo_edit_text = None

            self.data_changed = True
            # return  ...
            self.set_editor_mode(ChartTextAnnotator.ModeNavigate)

    def btn_edit_cancel_click(self, button):
        if self.edition_mode == ChartTextAnnotator.ModeEditingText:
            # make the selected element visible again (no changes)
            self.canvas_display.elements[str(
                self.tempo_edit_text.id)].visible = True

        # return ....
        self.set_editor_mode(ChartTextAnnotator.ModeNavigate)

    def img_mouse_down(self, img_object, pos, button):
        if button == 1:
            if self.edition_mode == ChartTextAnnotator.ModeAddingTextSelect:
                click_x, click_y = pos
                points = self.get_next_polygon(click_x, click_y)
                self.canvas_select.elements["selection_polygon"].update(points)

                self.set_editor_mode(ChartTextAnnotator.ModeAddingTextEdit)

    def lbx_text_list_option_changed(self, new_value, old_value):
        self.canvas_display.change_selected_element(new_value)

    def btn_edit_OCR_0_click(self, button):
        self.apply_OCR(ChartTextAnnotator.Rotation0)
        # self.apply_OCR(ChartTextAnnotator.RotationAuto)

    def btn_edit_OCR_any_click(self, button):
        self.apply_OCR(ChartTextAnnotator.RotationAuto)

    def btn_edit_OCR_90_click(self, button):
        self.apply_OCR(ChartTextAnnotator.Rotation90)

    def btn_edit_OCR_270_click(self, button):
        self.apply_OCR(ChartTextAnnotator.Rotation270)

    def btn_edit_OCR_180_click(self, button):
        self.apply_OCR(ChartTextAnnotator.Rotation180)

    def order_points(self, pts):
        # sort the points based on their x-coordinates
        xSorted = pts[np.argsort(pts[:, 0]), :]

        # grab the left-most and right-most points from the sorted
        # x-roodinate points
        leftMost = xSorted[:2, :]
        rightMost = xSorted[2:, :]

        # now, sort the left-most coordinates according to their
        # y-coordinates so we can grab the top-left and bottom-left
        # points, respectively
        leftMost = leftMost[np.argsort(leftMost[:, 1]), :]
        (tl, bl) = leftMost

        # now that we have the top-left coordinate, use it as an
        # anchor to calculate the Euclidean distance between the
        # top-left and right-most points; by the Pythagorean
        # theorem, the point with the largest distance will be
        # our bottom-right point
        D = dist.cdist(tl[np.newaxis], rightMost, "euclidean")[0]
        (br, tr) = rightMost[np.argsort(D)[::-1], :]

        # return the coordinates in top-left, top-right,
        # bottom-right, and bottom-left order
        return np.asarray([tl, tr, br, bl], dtype=pts.dtype)

    def crop_rotated_rectangle(self, img, coords):
        # find rotated rectangle
        rect = cv2.minAreaRect(coords.reshape(4, 1, 2).astype(np.float32))
        rbox = self.order_points(cv2.boxPoints(rect))
        # get width and height of the detected rectangle
        # output of minAreaRect is unreliable for already axis aligned rectangles!!
        width = np.linalg.norm(
            [rbox[0, 0] - rbox[1, 0], rbox[0, 1] - rbox[1, 1]])
        height = np.linalg.norm(
            [rbox[0, 0] - rbox[-1, 0], rbox[0, 1] - rbox[-1, 1]])
        src_pts = rbox.astype(np.float32)
        # coordinate of the points in box points after the rectangle has been straightened
        # this step needs order_points to be called on src
        dst_pts = np.array(
            [[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]],
            dtype="float32")
        # the perspective transformation matrix
        M = cv2.getPerspectiveTransform(src_pts, dst_pts)
        # directly warp the rotated rectangle to get the straightened rectangle
        warped = cv2.warpPerspective(img, M, (width, height), None,
                                     cv2.INTER_LINEAR, cv2.BORDER_CONSTANT,
                                     (255, 255, 255))
        return warped

    def crop_axis_aligned_rectangle(self, img, text_polygon, rotation):
        panel_h, panel_w, _ = img.shape

        # get polygon bounding box ...
        x1 = max(0, int(text_polygon[:, 0].min()))
        y1 = max(0, int(text_polygon[:, 1].min()))

        x2 = min(panel_w, int(text_polygon[:, 0].max() + 1))
        y2 = min(panel_h, int(text_polygon[:, 1].max() + 1))

        # get image first ..
        text_img = img[y1:y2, x1:x2]
        h, w, _ = text_img.shape

        # apply rotation ... (if any)
        if rotation == ChartTextAnnotator.Rotation90:
            M = cv2.getRotationMatrix2D((0, 0), -90, 1)
            M[0, 2] += (h - 1)
            text_img = cv2.warpAffine(text_img, M, (h, w))

        elif rotation == ChartTextAnnotator.Rotation180:
            M = cv2.getRotationMatrix2D((0, 0), -180, 1)
            M[0, 2] += (w - 1)
            M[1, 2] += (h - 1)
            text_img = cv2.warpAffine(text_img, M, (w, h))

        elif rotation == ChartTextAnnotator.Rotation270:
            M = cv2.getRotationMatrix2D((0, 0), 90, 1)
            M[1, 2] += (w - 1)
            text_img = cv2.warpAffine(text_img, M, (h, w))

        return text_img

    def apply_OCR(self, rotation):
        if OCR is None:
            print(
                "PyTesseract not found, please install to enable this function"
            )
            return

        text_polygon = self.canvas_select.elements[
            "selection_polygon"].points / self.view_scale
        print(text_polygon)

        if rotation == ChartTextAnnotator.RotationAuto:
            text_polygon = self.order_points(text_polygon)
            text_img = self.crop_rotated_rectangle(self.panel_image,
                                                   text_polygon)
        else:
            text_img = self.crop_axis_aligned_rectangle(
                self.panel_image, text_polygon, rotation)

        result = OCR.image_to_string(text_img, config='--psm 6')
        self.txt_edit_text.updateText(result)

        #if result.strip() == "":
        cv2.imwrite("TEMPO_CHECK_OCR_2.png", text_img)

        print("-> Tesseract Result: " + result)