Example #1
0
class Gui:

    def __init__(self, master, database):
        self.database = database
        self.point_idx = 0
        self.match_idx = 0
        self.curr_images = [None, None]
        self.curr_point = None
        self.plt_artists = [[], []]
        self.figures = []
        self.subplots = []
        self.canvases = []
        self.filename = None
        self.shot_std = {}
        p_shot_std = self.database.get_path() + '/shots_std.csv'
        if os.path.exists(p_shot_std):
            self.load_shot_std(p_shot_std)

        master.bind('q', lambda event: self.go_to_worst_gcp())
        self.create_ui(master)

    def create_ui(self, master):
        tools_frame = tk.Frame(master)
        tools_frame.pack(side='left', expand=0, fill=tk.Y)
        self.create_tools(tools_frame)

        viewer_frame = tk.Frame(master)
        viewer_frame.pack(side='right', fill=tk.BOTH, expand=1)
        self.init_image_pair_frames(viewer_frame)
        p_default_gcp = self.database.get_path() + '/ground_control_points.json'
        if os.path.exists(p_default_gcp):
            self.load_gcps(p_default_gcp)

    def create_tools(self, master):
        gcp_list_frame = tk.Frame(master)
        gcp_list_frame.pack(side='top', fill=tk.BOTH, expand=1)
        self.gcp_list_box = CustomListbox(gcp_list_frame, font=("monospace", 10), width=10)
        self.gcp_list_box.pack(side='left', expand=tk.YES, fill=tk.Y)
        self.gcp_list_box.bind('<<ListboxSelect>>', self.modify_gcp)

        plus_minus_frame = tk.Frame(master)
        plus_minus_frame.pack(side='top')
        self.add_button = tk.Button(plus_minus_frame, text="+", command=self.add_gcp)
        self.add_button.pack(side='left')
        self.remove_button = tk.Button(plus_minus_frame, text="-", command=self.remove_gcp)
        self.remove_button.pack(side='left')

        self.if_show_epipolar = tk.IntVar(value=0)
        self.check_button = tk.Checkbutton(master, text="Epipolar\nlines",
                                           var=self.if_show_epipolar)
        self.check_button.pack(side='top')

        io_frame = tk.Frame(master)
        io_frame.pack(side='top')
        self.load_button = tk.Button(io_frame, text="Load", command=self.load_gcps)
        self.load_button.pack(side='top')
        self.save_button = tk.Button(io_frame, text="Save", command=self.save_gcps)
        self.save_button.pack(side='top')
        self.save_button = tk.Button(io_frame, text="Save As", command=self.save_gcps_as)
        self.save_button.pack(side='top')

    def clear_images(self, idx):
        for artist in self.plt_artists[idx]:
            artist.set_visible(False)
            del artist

    def set_title(self, idx):
        shot = self.curr_images[idx]
        if shot in self.shot_std:
            shot_std_rank, shot_std = self.shot_std[shot]
            title = "{}\n#{} (std = {:.2f})".format(shot, shot_std_rank, shot_std)
        else:
            title = "{}\n - ".format(shot)
        self.subplots[idx].set_title(title)

    def init_image_pair_frames(self, master):
        for idx in range(2):
            nth_viewer_frame = tk.Frame(master)
            nth_viewer_frame.pack(side='left', fill=tk.BOTH, expand=1)

            button_frame = tk.Frame(nth_viewer_frame)
            button_frame.pack(side='top')
            prv_btn = tk.Button(button_frame, text=PREVIOUS_UNICODE,
                                command=lambda _idx=idx: self.go_to_previous_image(_idx))
            prv_btn.pack(side='left')
            nxt_btn = tk.Button(button_frame, text=NEXT_UNICODE,
                                command=lambda _idx=idx: self.go_to_next_image(_idx))
            nxt_btn.pack(side='left')

            figure = Figure()
            self.figures.append(figure)
            subplot = self.figures[idx].add_subplot(111)
            self.subplots.append(subplot)
            self.curr_images[idx] = self.database.get_seqs()[idx][0]  # in init, init the first pair = pairs[0]
            self.subplots[idx].imshow(self.database.get_image(self.curr_images[idx]), aspect='auto')
            self.set_title(idx)
            self.subplots[idx].axis('scaled')
            self.figures[idx].set_tight_layout(True)
            self.subplots[idx].axis('off')
            conv = FigureCanvasTkAgg(self.figures[idx], nth_viewer_frame)
            self.canvases.append(conv)
            self.canvases[idx].draw()
            self.canvases[idx].get_tk_widget().pack(side='top', fill=tk.BOTH, expand=1)
            self.canvases[idx].mpl_connect('button_press_event',
                                           lambda event:
                                           self.on_press_fig_modify(event))
            self.canvases[idx].mpl_connect('scroll_event',
                                           lambda event:
                                           self.on_scroll(event))

            self.zoomed_in = [False, False]

    def load_shot_std(self, path):
        with open(path, 'r') as f:
            for ix, line in enumerate(f):
                shot, std = line[:-1].split(',')
                self.shot_std[shot] = (ix + 1, float(std))

    def load_gcps(self, filename=None):
        if filename is None:
            filename = filedialog.askopenfilename(
                title="Open GCP file",
                initialdir=self.database.get_path(),
                filetypes=(("JSON files", "*.json"), ("all files", "*.*")),
            )
        if filename is None:
            return
        points = read_gcp_file(filename)
        self.filename = filename
        self.database.init_points(points)
        for image_idx, image in enumerate(self.curr_images):
            self.display_points(image_idx, image)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(image_idx)
        self.repopulate_modify_list()
        # for idx in range(2):
        #    for point_id in self.database.get_visible_points_coords(self.curr_images[idx]).keys():
        #        if point_id not in self.gcp_list_box:
        #            self.gcp_list_box.insert(tk.END, point_id)

    def display_points(self, image_idx, image):
        visible_points_coords = self.database.get_visible_points_coords(image)
        self.clear_images(image_idx)

        for point_id, coords in visible_points_coords.items():
            color = distinct_colors[divmod(hash(point_id), 19)[1]]
            text_path = TextPath((0, 0), point_id, size=10)
            p1 = PathPatch(text_path, transform=IdentityTransform(), alpha=1, color=color)
            offsetbox2 = AuxTransformBox(IdentityTransform())
            offsetbox2.add_artist(p1)
            ab = AnnotationBbox(offsetbox2, ((coords[0] + 30), (coords[1] + 30)), bboxprops=dict(alpha=0.05))
            circle = mpatches.Circle((coords[0], coords[1]), 20, color=color, fill=False)

            self.plt_artists[image_idx].append(ab)
            self.plt_artists[image_idx].append(circle)

            self.subplots[image_idx].add_artist(ab)
            self.subplots[image_idx].add_artist(circle)
        self.figures[image_idx].canvas.draw_idle()

    def add_gcp(self):
        new_id = id_generator()
        self.database.add_point(new_id)
        self.curr_point = new_id
        self.gcp_list_box.insert(tk.END, new_id)
        self.gcp_list_box.selection_clear(0, tk.END)
        self.gcp_list_box.selection_set(tk.END)

    def which_canvas(self, event):
        if event.canvas == self.canvases[0]:
            idx = 0
        elif event.canvas == self.canvases[1]:
            idx = 1
        else:
            idx = None
        return idx

    def on_scroll(self, event):
        idx = self.which_canvas(event)
        if event.xdata is None or event.ydata is None:
            return
        if event.button == 'up':
            self.go_to_next_image(idx)
        elif event.button == 'down':
            self.go_to_previous_image(idx)

    def zoom_in(self, image_idx, x, y):
        xlim = self.subplots[image_idx].get_xlim()
        width = max(xlim) - min(xlim)
        self.subplots[image_idx].set_xlim(x - width / 20, x + width / 20)
        self.subplots[image_idx].set_ylim(y + width / 20, y - width / 20)
        self.zoomed_in[image_idx] = True

    def zoom_out(self, image_idx):
        self.subplots[image_idx].autoscale()
        self.zoomed_in[image_idx] = False

    def on_press_fig_modify(self, event):
        idx = self.which_canvas(event)
        x, y = event.xdata, event.ydata
        if None in (x, y):
            return
        if event.button == 2:  # Middle / wheel click:
            if self.zoomed_in[idx]:
                self.zoom_out(idx)
            else:
                self.zoom_in(idx, x, y)
            self.figures[idx].canvas.draw_idle()
        elif self.curr_point is not None and event.button in (1, 3):
            if not self.curr_point:
                return
            self.database.remove_point_observation(self.curr_point, self.curr_images[idx])

            for l in self.subplots[idx].lines:
                l.remove()
            if event.button == 1: # Left click
                self.database.add_point_observation(self.curr_point, self.curr_images[idx], (x, y))

            self.display_points(idx, self.curr_images[idx])
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(idx)
        else:
            return

    def repopulate_modify_list(self):
        self.gcp_list_box.delete(0, tk.END)
        for point_id in self.database.get_points():
            self.gcp_list_box.insert(tk.END, point_id)

    def remove_gcp(self):
        to_be_removed_point = self.curr_point
        if not to_be_removed_point:
            return
        self.curr_point = None

        self.database.remove_gcp(to_be_removed_point)
        for image_idx, image in enumerate(self.curr_images):
            self.display_points(image_idx, image)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(image_idx)

        self.repopulate_modify_list()

    def modify_gcp(self, event):
        widget = event.widget
        value = widget.get(int(widget.curselection()[0]))
        self.curr_point = value

    def save_gcps(self):
        if self.filename is None:
            return self.save_gcps_as()
        else:
            return self.database.write_to_file(self.filename)

    def save_gcps_as(self):
        filename = filedialog.asksaveasfilename(
            initialfile="ground_control_points.json",
            title="Save GCP file",
            initialdir=self.database.get_path(),
            defaultextension=".json",
        )
        if filename is None:
            return
        else:
            self.filename = filename
            return self.save_gcps()

    def show_epipolar_lines(self, main_image_idx):
        img1_size = self.database.get_image_size(self.curr_images[0])
        img2_size = self.database.get_image_size(self.curr_images[1])
        matched_points = self.database.get_visible_points_coords(self.curr_images[main_image_idx])
        matched_points_coords = convert_tuple_cords_to_list(matched_points)
        matched_points_coords = features.normalized_image_coordinates(matched_points_coords, img1_size[1], img1_size[0])
        color_idx = 0
        for point_idx, point in enumerate(matched_points_coords):
            line = calc_epipol_line(point, self.curr_images, self.database.get_path(), main_image_idx)
            denormalized_lines = features.denormalized_image_coordinates(line, img2_size[1], img2_size[0])
            for line_segment in denormalized_lines:
                circle = mpatches.Circle((line_segment[0], line_segment[1]), 3,
                                         color=distinct_colors[divmod(hash(list(matched_points.keys())[point_idx]), 19)[1]])
                self.plt_artists[main_image_idx].append(circle)
                self.subplots[not main_image_idx].add_artist(circle)
            color_idx = color_idx + 1
        self.figures[not main_image_idx].canvas.draw_idle()

    def go_to_next_image(self, image_idx):
        new_image = self.database.bring_next_image(self.curr_images[image_idx], image_idx)
        if self.curr_images[image_idx] != new_image:
            self.bring_new_image(new_image, image_idx)

    def go_to_previous_image(self, image_idx):
        new_image = self.database.bring_previous_image(self.curr_images[image_idx], image_idx)
        if self.curr_images[image_idx] != new_image:
            self.bring_new_image(new_image, image_idx)

    def highlight_gcp_reprojection(self, image_idx, shot, point_id):
        x, y = 0,0
        for obs in self.database.get_points()[point_id]:
            if obs['shot_id'] == shot:
                x, y = obs['projection']

        x2, y2 = self.database.gcp_reprojections[point_id][shot]['reprojection']
        self.subplots[image_idx].plot([x, x2], [y, y2], 'r-')
        self.canvases[image_idx].draw()

    def go_to_worst_gcp(self):
        worst_gcp, shot_worst_gcp, worst_gcp_error = self.database.get_worst_gcp()
        idx_worst_gcp = 0 if shot_worst_gcp in self.database.seqs[0] else 1
        print("Worst GCP observation: {} in shot {}".format(worst_gcp, shot_worst_gcp))

        self.curr_point = worst_gcp
        self.gcp_list_box.selection_clear(0, "end")
        for ix, gcp_id in enumerate(self.gcp_list_box.get(0, "end")):
            if gcp_id == worst_gcp:
                self.gcp_list_box.selection_set(ix)
                break

        if self.curr_images[idx_worst_gcp] != shot_worst_gcp:
            self.bring_new_image(shot_worst_gcp, idx_worst_gcp)

        self.highlight_gcp_reprojection(idx_worst_gcp, shot_worst_gcp, worst_gcp)

    def bring_new_image(self, new_image, image_idx):
        self.curr_images[image_idx] = new_image
        self.subplots[image_idx].clear()
        self.subplots[image_idx].imshow(self.database.get_image(self.curr_images[image_idx]))
        self.subplots[image_idx].axis('off')
        self.set_title(image_idx)
        self.zoomed_in[image_idx] = False
        self.subplots[image_idx].axis('scaled')
        self.figures[image_idx].set_tight_layout(True)
        self.subplots[image_idx].axis('off')
        for idx, image in enumerate(self.curr_images):
            self.display_points(idx, image)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(idx)

        self.canvases[image_idx].draw()

    def print_all(self):
        for image1 in self.database.get_seqs()[0]:
            for image2 in self.database.get_seqs()[1]:
                print(image1, image2)
                print(self.database.get_visible_points_coords(image1))
Example #2
0
class Gui:
    def __init__(self, master, database, n_views, sequence_groups=[]):
        self.database = database
        self.point_idx = 0
        self.match_idx = 0
        self.curr_point = None
        self.last_saved_filename = None
        self.shot_std = {}
        self.sequence_groups = sequence_groups

        master.bind_all('q', lambda event: self.go_to_worst_gcp())
        master.bind_all('z', lambda event: self.toggle_zoom_on_key())
        self.create_ui(master, n_views=n_views)
        master.lift()

        p_default_gcp = self.database.get_path(
        ) + '/ground_control_points.json'
        if os.path.exists(p_default_gcp):
            self.load_gcps(p_default_gcp)
        p_shot_std = self.database.get_path() + '/shots_std.csv'
        if os.path.exists(p_shot_std):
            self.load_shot_std(p_shot_std)

    def create_ui(self, master, n_views):
        self.master = master

        tools_frame = tk.Frame(master)
        tools_frame.pack(side='left', expand=0, fill=tk.Y)
        self.create_tools(tools_frame)

        self.views = []
        for view_ix in range(n_views):
            new_window = tk.Toplevel(self.master)
            new_window.title(f"View {view_ix+1}")
            self.views.append(new_window)

        self.init_image_windows()

    def create_tools(self, master):
        gcp_list_frame = tk.Frame(master)
        gcp_list_frame.pack(side='top', fill=tk.BOTH, expand=1)
        self.gcp_list_box = CustomListbox(gcp_list_frame,
                                          font=("monospace", 10),
                                          width=10)
        self.gcp_list_box.pack(side='left', expand=tk.YES, fill=tk.Y)
        self.gcp_list_box.bind('<<ListboxSelect>>', self.modify_gcp)

        plus_minus_frame = tk.Frame(master)
        plus_minus_frame.pack(side='top')
        self.add_button = tk.Button(plus_minus_frame,
                                    text="+",
                                    command=self.add_gcp)
        self.add_button.pack(side='left')
        self.remove_button = tk.Button(plus_minus_frame,
                                       text="-",
                                       command=self.remove_gcp)
        self.remove_button.pack(side='left')

        self.if_show_epipolar = tk.IntVar(value=0)
        self.check_button = tk.Checkbutton(master,
                                           text="Epipolar\nlines",
                                           var=self.if_show_epipolar)
        self.check_button.pack(side='top')

        io_frame = tk.Frame(master)
        io_frame.pack(side='top')
        self.load_button = tk.Button(io_frame,
                                     text="Load",
                                     command=self.load_gcps)
        self.load_button.pack(side='top')
        self.save_button = tk.Button(io_frame,
                                     text="Save",
                                     command=self.save_gcps)
        self.save_button.pack(side='top')
        self.save_button = tk.Button(io_frame,
                                     text="Save As",
                                     command=self.save_gcps_as)
        self.save_button.pack(side='top')

    def clear_artists(self, view):
        for artist in view.plt_artists:
            artist.set_visible(False)
            del artist

    def set_title(self, view):
        shot = view.current_image
        seq_key = view.current_sequence
        seq = self.database.seqs[seq_key]
        seq_ix = seq.index(shot)

        if shot in self.shot_std:
            shot_std_rank, shot_std = self.shot_std[shot]
            title = "{} [{}/{}]: {} - #{} (std = {:.2f})".format(
                seq_key, seq_ix + 1, len(seq), shot, shot_std_rank, shot_std)
        else:
            title = "{} [{}/{}]: {}".format(seq_key, seq_ix + 1, len(seq),
                                            shot)
        view.title(title)

    def init_image_windows(self, nav_buttons=False):
        for idx, window in enumerate(self.views):
            nth_viewer_frame = tk.Frame(window)
            nth_viewer_frame.pack(side='left', fill=tk.BOTH, expand=1)

            if nav_buttons:
                button_frame = tk.Frame(nth_viewer_frame)
                button_frame.pack(side='top')
                prv_btn = tk.Button(button_frame,
                                    text=PREVIOUS_UNICODE,
                                    command=lambda _idx=idx: self.
                                    go_to_prev_image(self.views[_idx]))
                prv_btn.pack(side='left')
                nxt_btn = tk.Button(button_frame,
                                    text=NEXT_UNICODE,
                                    command=lambda _idx=idx: self.
                                    go_to_next_image(self.views[_idx]))
                nxt_btn.pack(side='left')

            window.current_sequence = list(self.database.seqs.keys())[idx]
            window.current_image = self.database.seqs[
                window.current_sequence][0]

            window.figure = Figure()
            window.subplot = window.figure.add_subplot(111)
            window.subplot.imshow(self.database.get_image(
                window.current_image),
                                  aspect='auto')
            window.subplot.axis('scaled')
            window.figure.set_tight_layout(True)
            window.subplot.axis('off')

            window.canvas = FigureCanvasTkAgg(window.figure, nth_viewer_frame)
            window.canvas.draw()
            window.canvas.get_tk_widget().pack(side='top',
                                               fill=tk.BOTH,
                                               expand=1)
            window.canvas.mpl_connect('button_press_event',
                                      lambda event: self.on_press(event))
            window.canvas.mpl_connect('scroll_event',
                                      lambda event: self.on_scroll(event))

            window.zoomed_in = False
            window.plt_artists = []
            self.set_title(window)

    def load_shot_std(self, path):
        with open(path, 'r') as f:
            for ix, line in enumerate(f):
                shot, std = line[:-1].split(',')
                self.shot_std[shot] = (ix + 1, float(std))

    def load_gcps(self, filename=None):
        if filename is None:
            filename = filedialog.askopenfilename(
                title="Open GCP file",
                initialdir=self.database.get_path(),
                filetypes=(("JSON files", "*.json"), ("all files", "*.*")),
            )
        if filename is None:
            return
        points = read_gcp_file(filename)
        self.last_saved_filename = filename
        self.database.init_points(points)
        for view_ix, view in enumerate(self.views):
            self.display_points(view)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(view_ix)
        self.repopulate_modify_list()

    def display_points(self, view):
        visible_points_coords = self.database.get_visible_points_coords(
            view.current_image)
        self.clear_artists(view)

        for point_id, coords in visible_points_coords.items():
            color = distinct_colors[divmod(hash(point_id), 19)[1]]
            text_path = TextPath((0, 0), point_id, size=10)
            p1 = PathPatch(text_path,
                           transform=IdentityTransform(),
                           alpha=1,
                           color=color)
            offsetbox2 = AuxTransformBox(IdentityTransform())
            offsetbox2.add_artist(p1)
            ab = AnnotationBbox(offsetbox2,
                                ((coords[0] + 30), (coords[1] + 30)),
                                bboxprops=dict(alpha=0.05))
            circle = mpatches.Circle((coords[0], coords[1]),
                                     10,
                                     color=color,
                                     fill=False)
            dot = mpatches.Circle((coords[0], coords[1]),
                                  2,
                                  color=color,
                                  fill=False)
            for art in (ab, circle, dot):
                view.plt_artists.append(art)
                view.subplot.add_artist(art)

        view.figure.canvas.draw()

    def add_gcp(self):
        new_id = id_generator()
        self.database.add_point(new_id)
        self.curr_point = new_id
        self.gcp_list_box.insert(tk.END, new_id)
        self.gcp_list_box.selection_clear(0, tk.END)
        self.gcp_list_box.selection_set(tk.END)

    def which_canvas(self, event):
        for ix, view in enumerate(self.views):
            if event.canvas == view.canvas:
                return ix
        return None

    def on_scroll(self, event):
        idx = self.which_canvas(event)
        if event.xdata is None or event.ydata is None:
            return
        if event.button == 'up':
            self.go_to_next_image(self.views[idx])
        elif event.button == 'down':
            self.go_to_prev_image(self.views[idx])

    def zoom_in(self, view, x, y):
        xlim = view.subplot.get_xlim()
        width = max(xlim) - min(xlim)
        view.subplot.set_xlim(x - width / 20, x + width / 20)
        view.subplot.set_ylim(y + width / 20, y - width / 20)
        view.zoomed_in = True

    def zoom_out(self, view):
        view.subplot.autoscale()
        view.zoomed_in = False

    def toggle_zoom_on_key(self):
        # Zoom in/out on every view, centered on the location of the current GCP
        if self.curr_point is None:
            return
        any_zoomed_in = any([view.zoomed_in for view in self.views])
        for view in self.views:
            if any_zoomed_in:
                self.zoom_out(view)
            else:
                for projection in self.database.points[self.curr_point]:
                    if projection["shot_id"] == view.current_image:
                        x, y = projection["projection"]
                        self.zoom_in(view, x, y)
            view.canvas.draw_idle()

    def on_press(self, event):
        idx = self.which_canvas(event)
        view = self.views[idx]
        x, y = event.xdata, event.ydata
        current_image = view.current_image
        if None in (x, y):
            return
        if event.button == 2:  # Middle / wheel click:
            if view.zoomed_in:
                self.zoom_out(view)
            else:
                self.zoom_in(view, x, y)
            view.figure.canvas.draw_idle()
        elif self.curr_point is not None and event.button in (1, 3):
            # Left click or right click
            if not self.curr_point:
                return
            self.database.remove_point_observation(self.curr_point,
                                                   current_image)

            for line in self.views[idx].subplot.lines:
                line.remove()
            if event.button == 1:  # Left click
                self.database.add_point_observation(self.curr_point,
                                                    current_image, (x, y))

            self.display_points(view)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(idx)
        else:
            return

    def repopulate_modify_list(self):
        self.gcp_list_box.delete(0, tk.END)
        errors = self.database.compute_gcp_errors()[-1]
        sorted_gcp_ids = sorted(errors, key=lambda k: -errors[k])
        for point_id in sorted_gcp_ids:
            self.gcp_list_box.insert(tk.END, "{}".format(point_id))

    def remove_gcp(self):
        to_be_removed_point = self.curr_point
        if not to_be_removed_point:
            return
        self.curr_point = None

        self.database.remove_gcp(to_be_removed_point)
        for image_idx, view in enumerate(self.views):
            self.display_points(view)
            if self.if_show_epipolar.get():
                self.show_epipolar_lines(image_idx)

        self.repopulate_modify_list()

    def modify_gcp(self, event):
        widget = event.widget
        value = widget.get(int(widget.curselection()[0]))
        self.curr_point = value

    def save_gcps(self):
        if self.last_saved_filename is None:
            return self.save_gcps_as()
        else:
            return self.database.write_to_file(self.last_saved_filename)

    def save_gcps_as(self):
        filename = filedialog.asksaveasfilename(
            initialfile="ground_control_points.json",
            title="Save GCP file",
            initialdir=self.database.get_path(),
            defaultextension=".json",
        )
        if filename is None:
            return
        else:
            self.last_saved_filename = filename
            return self.save_gcps()

    def show_epipolar_lines(self, main_image_idx):
        if len(self.views) > 2:
            raise NotImplementedError("Not implemented yet for >2 views")
        img1 = self.views[0].current_image
        img2 = self.views[1].current_image
        img1_size = self.database.get_image_size(img1)
        img2_size = self.database.get_image_size(img2)
        matched_points = self.database.get_visible_points_coords(
            self.views[main_image_idx].current_image)
        matched_points_coords = convert_tuple_cords_to_list(matched_points)
        matched_points_coords = features.normalized_image_coordinates(
            matched_points_coords, img1_size[1], img1_size[0])
        color_idx = 0
        for point_idx, point in enumerate(matched_points_coords):
            image_pair = [img1, img2]
            line = calc_epipol_line(point, image_pair,
                                    self.database.get_path(), main_image_idx)
            denormalized_lines = features.denormalized_image_coordinates(
                line, img2_size[1], img2_size[0])
            for line_segment in denormalized_lines:
                circle = mpatches.Circle(
                    (line_segment[0], line_segment[1]),
                    3,
                    color=distinct_colors[divmod(
                        hash(list(matched_points.keys())[point_idx]), 19)[1]])
                self.views[main_image_idx].plt_artists.append(circle)
                self.views[not main_image_idx].subplot.add_artist(circle)
            color_idx = color_idx + 1
        self.views[not main_image_idx].figure.canvas.draw_idle()

    def go_to_adjacent_image(self, view, offset):
        views_to_update = set([view])
        target_ix = self.database.get_image_index(
            view.current_image, view.current_sequence) + offset

        for v in self.views:
            for group in self.sequence_groups:
                if view.current_sequence in group:
                    views_to_update.add(v)

        for v in views_to_update:
            new_image = self.database.bring_image(v.current_sequence,
                                                  target_ix)
            self.bring_new_image(new_image, v)

    def go_to_next_image(self, view):
        self.go_to_adjacent_image(view, +1)

    def go_to_prev_image(self, view):
        self.go_to_adjacent_image(view, -1)

    def highlight_gcp_reprojection(self, image_idx, shot, point_id):
        x, y = 0, 0
        for obs in self.database.points[point_id]:
            if obs['shot_id'] == shot:
                x, y = obs['projection']

        x2, y2 = self.database.gcp_reprojections[point_id][shot][
            'reprojection']
        self.views[image_idx].subplot.plot([x, x2], [y, y2], 'r-')
        self.canvases[image_idx].draw_idle()

    def go_to_worst_gcp(self):
        if len(self.database.gcp_reprojections) == 0:
            print("No GCP reprojections available. Can't jump to worst GCP")
            return
        worst_gcp, shot_worst_gcp, worst_gcp_error = self.database.get_worst_gcp(
        )
        for i, sequence_images in enumerate(self.database.seqs.values()):
            if shot_worst_gcp in sequence_images:
                idx_worst_gcp = i
        print("Worst GCP observation: {} in shot {}".format(
            worst_gcp, shot_worst_gcp))

        self.curr_point = worst_gcp
        self.gcp_list_box.selection_clear(0, "end")
        for ix, gcp_id in enumerate(self.gcp_list_box.get(0, "end")):
            if worst_gcp in gcp_id:
                self.gcp_list_box.selection_set(ix)
                break

        self.bring_new_image(shot_worst_gcp, self.views[idx_worst_gcp])
        self.highlight_gcp_reprojection(idx_worst_gcp, shot_worst_gcp,
                                        worst_gcp)

    def bring_new_image(self, new_image, view):
        if new_image == view.current_image:
            return
        t0 = time.time()
        view.current_image = new_image
        view.subplot.clear()
        view.subplot.imshow(self.database.get_image(new_image))
        view.subplot.axis('off')
        self.set_title(view)
        view.zoomed_in = False
        view.subplot.axis('scaled')
        view.figure.set_tight_layout(True)
        view.subplot.axis('off')
        self.display_points(view)
        if self.if_show_epipolar.get():
            for idx, view in enumerate(self.views):
                self.show_epipolar_lines(idx)
        print("Took {:.2f}s to bring_new_image {}".format(
            time.time() - t0, new_image))