Source code for DiatomTrack.gui.editor

import os

import cv2 as cv
import numpy as np
import pandas as pd
import random as rd
import tkinter as tk

from PIL import Image, ImageTk
from sys import exit
from tkinter import ttk, messagebox


[docs]class TrackEditor: """ Main GUI window of the DiatomTrack editor for manual editing and review. """ def __init__(self, tracks, video_path): """ Initialize all window parameters and settings. Parameters ---------- tracks : Pandas dataframe The track data of DiatomTrack. video_path : str Path to the video that is overlayed. Returns ------- None. """ self.tracks = tracks self.tracks_unique = self.tracks.groupby('id').first().sort_values( ['tree', 'frame']) self.video = cv.VideoCapture(video_path) self.playing = False self.overlay = True self.speed = 1. self.fps = self.video.get(cv.CAP_PROP_FPS) self.frame_delay = int(1000 // self.fps) self.frame_num = 0 self.max_frame = min( self.tracks['frame'].max(), int(self.video.get(cv.CAP_PROP_FRAME_COUNT)) - 1) self.frame_seconds = 10 self.rect = None self.rect_coords = None self.rect_x = None self.rect_y = None self.zoomed = False self.drawing = False self.trace = False self.select_highlight = [50, None] self.show_highlight = True self.merge_mode_active = False self.move_mode_active = 0 self.swap_mode_active = False self.current_tree = -1 self.name_to_item = {} self.split_counter = set([self.tracks['id'].max() + 1]) self.tree_counter = set([self.tracks['tree'].max() + 1]) self.tree_limit_current = False self.tree_current_zoom = False self.selected_items_for_move = [] self.show_id = 0 self.show_frustule_age = 0 self.show_cell_age = 0 self.expand = True self.filter_short = False self.no_frustule_flips = 0 self.no_cell_flips = 0 self.no_splits = 0 self.no_merges = 0 self.no_moves = 0 self.no_swaps = 0 _, name = os.path.split(video_path) self.ui_init(name) self.build_tree() self.master.after(0, self.update_canvas) self.master.mainloop()
[docs] def ui_init(self, name): """ Initialize all window widgets. Parameters ---------- name : str Name of the experiment for window naming. Returns ------- None. """ self.master = tk.Tk() self.master.protocol("WM_DELETE_WINDOW", self.close_window) self.master.lift() self.master.focus_force() self.master.state('zoomed') self.time_var = tk.StringVar(value="00:00:00") self.frame_var = tk.IntVar(value=0) self.speed_var = tk.StringVar(value="1.00x") self.tree_var = tk.IntVar(value=-1) self.tree_limit = tk.IntVar(value=self.tracks['frame'].max()) self.master.title(f"DiatomTrack - {name}") self.master.geometry('1920x1080') # video self.canvas = tk.Canvas( self.master, width=1024, height=768, bg='black') self.canvas.grid( row=0, column=0, rowspan=5, columnspan=8, padx=10, pady=5) self.canvas.bind("<ButtonPress-1>", self.start_rectangle) self.canvas.bind("<B1-Motion>", self.draw_rectangle) self.canvas.bind("<ButtonRelease-1>", self.end_rectangle) self.canvas.bind("<Double-3>", self.canvas_double_right) # video scrollbar self.scrollbar_vid = tk.Scale( self.master, from_=0, to=self.max_frame, orient="horizontal", length=1000, command=self.scrub) self.scrollbar_vid.grid(row=5, column=0, columnspan=8, pady=5) # video controls self.video_control = tk.Frame(self.master) self.video_control.grid( row=6, column=0, rowspan=2, columnspan=8, pady=15) self.video_label = tk.Label(self.video_control, text="Video options:") self.video_label.grid(row=0, column=0, columnspan=1, padx=10, pady=5) self.restart_button = ttk.Button( self.video_control, text="Restart", command=self.restart_video) self.restart_button.grid( row=0, column=1, columnspan=1, padx=10, pady=5) self.play_button = ttk.Button( self.video_control, text="Play/Pause", command=self.play_video) self.play_button.grid(row=0, column=2, columnspan=1, padx=10, pady=5) self.frame_label = tk.Label(self.video_control, text="Frame Number:") self.frame_label.grid(row=0, column=3, columnspan=1, padx=10, pady=5) self.frame_entry = ttk.Entry( self.video_control, textvariable=self.frame_var) self.frame_entry.grid(row=0, column=4, columnspan=1, padx=10, pady=5) self.frame_entry.bind("<Return>", self.jump_to_frame) self.time_label = tk.Label(self.video_control, text="Experiment Time:") self.time_label.grid(row=0, column=5, columnspan=1, padx=10, pady=5) self.time_entry = ttk.Entry( self.video_control, textvariable=self.time_var) self.time_entry.grid(row=0, column=6, columnspan=1, padx=10, pady=5) self.time_entry.bind("<Return>", self.jump_to_time) self.slow_button = ttk.Button( self.video_control, text="Slower", command=self.slower_video) self.slow_button.grid(row=1, column=1, columnspan=1, padx=10, pady=5) self.fast_button = ttk.Button( self.video_control, text="Faster", command=self.faster_video) self.fast_button.grid(row=1, column=2, columnspan=1, padx=10, pady=5) self.speed_label = tk.Label( self.video_control, text="Speed multiplier:") self.speed_label.grid(row=1, column=3, columnspan=1, padx=10, pady=5) self.speed_entry = ttk.Entry( self.video_control, textvariable=self.speed_var, state='readonly') self.speed_entry.grid(row=1, column=4, columnspan=1, padx=10, pady=5) # overlay controls self.overlay_control = tk.Frame(self.master) self.overlay_control.grid( row=8, column=0, rowspan=1, columnspan=8, pady=15) self.overlay_label = tk.Label( self.overlay_control, text="Edit video overlay:") self.overlay_label.grid(row=0, column=0, columnspan=1, padx=10, pady=5) self.overlay_button = tk.Button( self.overlay_control, text="On", width=3, relief='sunken', command=self.toggle_overlay) self.overlay_button.grid( row=0, column=1, columnspan=1, padx=10, pady=5) self.trace_button = tk.Button( self.overlay_control, text="Trace", command=self.toggle_trace) self.trace_button.grid( row=0, column=2, columnspan=1, padx=10, pady=5, sticky='w') self.id_label = tk.Label(self.overlay_control, text="Show ID:") self.id_label.grid(row=0, column=3, columnspan=1, padx=10, pady=5) self.id_button = tk.Button( self.overlay_control, text="None", width=4, command=self.toggle_id) self.id_button.grid( row=0, column=4, columnspan=1, padx=10, pady=5, sticky='w') self.tree_button = tk.Button( self.overlay_control, text="Show only tree", command=self.toggle_tree) self.tree_button.grid(row=0, column=5, columnspan=1, padx=10, pady=5) self.tree_field = ttk.Entry( self.overlay_control, textvariable=self.tree_var) self.tree_field.grid(row=0, column=6, columnspan=1, padx=10, pady=5) self.tree_field.bind("<Return>", self.toggle_tree) self.frustule_age_label = tk.Label( self.overlay_control, text="Show frustule age:") self.frustule_age_label.grid( row=0, column=7, columnspan=1, padx=10, pady=5) self.frustule_age_button = tk.Button( self.overlay_control, text="All", relief='sunken', width=8, command=self.toggle_frustule_age) self.frustule_age_button.grid( row=0, column=8, columnspan=1, padx=10, pady=5, sticky='w') self.cell_age_label = tk.Label( self.overlay_control, text="Show cell age:") self.cell_age_label.grid( row=0, column=9, columnspan=1, padx=10, pady=5) self.cell_age_button = tk.Button( self.overlay_control, text="All", relief='sunken', width=8, command=self.toggle_cell_age) self.cell_age_button.grid( row=0, column=10, columnspan=1, padx=10, pady=5, sticky='w') # tree self.tree = ttk.Treeview( self.master, columns=('tree'), show='tree headings') self.tree.heading('#0', text='Tracklet ID') self.tree.heading('tree', text='Tree ID') self.tree.column('#0', width=450) self.tree.column('tree', width=50, anchor='center') self.tree.tag_configure('0', foreground='black') self.tree.tag_configure('1', foreground='blue') self.tree.tag_configure('2', foreground='red') self.tree.grid(row=0, column=8, rowspan=7, columnspan=2, sticky='nsew') tree_scrollbar = tk.Scrollbar( self.master, orient='vertical', command=self.tree.yview) tree_scrollbar.grid(row=0, column=10, rowspan=7, sticky='ns') self.tree.config(yscrollcommand=tree_scrollbar.set) self.tree_control = tk.Frame(self.master) self.tree_control.grid(row=7, column=8, rowspan=2, columnspan=2) self.expand_button = tk.Button( self.tree_control, text="Expand/Collapse", command=self.expand_tree) self.expand_button.grid(row=0, column=0, columnspan=1, padx=10, pady=5) self.track_highlight_button = tk.Button( self.tree_control, text="Highlight track", relief='sunken', command=self.highlight_track) self.track_highlight_button.grid( row=0, column=1, columnspan=1, padx=10, pady=5) self.tree_zoom_button = tk.Button( self.tree_control, text='Show tree in current zoom', command=self.tree_zoom) self.tree_zoom_button.grid( row=0, column=2, columnspan=1, padx=10, pady=5) self.tree_limit_label = tk.Label( self.tree_control, text="Limit tree until frame:") self.tree_limit_label.grid( row=1, column=0, columnspan=1, padx=10, pady=5, sticky='e') self.tree_limit_field = ttk.Entry( self.tree_control, textvariable=self.tree_limit) self.tree_limit_field.grid( row=1, column=1, columnspan=1, padx=10, pady=5) self.tree_limit_field.bind("<Return>", self.tree_set_limit) self.tree_limit_button = tk.Button( self.tree_control, text="Current frame", command=self.tree_current) self.tree_limit_button.grid( row=1, column=2, columnspan=1, padx=10, pady=5) self.listbox = tk.Listbox(self.master, selectmode='single') listbox_scrollbar = tk.Scrollbar( self.master, orient='vertical', command=self.listbox.yview) self.listbox.grid(row=0, column=11, rowspan=5, sticky='nsew') listbox_scrollbar.grid(row=0, column=12, rowspan=5, sticky='ns') self.listbox.config(yscrollcommand=listbox_scrollbar.set) self.removebox = tk.Listbox(self.master, selectmode='single') removebox_scrollbar = tk.Scrollbar( self.master, orient='vertical', command=self.removebox.yview) self.removebox.grid(row=5, column=11, rowspan=4, sticky='nsew') removebox_scrollbar.grid(row=5, column=12, rowspan=4, sticky='ns') self.removebox.config(yscrollcommand=removebox_scrollbar.set) self.tree.bind('<<TreeviewSelect>>', self.tree_select) self.tree.bind("<Double-Button-1>", self.tree_double_click) self.tree.bind("<Double-Button-3>", self.tree_double_right) self.listbox.bind('<Button-3>', self.listbox_right_click) self.listbox.bind("<Double-Button-1>", self.listbox_double_click) # editing buttons self.edits_control = tk.Frame(self.master) self.edits_control.grid( row=0, column=13, rowspan=5, columnspan=1, pady=10, sticky='n') self.flip_frustule_button = tk.Button( self.edits_control, text="Flip frustule age\nat selected frame", width=25, height=4, command=self.flip_frustule) self.flip_frustule_button.grid( row=0, columnspan=1, padx=5, pady=10, sticky='n') self.flip_cell_button = tk.Button( self.edits_control, text="Flip cell age on level", width=25, height=4, command=self.flip_cell) self.flip_cell_button.grid( row=1, columnspan=1, padx=5, pady=10, sticky='n') self.flip_selected_cell_button = tk.Button( self.edits_control, text="Flip selected cell's age", width=25, height=4, command=self.flip_selected_cell) self.flip_selected_cell_button.grid( row=2, columnspan=1, padx=5, pady=10, sticky='n') self.split_button = tk.Button( self.edits_control, text="Split tracklet\nat selected frame", width=25, height=4, command=self.split) self.split_button.grid( row=3, columnspan=1, padx=5, pady=10, sticky='n') self.merge_button = tk.Button( self.edits_control, text="Merge tracklets", width=25, height=4, command=self.toggle_merge) self.merge_button.grid( row=4, columnspan=1, padx=5, pady=10, sticky='n') self.move_button = tk.Button( self.edits_control, text="Move tracklets", width=25, height=4, command=self.toggle_move) self.move_button.grid(row=5, columnspan=1, padx=5, pady=10, sticky='n') self.swap_button = tk.Button( self.edits_control, text='Swap tracklets \nat current frame', width=25, height=4, command=self.swap_tracklets) self.swap_button.grid(row=6, columnspan=1, padx=5, pady=10, sticky='n') self.remove_control = tk.Frame(self.master) self.remove_control.grid( row=5, column=13, rowspan=2, columnspan=1, pady=5, sticky='w') self.remove_button = tk.Button( self.remove_control, text="Remove tracklet", width=25, command=self.remove) self.remove_button.grid( row=0, columnspan=1, padx=5, pady=10, sticky='n') self.restore_button = tk.Button( self.remove_control, text="Restore tracklet", width=25, command=self.restore) self.restore_button.grid( row=1, columnspan=1, padx=5, pady=10, sticky='n') self.toggle_short_button = tk.Button( self.remove_control, text="Remove/Restore short tracklets", width=25, command=self.remove_restore_short) self.toggle_short_button.grid( row=2, columnspan=1, padx=5, pady=10, sticky='n') self.save_button = tk.Button( self.master, text="Save & Exit", fg='red', width=25, command=self.finish) self.save_button.grid( row=8, column=13, columnspan=1, padx=5, pady=5, sticky='s') # bind keys self.master.bind("<plus>", self.faster_video) self.master.bind("<minus>", self.slower_video) self.master.bind("<KP_Add>", self.faster_video) self.master.bind("<KP_Subtract>", self.slower_video) self.master.bind("<space>", self.spacebar_press) self.master.bind("<Left>", self.left_arrow_press) self.master.bind("<Right>", self.right_arrow_press) self.master.bind("<Down>", self.down_arrow_press) self.master.bind("<Up>", self.up_arrow_press) self.master.bind('<Shift_L>', self.shift_overlay) self.master.bind('<Shift_R>', self.shift_overlay) self.master.bind('<Escape>', self.reset_zoom)
[docs] def build_tree(self): """ Build the forest in the tree widget. """ self.tree.delete(*self.tree.get_children()) self.name_to_item.clear() self.removebox.delete(0, tk.END) if self.tree_current_zoom and self.zoomed: df = self.tracks[self.tracks['frame'] == self.frame_num] (x_min, x_max, y_min, y_max) = self.rect_coords df_x_y = df[ (df['x'].between(x_min, x_max)) & (df['y'].between(y_min, y_max))] tracks_unique = self.tracks_unique[ self.tracks_unique['tree'].isin(df_x_y['tree'])] else: tracks_unique = self.tracks_unique for name, row in tracks_unique.iterrows(): if ( row['show'] == 1 and (row['tree'] == self.current_tree or self.current_tree == -1) and row['frame'] <= self.tree_limit.get() ): parent_name = row['parent'] c = int(255 - ((row['tree']+3)*10 % 35)) color = f'#{c:02x}{c:02x}{c:02x}' self.tree.tag_configure(str(row['tree']), background=color) if parent_name == -1: item_id = self.tree.insert( '', 'end', iid=name, text=name, values=int(row['tree']), tags=(str(int(row['age'])),str(row['tree']))) else: parent_item_id = self.name_to_item.get(parent_name) if parent_item_id is not None: item_id = self.tree.insert( parent_item_id, 'end', iid=name, text=name, tags=(str(int(row['age'])),str(row['tree']))) self.name_to_item[name] = item_id elif row['show'] == 0: self.removebox.insert(tk.END, name) if self.expand == True: for item in self.tree.get_children(): self.tree.item(item, open=True) self.expand_tree_children(item)
[docs] def expand_tree_children(self, parent): """ Expand a tree. """ for child in self.tree.get_children(parent): self.tree.item(child, open=True) self.expand_tree_children(child)
[docs] def expand_tree(self): """ Expand all trees. """ self.expand = not self.expand self.build_tree()
[docs] def highlight_track(self): """ Toggle highliting of the selected track in video. """ if self.show_highlight: self.track_highlight_button.config(relief='raised') else: self.track_highlight_button.config(relief='sunken') self.show_highlight = not self.show_highlight
[docs] def tree_current(self): """ Toggle tree to only show up to current frame. """ self.tree_limit_current = not self.tree_limit_current if self.tree_limit_current: self.tree_limit_button.config(relief='sunken') self.tree_limit.set(self.frame_num) self.build_tree() else: self.tree_limit_button.config(relief='raised') try: _ = self.tree_limit.get() except: self.tree_limit.set(self.max_frame) self.build_tree()
[docs] def tree_set_limit(self, *args): """ Limit tree to frame that is set in widget. """ if self.tree_limit_current: self.tree_current() else: try: _ = self.tree_limit.get() except: self.tree_limit.set(self.max_frame) self.build_tree()
[docs] def tree_zoom(self): """ Only show trees in current video zoom. """ if self.tree_current_zoom: self.tree_zoom_button.config(relief='raised') else: self.tree_zoom_button.config(relief='sunken') self.tree_current_zoom = not self.tree_current_zoom self.build_tree()
[docs] def toggle_overlay(self): """ Toggle the drawn overlay in video. """ self.overlay = not self.overlay if self.overlay: self.overlay_button.config(text="On", relief='sunken') else: self.overlay_button.config(text="Off", relief='raised') self.manual_update_canvas()
[docs] def shift_overlay(self, *args): """ Toggle overlay with button press. """ self.toggle_overlay() self.highlight_track()
[docs] def toggle_trace(self): """ Toggle trace overlay for last frames. """ self.trace = not self.trace if not self.trace: self.trace_button.config(relief='raised') else: self.trace_button.config(relief='sunken')
[docs] def toggle_id(self): """ Toggle between shown id for the objects in the frame. """ if self.show_id == 0: self.show_id = 1 self.id_button.config(text="Tree", relief='sunken') elif self.show_id == 1: self.show_id = 2 self.id_button.config(text="Track", relief='sunken') else: self.show_id = 0 self.id_button.config(text="None", relief='raised')
[docs] def toggle_tree(self, *args): """ Toggle between showing only selected tree and all trees. """ if self.current_tree < 0 and self.tree_var.get() != -1: self.tree_button.config(text="Show all trees", relief='sunken') self.current_tree = self.tree_var.get() else: self.tree_button.config(text="Show only tree", relief='raised') self.tree_var.set(-1) self.current_tree = self.tree_var.get() self.build_tree() self.manual_update_canvas()
[docs] def toggle_frustule_age(self): """ Toggle theca age overlay modes. """ if self.show_frustule_age == 0: self.show_frustule_age = 1 self.frustule_age_button.config( text="Older", fg='blue', relief='sunken') elif self.show_frustule_age == 1: self.show_frustule_age = 2 self.frustule_age_button.config( text="Younger", fg='red', relief='sunken') elif self.show_frustule_age == 2: self.show_frustule_age = 3 self.frustule_age_button.config( text="None", fg='black', relief='raised') else: self.show_frustule_age = 0 self.frustule_age_button.config(text="All", relief='sunken')
[docs] def toggle_cell_age(self): """ Toggle cell age overlay modes. """ if self.show_cell_age == 0: self.show_cell_age = 1 self.cell_age_button.config( text="Older", fg='orange', relief='sunken') elif self.show_cell_age == 1: self.show_cell_age = 2 self.cell_age_button.config( text="Younger", fg='orange', relief='sunken') elif self.show_cell_age == 2: self.show_cell_age = 3 self.cell_age_button.config( text="None", fg='black', relief='raised') else: self.show_cell_age = 0 self.cell_age_button.config(text="All", relief='sunken')
[docs] def play_video(self): """ Toggle video playback. """ self.playing = not self.playing self.master.focus()
[docs] def faster_video(self, *args): """ Accelerate video playback. """ self.speed = min(4., self.speed * 2) self.frame_delay = int((1000 // self.fps) / self.speed) self.update_speed_display()
[docs] def slower_video(self, *args): """ Decelerate video playback. """ self.speed = max(0.25, self.speed * 0.5) self.frame_delay = int((1000 // self.fps) / self.speed) self.update_speed_display()
[docs] def update_speed_display(self): """ Update the speed multiplier widget. """ self.speed_var.set(f"{self.speed:.2f}x")
[docs] def restart_video(self): """ Jump to first frame of video. """ self.video.set(cv.CAP_PROP_POS_FRAMES, 0) self.frame_var.set(0) self.jump_to_frame(None)
[docs] def update_frame_number(self, frame): """ Update the frame number widget. """ self.frame_var.set(frame)
[docs] def update_time(self, seconds): """ Update the video time widget. """ hours = seconds // 3600 minutes = (seconds % 3600) // 60 seconds = seconds % 60 self.time_var.set(f"{hours:02}:{minutes:02}:{seconds:02}")
[docs] def scrub(self, val): """ Enable scrubbing of the video scrollbar. """ if int(val) != self.frame_var.get(): self.playing = False self.frame_var.set(int(val)) self.jump_to_frame(None)
[docs] def jump_to_frame(self, *args): """ Jump to a frame in the video and update corresponding widgets and overlay. """ try: self.playing = False frame_num = int(self.frame_entry.get()) if frame_num > self.max_frame: raise ValueError self.frame_num = frame_num self.scrollbar_vid.set(frame_num) self.video.set(cv.CAP_PROP_POS_FRAMES, frame_num) self.update_time(frame_num * self.frame_seconds) self.manual_update_canvas() except ValueError: messagebox.showwarning( "Warning", f"Frame number has to be between 0 and {self.max_frame}.")
[docs] def jump_to_time(self, *args): """ Jump to a timestamp in the video and update corresponding widgets and overlay. """ try: self.playing = False time = self.time_var.get().split(':') seconds = int(time[0])*3600+int(time[1])*60+int(time[2]) frame_num = int(seconds//self.frame_seconds) if frame_num > self.max_frame: raise ValueError self.frame_num = frame_num self.scrollbar_vid.set(int(frame_num)) self.video.set(cv.CAP_PROP_POS_FRAMES, frame_num) self.update_frame_number(frame_num) self.manual_update_canvas() except ValueError: total_seconds = self.max_frame * 10 hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 max_time = f"{hours:02}:{minutes:02}:{seconds:02}" messagebox.showwarning( "Warning", f"Time has to be between 00:00:00 and {max_time}.")
[docs] def canvas_double_right(self, event): """ Select and highlight closest object in canvas upon double right click. """ if self.zoomed: zoom = 2 / float(2048/np.diff(self.rect_coords[:2])) x, y = event.x * zoom, event.y * zoom x += self.rect_coords[0] y += self.rect_coords[2] else: x, y = event.x * 2, event.y * 2 rows = self.tracks[ (self.tracks['frame'] == self.frame_num) & (self.tracks['show'] == 1) ].reset_index() if self.current_tree != -1: rows = rows[rows['tree'] == self.current_tree] distances = np.sqrt((rows['x']-x)**2 + (rows['y']-y)**2) if distances.min() < 30: clicked_id = rows.loc[distances.idxmin(), 'id'] if int(self.tree_limit.get()) < self.frame_num: self.tree_limit.set(self.frame_num) self.build_tree() try: self.tree.selection_set(clicked_id) self.tree.see(clicked_id) except: self.tracks.loc[self.tracks['id'] == clicked_id, 'parent'] = -1 messagebox.showerror( "ID not found in tree.", "An error occurred: selected ID was not found in tree.\ \nRelocated automatically to top level.") self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree() self.tree.selection_set(clicked_id) self.tree.see(clicked_id)
[docs] def start_rectangle(self, event): """ Start drwaing a rectangle as overlay in the canvas upon left click. """ if not self.zoomed: self.rect_coords = None self.drawing = True self.rect_x = event.x self.rect_y = event.y self.rect = self.canvas.create_rectangle( self.rect_x, self.rect_y, self.rect_x, self.rect_y, outline='white')
[docs] def draw_rectangle(self, event): """ Draw rectangle overlay to mark out zoomed area. """ if not self.zoomed: width = event.x - self.rect_x h = event.y - self.rect_y if event.y != self.rect_y else 1 sign = int((h)/abs(h)) if abs(width) < 20: return height = (abs(width) * 3) // 4 x = self.rect_x + width y = self.rect_y + sign * height self.canvas.coords(self.rect, self.rect_x, self.rect_y, x, y) x_min = max(min(self.rect_x, x), 0)*2 x_max = min(max(self.rect_x, x), 1024)*2 y_min = max(min(self.rect_y, y), 0)*2 y_max = min(max(self.rect_y, y), 768)*2 self.rect_coords = (x_min, x_max, y_min, y_max)
[docs] def end_rectangle(self, *args): """ Set dimensions of rectangle and refresh parameters, canvas and tree. """ self.zoomed = not self.zoomed self.drawing = False if self.rect_coords and not ( 0 <= self.rect_coords[0] < self.rect_coords[1] <= 2048 and 0 <= self.rect_coords[2] < self.rect_coords[3] <= 2048): self.zoomed = False self.rect_coords = None if self.tree_current_zoom and self.rect_coords: self.build_tree() if self.rect: self.canvas.delete(self.rect) self.rect = None
[docs] def apply_zoom(self, image): """ Apply zoom on current frame. """ if not self.rect_coords: self.zoomed = False return image x_min, x_max, y_min, y_max = self.rect_coords cropped_image = image[y_min:y_max, x_min:x_max] return cropped_image
[docs] def reset_zoom(self, *args): """ Reset video zoom and rebuild tree. """ self.zoomed = False self.build_tree()
[docs] def update_canvas(self): """ Update video canvas and associated parameters if not currently drawing rectangle and video is still playing. """ if not self.drawing: if self.playing and not self.frame_num == self.max_frame: ret, frame = self.video.read() if ret: self.frame_num = int( self.video.get(cv.CAP_PROP_POS_FRAMES)) - 1 frame_num = self.frame_num self.update_frame_number(frame_num) self.update_time(frame_num * self.frame_seconds) self.scrollbar_vid.set(frame_num) if self.tree_limit_current: self.tree_limit.set(frame_num) self.build_tree() if self.overlay: frame = self.draw_overlay(frame) image = cv.cvtColor(frame, cv.COLOR_BGR2RGB) if self.zoomed: image = self.apply_zoom(image) resized_image = cv.resize(image, (1024, 768)) img = Image.fromarray(resized_image) imgtk = ImageTk.PhotoImage(image=img) self.canvas.create_image(0, 0, anchor=tk.NW, image=imgtk) self.canvas.imgtk = imgtk else: self.manual_update_canvas() self.master.after(self.frame_delay, self.update_canvas)
[docs] def manual_update_canvas(self): """ Update video canvas for cases not covered by update_canvas / manual interventions. """ frame_num = self.frame_num self.video.set(cv.CAP_PROP_POS_FRAMES, frame_num) ret, frame = self.video.read() if ret: if self.overlay: frame = self.draw_overlay(frame) image = cv.cvtColor(frame, cv.COLOR_BGR2RGB) if self.zoomed: image = self.apply_zoom(image) resized_image = cv.resize(image, (1024, 768)) img = Image.fromarray(resized_image) imgtk = ImageTk.PhotoImage(image=img) self.canvas.create_image(0, 0, anchor=tk.NW, image=imgtk) self.canvas.imgtk = imgtk if self.frame_num == self.max_frame: self.playing = False
[docs] def draw_overlay(self, image): """ Draw the corresponding overlay on the current video frame for all activated information. """ image_tracks = self.tracks[self.tracks['frame']==self.frame_num] selected_item = self.tree.selection() if selected_item: try: item = int(*selected_item) except: messagebox.showerror( 'Error', 'Multiple items in tree selected.') self.overlay = False return image if self.current_tree == -1: points = image_tracks[[ 'x', 'y', 'side', 'width', 'height', 'show', 'tree', 'id', 'age', 'parent' ]].values.astype(float) else: points = image_tracks[image_tracks['tree']==self.current_tree][[ 'x', 'y', 'side', 'width', 'height', 'show', 'tree', 'id', 'age', 'parent' ]].values.astype(float) for p in points: if p[-5] == 1: if self.trace: t_points = self.find_trace_line_points(p[-3]) for tp in t_points: cv.line(image, tp[0], tp[1], tp[-1], thickness=2) if p[-1] != -1: old, new = self.find_age_line_points(p) if self.show_frustule_age < 2: cv.line(image, old[0], old[1], (255, 0, 0), 2) if ( self.show_frustule_age == 2 or self.show_frustule_age == 0 ): cv.line(image, new[0], new[1], (0, 0, 255), 2) elif self.show_frustule_age == 0: old, new = self.find_age_line_points(p) cv.line(image, old[0], old[1], (0, 255, 0), 2) cv.line(image, new[0], new[1], (0, 255, 0), 2) if p[-2] == self.show_cell_age and self.show_cell_age > 0: cv.circle( image, (int(p[0]), int(p[1])), 4, (255, 165, 0), -1) elif self.show_cell_age == 0: if p[-2] == 0: cv.circle( image, (int(p[0]), int(p[1])), 4, (255, 255, 0), -1) if p[-2] > 0: cv.circle( image, (int(p[0]), int(p[1])), 4, (255, 165, 0), -1) elif self.show_cell_age > 2: cv.circle(image, (int(p[0]), int(p[1])), 3, (0, 0, 0), -1) if self.show_id > 0: if self.show_id == 1: text = str(int(p[-4])) else: text = str(int(p[-3])) position = (int(p[0])-15, int(p[1])-15) cv.putText( image, text, position, cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv.LINE_AA) if selected_item and self.show_highlight: if p[-3] == item and self.select_highlight[0] > 15: cv.circle( image, (int(p[0]), int(p[1])), self.select_highlight[0], (160, 105, 255), 3) self.select_highlight[0] -= 3 return image
[docs] def find_age_line_points(self, row): """ Find end points for drawing the side / age / theca marker lines of the cells. """ x, y, rot, l1, l2, _, _, _, _, parent = row w = min(l1,l2) h = max(l1,l2) offset_x = -np.sin(rot) * w/2 offset_y = np.cos(rot) * w/2 ortho_x = np.cos(rot) * h/2 ortho_y = np.sin(rot) * h/2 start_old = (int(x + offset_x + ortho_x), int(y + offset_y + ortho_y)) end_old = (int(x + offset_x - ortho_x), int(y + offset_y - ortho_y)) start_new = (int(x - offset_x + ortho_x), int(y - offset_y + ortho_y)) end_new = (int(x - offset_x - ortho_x), int(y - offset_y - ortho_y)) return (start_old, end_old), (start_new, end_new)
[docs] def find_trace_line_points(self, item): """ Find the line points of the long axis to draw the trace of an object for the last 10 frames' position. """ rows = self.tracks.loc[ pd.IndexSlice[self.frame_num-10:self.frame_num-1, :]] rows = rows[rows['id'] == item].tail(4) data = rows[['x', 'y', 'height', 'width', 'side']].values.astype(float) color = self.get_color(item) points = [] for det in data: x, y, h, w, rot = det l = min(h, w) / 2 x, y = int(x), int(y) x_start = int(x - l * np.cos(rot)) y_start = int(y - l * np.sin(rot)) x_end = int(x + l * np.cos(rot)) y_end = int(y + l * np.sin(rot)) points.append(((x_start, y_start), (x_end, y_end), color)) return points
[docs] def get_color(self, item): """ Get a random RGB tuple from an objects id. """ rd.seed(item) return tuple(rd.randint(30,225) for _ in range(3))
[docs] def tree_select(self, *args): """ Populate list box with frames upon selecting a tree item and highlight the object in the video frame if applicable. """ selected_item = self.tree.selection() if ( self.merge_mode_active or self.move_mode_active == 1 or self.swap_mode_active ): if selected_item: item_text = self.tree.item(selected_item[0], 'text') if item_text not in self.listbox.get(0, tk.END): self.listbox.insert(tk.END, item_text) else: self.listbox.delete(0, tk.END) if selected_item: item_id = selected_item[0] item_text = self.tree.item(item_id, 'text') filtered_numbers = self.tracks[ self.tracks['id'] == item_text]['frame'] index = None for i, number in enumerate(filtered_numbers): self.listbox.insert(tk.END, number) if number == self.frame_num: index = i if index is not None: self.listbox.selection_set(index) self.listbox.activate(index) self.listbox.see(index) if selected_item: item = int(*selected_item) self.select_highlight = [50, item]
[docs] def tree_double_click(self, *args): """ Show only tree of double clicked item in overlay and forest. """ selected_item = self.tree.selection() if selected_item: item = self.tree.item(selected_item[0], 'text') tree_id = self.tracks.loc[ self.tracks['id'] == item, 'tree'].iloc[0] self.tree_var.set(tree_id) self.toggle_tree() self.build_tree()
[docs] def tree_double_right(self, *args): """ Reset selected tree and rebuild forest. """ self.tree_var.set(-1) self.toggle_tree() self.build_tree()
[docs] def listbox_right_click(self, *args): """ Remove selected item from listbox when moving tracks in the forest. """ if ( self.merge_mode_active or self.move_mode_active != 0 or self.swap_mode_active ): selected_index = self.listbox.curselection() if selected_index: self.listbox.delete(selected_index[0])
[docs] def listbox_double_click(self, *args): """ Jump to double clicked frame in standard listbox view. """ if ( not self.merge_mode_active and self.move_mode_active == 0 and not self.swap_mode_active ): selected_index = self.listbox.curselection() if selected_index: selected_number = self.listbox.get(selected_index[0]) self.update_frame_number(selected_number) self.jump_to_frame(None)
[docs] def remove(self): """ Remove items from tree by setting 'show' column to 0 in the dataframe and display them in the sparate deleted listbox. """ selected_item = self.tree.selection() if selected_item: item_text = self.tree.item(selected_item[0], 'text') if item_text not in self.tracks['parent'].values: self.tracks.loc[self.tracks['id'] == item_text, 'show'] = 0 elif messagebox.askyesno( "Warning", "Tracklet is not childless. Remove all?"): child_id = self.tracks[ self.tracks['parent'] == item_text]['id'].unique().tolist() remove_ids = [item_text] while len(child_id) > 0: remove_ids.extend(child_id) child_id = self.tracks[ self.tracks['parent'].isin(child_id) ]['id'].unique().tolist() self.tracks.loc[self.tracks['id'].isin(remove_ids), 'show'] = 0 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def restore(self): """ Restore elements that were removed from the tree by setting the 'show' column to 1 in the dataframe. """ selected_index = self.removebox.curselection() if selected_index: item_text = self.removebox.get(selected_index[0]) if item_text not in self.tracks['parent'].values: self.tracks.loc[self.tracks['id'] == item_text, 'show'] = 1 elif messagebox.askyesno( "Warning", "Tracklet is not childless. Restore all?"): child_id = self.tracks[ self.tracks['parent'] == item_text]['id'].unique().tolist() remove_ids = [item_text] while len(child_id) > 0: remove_ids.extend(child_id) child_id = self.tracks[ self.tracks['parent'].isin(child_id) ]['id'].unique().tolist() self.tracks.loc[self.tracks['id'].isin(remove_ids), 'show'] = 1 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def remove_restore_short(self): """ Automatically remove or restore all tracks that are short (len==1). """ self.filter_short = not self.filter_short unique = self.tracks['id'].value_counts() unique_occurence = unique[unique == 1].index.tolist() if not self.filter_short: self.tracks.loc[ self.tracks['id'].isin(unique_occurence), 'show'] = 1 else: self.tracks.loc[ self.tracks['id'].isin(unique_occurence), 'show'] = 0 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def spacebar_press(self, *args): """ Start / stop video playback upon spacebar press. """ self.play_video()
[docs] def left_arrow_press(self, *args): """ Jump video backward upon left arrow press. """ jump = int(min(self.max_frame*.01, 10)) new_frame = max(self.frame_num - jump, 0) self.update_frame_number(new_frame) self.jump_to_frame(None)
[docs] def right_arrow_press(self, *args): """ Jump video forward upon right arrow press. """ jump = int(min(self.max_frame*.01, 10)) new_frame = min(self.frame_num + jump, self.max_frame) self.update_frame_number(new_frame) self.jump_to_frame(None)
[docs] def down_arrow_press(self, *args): """ Jump one frame backward upon down arrow press. """ new_frame = max(self.frame_num - 1, 0) self.update_frame_number(new_frame) self.jump_to_frame(None)
[docs] def up_arrow_press(self, *args): """ Jump one frame forward upon up arrow press. """ new_frame = min(self.frame_num + 1, self.max_frame) self.update_frame_number(new_frame) self.jump_to_frame(None)
[docs] def split(self): """ Split selected tree item / track at selected frame in listbox. """ selected_index = self.listbox.curselection() if selected_index: selected_number = self.listbox.get(selected_index[0]) if selected_number is not None: selected_item = self.tree.selection() if not selected_item: return selected_name = self.tree.item(selected_item[0], 'text') if not ( len(self.tracks[self.tracks['id'] == selected_name]) > 1 ): return self.no_splits += 1 new_name = min(self.split_counter) self.split_counter.remove(new_name) if len(self.split_counter) == 0: self.split_counter.add(new_name+1) self.tracks.loc[ (self.tracks['id'] == selected_name) & (self.tracks['frame'] < selected_number), 'division'] = selected_number - 1 self.tracks.loc[ (self.tracks['id'] == selected_name) & (self.tracks['frame'] >= selected_number), 'start'] = selected_number self.tracks.loc[ (self.tracks['id'] == selected_name) & (self.tracks['frame'] >= selected_number), 'id'] = new_name if self.tracks.loc[ self.tracks['id'] == new_name, 'parent'].iloc[0] == -1: new_tree = min(self.tree_counter) self.tree_counter.remove(new_tree) self.tracks.loc[ self.tracks['id'] == new_name, 'tree'] = new_tree if len(self.tree_counter) == 0: self.tree_counter.add(self.tracks['tree'].max() + 1) self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree() self.listbox.delete(0, tk.END) selected_number = None
[docs] def toggle_merge(self): """ Toggle merge mode of tree items. """ self.merge_mode_active = not self.merge_mode_active if self.merge_mode_active: self.merge_button.config( text="Select tracklets\n(Click to Finish)", relief='sunken') self.listbox.delete(0, tk.END) else: self.merge_button.config(text="Merge tracklets", relief='raised') self.merge() self.listbox.delete(0, tk.END)
[docs] def merge(self): """ Merge selected tree items if not occuring in the same frame. """ selected_items = self.listbox.get(0, tk.END) if len(selected_items) < 2: return self.no_merges += 1 min_item_parent = None selected_items_set = set(selected_items) filtered_tracks = self.tracks[self.tracks['id'].isin(selected_items)] duplicates = filtered_tracks[ filtered_tracks['frame'].duplicated(keep=False)] if not duplicates.empty: if not messagebox.askyesno( "Confirm", f"The merged branches occur more than once in a frame.\ First frame: {duplicates['frame'].iloc[0]}. Continue?" ): return min_start = filtered_tracks['start'].min() div = -1 if -1 in filtered_tracks['division'] else filtered_tracks[ 'division'].max() min_start_rows = filtered_tracks[filtered_tracks['start'] == min_start] min_item_parent = min_start_rows.iloc[0]['parent'] min_item_id = min_start_rows.iloc[0]['id'] min_item_tree = min_start_rows.iloc[0]['tree'] min_item_age = min_start_rows.iloc[0]['age'] selected_items_set.remove(min_item_id) self.split_counter.update(selected_items_set) self.tracks.loc[ self.tracks['parent'].isin(selected_items), 'parent'] = min_item_id self.tracks.loc[ self.tracks['id'].isin(selected_items), 'id'] = min_item_id self.tracks.loc[ self.tracks['id'].isin(selected_items), 'tree'] = min_item_tree self.tracks.loc[ self.tracks['id'].isin(selected_items), 'parent'] = min_item_parent self.tracks.loc[ self.tracks['id'].isin(selected_items), 'division'] = div self.tracks.loc[ self.tracks['id'].isin(selected_items), 'start'] = min_start self.tracks.loc[ self.tracks['id'].isin(selected_items), 'age'] = min_item_age child_id = self.tracks[ self.tracks['parent'].isin(selected_items)]['id'].unique().tolist() while len(child_id) > 0: self.tracks.loc[ self.tracks['id'].isin(child_id), 'tree'] = min_item_tree child_id = self.tracks[ self.tracks['parent'].isin(child_id)]['id'].unique().tolist() self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def toggle_move(self): """ Move tracks within a tree or the forest if not overlapping. """ if self.move_mode_active == 2: self.move_button.config(text="Move tracklets", relief='raised') self.listbox.delete(0, tk.END) selected_tree_item = self.tree.selection() new_parent = self.tree.item( selected_tree_item[0], 'text') if selected_tree_item else -1 if new_parent != -1: parent_frames = list(range( self.tracks[ self.tracks['id'] == new_parent ]['frame'].values[-1] + 1)) overlap = self.tracks[ self.tracks['frame'].isin(parent_frames) & self.tracks['id'].isin(self.selected_items_for_move)] if not overlap.empty: values = overlap['id'].unique() messagebox.showwarning( "Overlap found", f"Tracklets are overlapping: {new_parent} \ and {', '.join(map(str, values))}") self.move_mode_active = 0 self.selected_items_for_move.clear() return old_tree = [] for item in self.selected_items_for_move: self.no_moves += 1 self.tracks.loc[ self.tracks['id'] == item, 'parent'] = new_parent old_tree.append( self.tracks[self.tracks['id'] == item]['tree'].values[0]) if new_parent != -1: new_tree = self.tracks[ self.tracks['id'] == new_parent]['tree'].values[0] else: new_tree = min(self.tree_counter) self.tree_counter.remove(new_tree) self.tracks.loc[ self.tracks['id'].isin(self.selected_items_for_move), 'tree' ] = new_tree child_id = self.tracks[ self.tracks['parent'].isin(self.selected_items_for_move) ]['id'].unique().tolist() while len(child_id) > 0: self.tracks.loc[ self.tracks['id'].isin(child_id), 'tree'] = new_tree child_id = self.tracks[ self.tracks['parent'].isin(child_id) ]['id'].unique().tolist() self.tree_counter |= set(old_tree) - set(self.tracks['tree']) if len(self.tree_counter) == 0: self.tree_counter.add(new_tree+1) self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree() self.move_mode_active = 0 self.selected_items_for_move.clear() elif self.move_mode_active == 1: self.move_button.config( text="Select destination\n(default: top level)", relief='sunken') self.selected_items_for_move = list(self.listbox.get(0, tk.END)) self.tree.selection_remove(self.tree.selection()) self.listbox.delete(0, tk.END) self.move_mode_active = 2 elif self.move_mode_active == 0: self.move_button.config(text="Choose tracklets", relief='sunken') self.listbox.delete(0, tk.END) self.selected_items_for_move.clear() self.tree.selection_remove(self.tree.selection()) self.move_mode_active = 1
[docs] def swap_tracklets(self): """ Swap two selected tracks at the selected frame (assign everything after the frame to the other). """ self.swap_mode_active = not self.swap_mode_active if self.swap_mode_active: self.listbox.delete(0, tk.END) self.swap_button.config( text='Select two tracklets', relief='sunken') else: selected_items = self.listbox.get(0, tk.END) self.swap_button.config( text='Swap tracklets \nat current frame', relief='raised') self.listbox.delete(0, tk.END) if len(selected_items) != 2: return id1 = selected_items[0] id2 = selected_items[1] ids = [id1, id2] if self.tracks[ self.tracks['id'].isin(ids)]['parent'].isin(ids).any(): if messagebox.askyesno( "Swapping overlap", "Swapping child and parent may lead to errors.\ \nChange hierarchy to proceed?"): for idx, i in enumerate(ids): if self.tracks[ self.tracks['id'] == i ]['parent'].isin([ids[idx-1]]).any(): self.tracks.loc[ self.tracks['id'] == i, 'parent'] = -1 return loc1 = self.tracks.index[ (self.tracks['frame'] >= self.frame_num) & (self.tracks['id'] == id1)] loc2 = self.tracks.index[ (self.tracks['frame'] >= self.frame_num) & (self.tracks['id'] == id2)] row1 = self.tracks[self.tracks['id'] == id1].iloc[0] row2 = self.tracks[self.tracks['id'] == id2].iloc[0] tree1 = row1['tree'] tree2 = row2['tree'] parent1 = row1['parent'] parent2 = row2['parent'] start1 = row1['start'] start2 = row2['start'] division1 = row1['division'] division2 = row2['division'] age1 = row1['age'] age2 = row2['age'] self.tracks.loc[ loc1, ['id', 'tree', 'parent', 'start', 'age'] ] = [id2, tree2, parent2, start2, age2] self.tracks.loc[ loc2, ['id', 'tree', 'parent', 'start', 'age'] ] = [id1, tree1, parent1, start1, age1] self.tracks.loc[self.tracks['id'] == id1, 'division'] = division2 self.tracks.loc[self.tracks['id'] == id2, 'division'] = division1 child_id1 = self.tracks[ self.tracks['parent'] == id1]['id'].unique().tolist() child_id2 = self.tracks[ self.tracks['parent'] == id2]['id'].unique().tolist() self.tracks.loc[self.tracks['id'].isin(child_id1), 'parent'] = id2 self.tracks.loc[self.tracks['id'].isin(child_id2), 'parent'] = id1 while len(child_id1) > 0: self.tracks.loc[ self.tracks['id'].isin(child_id1), 'tree'] = tree2 child_id1 = self.tracks[ self.tracks['parent'].isin(child_id1) ]['id'].unique().tolist() while len(child_id2) > 0: self.tracks.loc[ self.tracks['id'].isin(child_id2), 'tree'] = tree1 child_id2 = self.tracks[ self.tracks['parent'].isin(child_id2) ]['id'].unique().tolist() self.no_swaps += 1 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def flip_frustule(self): """ Flip the thecae / side assignment for the selected tree item. """ if self.merge_mode_active or self.move_mode_active != 0: return selected_item = self.tree.selection() selected_index = self.listbox.curselection() if selected_index: selected_number = self.listbox.get(selected_index[0]) if not selected_item or selected_number is None: return selected_name = self.tree.item(selected_item[0], 'text') mask = ( (self.tracks['id'] == selected_name) & (self.tracks['frame'] >= selected_number)) self.tracks.loc[mask, 'side'] = ( self.tracks.loc[mask, 'side'] + np.pi).mod(2*np.pi) self.no_frustule_flips += 1 parent_id = self.tree.parent(selected_name) if not self.tree.parent(parent_id): return children = self.tree.get_children(selected_name) if len(children) > 2: messagebox.showwarning( "Warning", "The selected tree item has more than two children.") children = [self.tree.item(c, 'text') for c in children] self.tracks.loc[ self.tracks['id'].isin(children), 'age' ] = self.tracks.loc[ self.tracks['id'].isin(children), 'age'].replace({1: 2, 2: 1}) self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def flip_cell(self): """ Flip assigned age of selected track's / cell's children if applicable. """ if self.merge_mode_active or self.move_mode_active != 0: return selected_item = self.tree.selection() if not selected_item: return selected_name = selected_item[0] parent_id = self.tree.parent(selected_name) if not self.tree.parent(parent_id): return children = self.tree.get_children(parent_id) if not len(children) == 2 and messagebox.askyesno( "Confirm", "This parent does not have two children.\ Flip only selected one?"): self.tracks.loc[ self.tracks['id'] == selected_name, 'age' ] = self.tracks.loc[ self.tracks['id'] == selected_name, 'age' ].replace({0: 1, 1: 2, 2: 0}) else: track_ids = [self.tree.item(child, 'text') for child in children] self.tracks.loc[ self.tracks['id'].isin(track_ids), 'age' ] = self.tracks.loc[ self.tracks['id'].isin(track_ids), 'age'].replace({1: 2, 2: 1}) self.no_cell_flips += 1 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def flip_selected_cell(self): """ Flip assigned age of the selected cell / track. """ if self.merge_mode_active or self.move_mode_active != 0: return selected_item = self.tree.selection() if not selected_item: return selected_name = int(selected_item[0]) self.tracks.loc[ self.tracks['id'] == selected_name, 'age' ] = self.tracks.loc[ self.tracks['id'] == selected_name, 'age' ].replace({0: 1, 1: 2, 2: 0}) self.no_cell_flips += 1 self.tracks_unique = self.tracks.groupby( 'id').first().sort_values(['tree', 'frame']) self.build_tree()
[docs] def finish(self): """ Close window to progress orderly to the next step. """ if messagebox.askyesno( "Confirm", "Have all trees been cleared of tracklet overlaps or\ excess child branches?\ \nThis will not be checked automatically."): self.master.destroy() self.video.release()
[docs] def close_window(self): """ Confirm abrupt exit. """ if messagebox.askyesno( "Confirm Exit", "Are you sure you want to exit without saving?"): self.master.destroy() self.video.release() self.window_exception()
[docs] def window_exception(self): """ Exit immediately. """ messagebox.showerror( "Window closed.", "Window was closed unexpectedly. The program terminates now.") exit()