import os, threading, time, sqlite3, webbrowser, pyautogui
import tkinter as tk
from tkinter import ttk
import tkinter.font as tkFont
from tkinter import filedialog
from tkinter import font
from queue import Queue
from tkinter import Label, Frame, Button
import numpy as np
from PIL import Image, ImageOps, ImageTk
from concurrent.futures import ThreadPoolExecutor
from skimage.exposure import rescale_intensity
from IPython.display import display, HTML
import imageio.v2 as imageio
from collections import deque
from skimage.draw import polygon, line
from skimage.transform import resize
from scipy.ndimage import binary_fill_holes, label
from tkinter import ttk, scrolledtext
fig = None
[docs]
def set_element_size():
screen_width, screen_height = pyautogui.size()
screen_area = screen_width * screen_height
# Calculate sizes based on screen dimensions
btn_size = int((screen_area * 0.002) ** 0.5) # Button size as a fraction of screen area
bar_size = screen_height // 20 # Bar size based on screen height
settings_width = screen_width // 4 # Settings panel width as a fraction of screen width
panel_width = screen_width - settings_width # Panel width as a fraction of screen width
panel_height = screen_height // 6 # Panel height as a fraction of screen height
size_dict = {
'btn_size': btn_size,
'bar_size': bar_size,
'settings_width': settings_width,
'panel_width': panel_width,
'panel_height': panel_height
}
return size_dict
[docs]
def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font_family="OpenSans", font_size=12, bg_color='black', fg_color='white', active_color='blue', inactive_color='dark_gray'):
if active_color == 'teal':
active_color = '#008080'
if inactive_color == 'dark_gray':
inactive_color = '#2B2B2B' # '#333333' #'#050505'
if bg_color == 'black':
bg_color = '#000000'
if fg_color == 'white':
fg_color = '#ffffff'
if active_color == 'blue':
active_color = '#007BFF'
padding = '5 5 5 5'
font_style = tkFont.Font(family=font_family, size=font_size)
if font_family == 'OpenSans':
font_loader = spacrFont(font_name='OpenSans', font_style='Regular', font_size=12)
else:
font_loader = None
style.theme_use('clam')
style.configure('TEntry', padding=padding)
style.configure('TCombobox', padding=padding)
style.configure('Spacr.TEntry', padding=padding)
style.configure('TEntry', padding=padding)
style.configure('Spacr.TEntry', padding=padding)
style.configure('Custom.TLabel', padding=padding)
style.configure('TButton', padding=padding)
style.configure('TFrame', background=bg_color)
style.configure('TPanedwindow', background=bg_color)
if font_loader:
style.configure('TLabel', background=bg_color, foreground=fg_color, font=font_loader.get_font(size=font_size))
else:
style.configure('TLabel', background=bg_color, foreground=fg_color, font=(font_family, font_size))
if parent_frame:
parent_frame.configure(bg=bg_color)
parent_frame.grid_rowconfigure(0, weight=1)
parent_frame.grid_columnconfigure(0, weight=1)
if containers:
for container in containers:
if isinstance(container, ttk.Frame):
container_style = ttk.Style()
container_style.configure(f'{container.winfo_class()}.TFrame', background=bg_color)
container.configure(style=f'{container.winfo_class()}.TFrame')
else:
container.configure(bg=bg_color)
if widgets:
for widget in widgets:
if isinstance(widget, (tk.Label, tk.Button, tk.Frame, ttk.LabelFrame, tk.Canvas)):
widget.configure(bg=bg_color)
if isinstance(widget, (tk.Label, tk.Button)):
if font_loader:
widget.configure(fg=fg_color, font=font_loader.get_font(size=font_size))
else:
widget.configure(fg=fg_color, font=(font_family, font_size))
if isinstance(widget, scrolledtext.ScrolledText):
widget.configure(bg=bg_color, fg=fg_color, insertbackground=fg_color)
if isinstance(widget, tk.OptionMenu):
if font_loader:
widget.configure(bg=bg_color, fg=fg_color, font=font_loader.get_font(size=font_size))
else:
widget.configure(bg=bg_color, fg=fg_color, font=(font_family, font_size))
menu = widget['menu']
if font_loader:
menu.configure(bg=bg_color, fg=fg_color, font=font_loader.get_font(size=font_size))
else:
menu.configure(bg=bg_color, fg=fg_color, font=(font_family, font_size))
return {'font_loader':font_loader, 'font_family': font_family, 'font_size': font_size, 'bg_color': bg_color, 'fg_color': fg_color, 'active_color': active_color, 'inactive_color': inactive_color}
[docs]
class spacrFont:
def __init__(self, font_name, font_style, font_size=12):
"""
Initializes the FontLoader class.
Parameters:
- font_name: str, the name of the font (e.g., 'OpenSans').
- font_style: str, the style of the font (e.g., 'Regular', 'Bold').
- font_size: int, the size of the font (default: 12).
"""
self.font_name = font_name
self.font_style = font_style
self.font_size = font_size
# Determine the path based on the font name and style
self.font_path = self.get_font_path(font_name, font_style)
# Register the font with Tkinter
self.load_font()
[docs]
def get_font_path(self, font_name, font_style):
"""
Returns the font path based on the font name and style.
Parameters:
- font_name: str, the name of the font.
- font_style: str, the style of the font.
Returns:
- str, the path to the font file.
"""
base_dir = os.path.dirname(__file__)
if font_name == 'OpenSans':
if font_style == 'Regular':
return os.path.join(base_dir, 'resources/font/open_sans/static/OpenSans-Regular.ttf')
elif font_style == 'Bold':
return os.path.join(base_dir, 'resources/font/open_sans/static/OpenSans-Bold.ttf')
elif font_style == 'Italic':
return os.path.join(base_dir, 'resources/font/open_sans/static/OpenSans-Italic.ttf')
# Add more styles as needed
# Add more fonts as needed
raise ValueError(f"Font '{font_name}' with style '{font_style}' not found.")
[docs]
def load_font(self):
"""
Loads the font into Tkinter.
"""
try:
font.Font(family=self.font_name, size=self.font_size)
except tk.TclError:
# Load the font manually if it's not already loaded
self.tk_font = font.Font(
name=self.font_name,
file=self.font_path,
size=self.font_size
)
[docs]
def get_font(self, size=None):
"""
Returns the font in the specified size.
Parameters:
- size: int, the size of the font (optional).
Returns:
- tkFont.Font object.
"""
if size is None:
size = self.font_size
return font.Font(family=self.font_name, size=size)
[docs]
class spacrContainer(tk.Frame):
def __init__(self, parent, orient=tk.VERTICAL, bg=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.orient = orient
self.bg = bg if bg else 'lightgrey'
self.sash_thickness = 10
self.panes = []
self.sashes = []
self.bind("<Configure>", self.on_configure)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
[docs]
def add(self, widget, stretch='always'):
print(f"Adding widget: {widget} with stretch: {stretch}")
pane = tk.Frame(self, bg=self.bg)
pane.grid_propagate(False)
widget.grid(in_=pane, sticky="nsew") # Use grid for the widget within the pane
self.panes.append((pane, widget))
if len(self.panes) > 1:
self.create_sash()
self.reposition_panes()
[docs]
def create_sash(self):
sash = tk.Frame(self, bg=self.bg, cursor='sb_v_double_arrow' if self.orient == tk.VERTICAL else 'sb_h_double_arrow', height=self.sash_thickness, width=self.sash_thickness)
sash.bind("<Enter>", self.on_enter_sash)
sash.bind("<Leave>", self.on_leave_sash)
sash.bind("<ButtonPress-1>", self.start_resize)
self.sashes.append(sash)
[docs]
def reposition_panes(self):
if not self.panes:
return
total_size = self.winfo_height() if self.orient == tk.VERTICAL else self.winfo_width()
pane_size = total_size // len(self.panes)
print(f"Total size: {total_size}, Pane size: {pane_size}, Number of panes: {len(self.panes)}")
for i, (pane, widget) in enumerate(self.panes):
if self.orient == tk.VERTICAL:
pane.grid(row=i * 2, column=0, sticky="nsew", pady=(0, self.sash_thickness if i < len(self.panes) - 1 else 0))
else:
pane.grid(row=0, column=i * 2, sticky="nsew", padx=(0, self.sash_thickness if i < len(self.panes) - 1 else 0))
for i, sash in enumerate(self.sashes):
if self.orient == tk.VERTICAL:
sash.grid(row=(i * 2) + 1, column=0, sticky="ew")
else:
sash.grid(row=0, column=(i * 2) + 1, sticky="ns")
[docs]
def on_enter_sash(self, event):
event.widget.config(bg='blue')
[docs]
def on_leave_sash(self, event):
event.widget.config(bg=self.bg)
[docs]
def start_resize(self, event):
sash = event.widget
self.start_pos = event.y_root if self.orient == tk.VERTICAL else event.x_root
self.start_size = sash.winfo_y() if self.orient == tk.VERTICAL else sash.winfo_x()
sash.bind("<B1-Motion>", self.perform_resize)
[docs]
class spacrEntry(tk.Frame):
def __init__(self, parent, textvariable=None, outline=False, width=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# Set dark style
style_out = set_dark_style(ttk.Style())
self.bg_color = style_out['inactive_color']
self.active_color = style_out['active_color']
self.fg_color = style_out['fg_color']
self.outline = outline
self.font_family = style_out['font_family']
self.font_size = style_out['font_size']
self.font_loader = style_out['font_loader']
# Set the background color of the frame
self.configure(bg=style_out['bg_color'])
# Create a canvas for the rounded rectangle background
if width is None:
self.canvas_width = 220 # Adjusted for padding
else:
self.canvas_width = width
self.canvas_height = 40 # Adjusted for padding
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=style_out['bg_color'])
self.canvas.pack()
# Create the entry widget
if self.font_loader:
self.entry = tk.Entry(self, textvariable=textvariable, bd=0, highlightthickness=0, fg=self.fg_color, font=self.font_loader.get_font(size=self.font_size), bg=self.bg_color)
else:
self.entry = tk.Entry(self, textvariable=textvariable, bd=0, highlightthickness=0, fg=self.fg_color, font=(self.font_family, self.font_size), bg=self.bg_color)
self.entry.place(relx=0.5, rely=0.5, anchor=tk.CENTER, width=self.canvas_width - 30, height=20) # Centered positioning
# Bind events to change the background color on focus
self.entry.bind("<FocusIn>", self.on_focus_in)
self.entry.bind("<FocusOut>", self.on_focus_out)
self.draw_rounded_rectangle(self.bg_color)
[docs]
def draw_rounded_rectangle(self, color):
radius = 15 # Increased radius for more rounded corners
x0, y0 = 10, 5
x1, y1 = self.canvas_width - 10, self.canvas_height - 5
self.canvas.delete("all")
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=color)
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=color)
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
[docs]
def on_focus_in(self, event):
self.draw_rounded_rectangle(self.active_color)
self.entry.config(bg=self.active_color)
[docs]
def on_focus_out(self, event):
self.draw_rounded_rectangle(self.bg_color)
self.entry.config(bg=self.bg_color)
[docs]
class spacrCheck(tk.Frame):
def __init__(self, parent, text="", variable=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
style_out = set_dark_style(ttk.Style())
self.bg_color = style_out['bg_color']
self.active_color = style_out['active_color']
self.fg_color = style_out['fg_color']
self.inactive_color = style_out['inactive_color']
self.variable = variable
self.configure(bg=self.bg_color)
# Create a canvas for the rounded square background
self.canvas_width = 20
self.canvas_height = 20
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=self.bg_color)
self.canvas.pack()
# Draw the initial rounded square based on the variable's value
self.draw_rounded_square(self.active_color if self.variable.get() else self.inactive_color)
# Bind variable changes to update the checkbox
self.variable.trace_add('write', self.update_check)
# Bind click event to toggle the variable
self.canvas.bind("<Button-1>", self.toggle_variable)
[docs]
def draw_rounded_square(self, color):
radius = 5 # Adjust the radius for more rounded corners
x0, y0 = 2, 2
x1, y1 = 18, 18
self.canvas.delete("all")
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=self.fg_color)
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=self.fg_color)
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=self.fg_color)
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=self.fg_color)
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
self.canvas.create_line(x0 + radius / 2, y0, x1 - radius / 2, y0, fill=self.fg_color)
self.canvas.create_line(x0 + radius / 2, y1, x1 - radius / 2, y1, fill=self.fg_color)
self.canvas.create_line(x0, y0 + radius / 2, x0, y1 - radius / 2, fill=self.fg_color)
self.canvas.create_line(x1, y0 + radius / 2, x1, y1 - radius / 2, fill=self.fg_color)
[docs]
def update_check(self, *args):
self.draw_rounded_square(self.active_color if self.variable.get() else self.inactive_color)
[docs]
def toggle_variable(self, event):
self.variable.set(not self.variable.get())
[docs]
class spacrCombo(tk.Frame):
def __init__(self, parent, textvariable=None, values=None, width=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# Set dark style
style_out = set_dark_style(ttk.Style())
self.bg_color = style_out['bg_color']
self.active_color = style_out['active_color']
self.fg_color = style_out['fg_color']
self.inactive_color = style_out['inactive_color']
self.font_family = style_out['font_family']
self.font_size = style_out['font_size']
self.font_loader = style_out['font_loader']
self.values = values or []
# Create a canvas for the rounded rectangle background
self.canvas_width = width if width is not None else 220 # Adjusted for padding
self.canvas_height = 40 # Adjusted for padding
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=self.bg_color)
self.canvas.pack()
self.var = textvariable if textvariable else tk.StringVar()
self.selected_value = self.var.get()
# Create the label to display the selected value
if self.font_loader:
self.label = tk.Label(self, text=self.selected_value, bg=self.inactive_color, fg=self.fg_color, font=self.font_loader.get_font(size=self.font_size))
else:
self.label = tk.Label(self, text=self.selected_value, bg=self.inactive_color, fg=self.fg_color, font=(self.font_family, self.font_size))
self.label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
# Bind events to open the dropdown menu
self.canvas.bind("<Button-1>", self.on_click)
self.label.bind("<Button-1>", self.on_click)
self.draw_rounded_rectangle(self.inactive_color)
self.dropdown_menu = None
[docs]
def draw_rounded_rectangle(self, color):
radius = 15 # Increased radius for more rounded corners
x0, y0 = 10, 5
x1, y1 = self.canvas_width - 10, self.canvas_height - 5
self.canvas.delete("all")
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=color)
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=color)
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
self.label.config(bg=color) # Update label background to match rectangle color
[docs]
def on_click(self, event):
if self.dropdown_menu is None:
self.open_dropdown()
else:
self.close_dropdown()
[docs]
def open_dropdown(self):
self.draw_rounded_rectangle(self.active_color)
self.dropdown_menu = tk.Toplevel(self)
self.dropdown_menu.wm_overrideredirect(True)
x, y, width, height = self.winfo_rootx(), self.winfo_rooty(), self.winfo_width(), self.winfo_height()
self.dropdown_menu.geometry(f"{width}x{len(self.values) * 30}+{x}+{y + height}")
for index, value in enumerate(self.values):
display_text = value if value is not None else 'None'
if self.font_loader:
item = tk.Label(self.dropdown_menu, text=display_text, bg=self.inactive_color, fg=self.fg_color, font=self.font_loader.get_font(size=self.font_size), anchor='w')
else:
item = tk.Label(self.dropdown_menu, text=display_text, bg=self.inactive_color, fg=self.fg_color, font=(self.font_family, self.font_size), anchor='w')
item.pack(fill='both')
item.bind("<Button-1>", lambda e, v=value: self.on_select(v))
item.bind("<Enter>", lambda e, w=item: w.config(bg=self.active_color))
item.bind("<Leave>", lambda e, w=item: w.config(bg=self.inactive_color))
[docs]
def close_dropdown(self):
self.draw_rounded_rectangle(self.inactive_color)
if self.dropdown_menu:
self.dropdown_menu.destroy()
self.dropdown_menu = None
[docs]
def on_select(self, value):
display_text = value if value is not None else 'None'
self.var.set(value)
self.label.config(text=display_text)
self.selected_value = value
self.close_dropdown()
[docs]
def set(self, value):
display_text = value if value is not None else 'None'
self.var.set(value)
self.label.config(text=display_text)
self.selected_value = value
[docs]
class spacrProgressBar(ttk.Progressbar):
def __init__(self, parent, label=True, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
# Get the style colors
style_out = set_dark_style(ttk.Style())
self.fg_color = style_out['fg_color']
self.bg_color = style_out['bg_color']
self.active_color = style_out['active_color']
self.inactive_color = style_out['inactive_color']
self.font_size = style_out['font_size']
self.font_loader = style_out['font_loader']
# Configure the style for the progress bar
self.style = ttk.Style()
# Remove any borders and ensure the active color fills the entire space
self.style.configure(
"spacr.Horizontal.TProgressbar",
troughcolor=self.inactive_color, # Set the trough to bg color
background=self.active_color, # Active part is the active color
borderwidth=0, # Remove border width
pbarrelief="flat", # Flat relief for the progress bar
troughrelief="flat", # Flat relief for the trough
thickness=20, # Set the thickness of the progress bar
darkcolor=self.active_color, # Ensure darkcolor matches the active color
lightcolor=self.active_color, # Ensure lightcolor matches the active color
bordercolor=self.bg_color # Set the border color to the background color to hide it
)
self.configure(style="spacr.Horizontal.TProgressbar")
# Set initial value to 0
self['value'] = 0
# Track whether to show the progress label
self.label = label
# Create the progress label with text wrapping
if self.label:
self.progress_label = tk.Label(
parent,
text="Processing: 0/0",
anchor='w',
justify='left',
bg=self.inactive_color,
fg=self.fg_color,
wraplength=300,
font=self.font_loader.get_font(size=self.font_size)
)
self.progress_label.grid_forget()
# Initialize attributes for time and operation
self.operation_type = None
self.additional_info = None
[docs]
def set_label_position(self):
if self.label and self.progress_label:
row_info = self.grid_info().get('row', 0)
col_info = self.grid_info().get('column', 0)
col_span = self.grid_info().get('columnspan', 1)
self.progress_label.grid(row=row_info + 1, column=col_info, columnspan=col_span, pady=5, padx=5, sticky='ew')
[docs]
def update_label(self):
if self.label and self.progress_label:
# Update the progress label with current progress and additional info
label_text = f"Processing: {self['value']}/{self['maximum']}"
if self.operation_type:
label_text += f", {self.operation_type}"
if hasattr(self, 'additional_info') and self.additional_info:
# Add a space between progress information and additional information
label_text += "\n\n"
# Split the additional_info into a list of items
items = self.additional_info.split(", ")
formatted_additional_info = ""
# Group the items in pairs, adding them to formatted_additional_info
for i in range(0, len(items), 2):
if i + 1 < len(items):
formatted_additional_info += f"{items[i]}, {items[i + 1]}\n\n"
else:
formatted_additional_info += f"{items[i]}\n\n" # If there's an odd item out, add it alone
label_text += formatted_additional_info.strip()
self.progress_label.config(text=label_text)
[docs]
class spacrFrame(ttk.Frame):
def __init__(self, container, width=None, *args, bg='black', radius=20, scrollbar=True, textbox=False, **kwargs):
super().__init__(container, *args, **kwargs)
self.configure(style='TFrame')
if width is None:
screen_width = self.winfo_screenwidth()
width = screen_width // 4
# Create the canvas
canvas = tk.Canvas(self, bg=bg, width=width, highlightthickness=0)
self.rounded_rectangle(canvas, 0, 0, width, self.winfo_screenheight(), radius, fill=bg)
# Define scrollbar styles
style_out = set_dark_style(ttk.Style())
self.inactive_color = style_out['inactive_color']
self.active_color = style_out['active_color']
self.fg_color = style_out['fg_color'] # Foreground color for text
# Set custom scrollbar style
style = ttk.Style()
spacrScrollbarStyle(style, self.inactive_color, self.active_color)
# Create scrollbar with custom style if scrollbar option is True
if scrollbar:
scrollbar_widget = ttk.Scrollbar(self, orient="vertical", command=canvas.yview, style='Custom.Vertical.TScrollbar')
if textbox:
self.scrollable_frame = tk.Text(canvas, bg=bg, fg=self.fg_color, wrap=tk.WORD)
else:
self.scrollable_frame = ttk.Frame(canvas, style='TFrame')
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
if scrollbar:
canvas.configure(yscrollcommand=scrollbar_widget.set)
canvas.grid(row=0, column=0, sticky="nsew")
if scrollbar:
scrollbar_widget.grid(row=0, column=1, sticky="ns")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
if scrollbar:
self.grid_columnconfigure(1, weight=0)
_ = set_dark_style(style, containers=[self], widgets=[canvas, self.scrollable_frame])
if scrollbar:
_ = set_dark_style(style, widgets=[scrollbar_widget])
[docs]
def rounded_rectangle(self, canvas, x1, y1, x2, y2, radius=20, **kwargs):
points = [
x1 + radius, y1,
x2 - radius, y1,
x2 - radius, y1,
x2, y1,
x2, y1 + radius,
x2, y2 - radius,
x2, y2 - radius,
x2, y2,
x2 - radius, y2,
x1 + radius, y2,
x1 + radius, y2,
x1, y2,
x1, y2 - radius,
x1, y2 - radius,
x1, y1 + radius,
x1, y1 + radius,
x1, y1
]
return canvas.create_polygon(points, **kwargs, smooth=True)
[docs]
class spacrLabel(tk.Frame):
def __init__(self, parent, text="", font=None, style=None, align="right", height=None, **kwargs):
valid_kwargs = {k: v for k, v in kwargs.items() if k not in ['foreground', 'background', 'font', 'anchor', 'justify', 'wraplength']}
super().__init__(parent, **valid_kwargs)
self.text = text
self.align = align
if height is None:
screen_height = self.winfo_screenheight()
label_height = screen_height // 50
label_width = label_height * 10
else:
label_height = height
label_width = label_height * 10
self.style_out = set_dark_style(ttk.Style())
self.font_style = self.style_out['font_family']
self.font_size = self.style_out['font_size']
self.font_family = self.style_out['font_family']
self.font_loader = self.style_out['font_loader']
self.canvas = tk.Canvas(self, width=label_width, height=label_height, highlightthickness=0, bg=self.style_out['bg_color'])
self.canvas.grid(row=0, column=0, sticky="ew")
if self.style_out['font_family'] != 'OpenSans':
self.font_style = font if font else tkFont.Font(family=self.style_out['font_family'], size=self.style_out['font_size'], weight=tkFont.NORMAL)
self.style = style
if self.align == "center":
anchor_value = tk.CENTER
text_anchor = 'center'
else: # default to right alignment
anchor_value = tk.E
text_anchor = 'e'
if self.style:
ttk_style = ttk.Style()
if self.font_loader:
ttk_style.configure(self.style, font=self.font_loader.get_font(size=self.font_size), background=self.style_out['bg_color'], foreground=self.style_out['fg_color'])
else:
ttk_style.configure(self.style, font=self.font_style, background=self.style_out['bg_color'], foreground=self.style_out['fg_color'])
self.label_text = ttk.Label(self.canvas, text=self.text, style=self.style, anchor=text_anchor)
self.label_text.pack(fill=tk.BOTH, expand=True)
else:
if self.font_loader:
self.label_text = self.canvas.create_text(label_width // 2 if self.align == "center" else label_width - 5,
label_height // 2, text=self.text, fill=self.style_out['fg_color'],
font=self.font_loader.get_font(size=self.font_size), anchor=anchor_value, justify=tk.RIGHT)
else:
self.label_text = self.canvas.create_text(label_width // 2 if self.align == "center" else label_width - 5,
label_height // 2, text=self.text, fill=self.style_out['fg_color'],
font=self.font_style, anchor=anchor_value, justify=tk.RIGHT)
_ = set_dark_style(ttk.Style(), containers=[self], widgets=[self.canvas])
[docs]
def set_text(self, text):
if self.style:
self.label_text.config(text=text)
else:
self.canvas.itemconfig(self.label_text, text=text)
[docs]
class spacrSwitch(ttk.Frame):
def __init__(self, parent, text="", variable=None, command=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.text = text
self.variable = variable if variable else tk.BooleanVar()
self.command = command
self.canvas = tk.Canvas(self, width=40, height=20, highlightthickness=0, bd=0)
self.canvas.grid(row=0, column=1, padx=(10, 0))
self.switch_bg = self.create_rounded_rectangle(2, 2, 38, 18, radius=9, outline="", fill="#fff")
self.switch = self.canvas.create_oval(4, 4, 16, 16, outline="", fill="#800080")
self.label = spacrLabel(self, text=self.text)
self.label.grid(row=0, column=0, padx=(0, 10))
self.bind("<Button-1>", self.toggle)
self.canvas.bind("<Button-1>", self.toggle)
self.label.bind("<Button-1>", self.toggle)
self.update_switch()
style = ttk.Style()
_ = set_dark_style(style, containers=[self], widgets=[self.canvas, self.label])
[docs]
def toggle(self, event=None):
self.variable.set(not self.variable.get())
self.animate_switch()
if self.command:
self.command()
[docs]
def update_switch(self):
if self.variable.get():
self.canvas.itemconfig(self.switch, fill="#008080")
self.canvas.coords(self.switch, 24, 4, 36, 16)
else:
self.canvas.itemconfig(self.switch, fill="#800080")
self.canvas.coords(self.switch, 4, 4, 16, 16)
[docs]
def animate_switch(self):
if self.variable.get():
start_x, end_x = 4, 24
final_color = "#008080"
else:
start_x, end_x = 24, 4
final_color = "#800080"
self.animate_movement(start_x, end_x, final_color)
[docs]
def animate_movement(self, start_x, end_x, final_color):
step = 1 if start_x < end_x else -1
for i in range(start_x, end_x, step):
self.canvas.coords(self.switch, i, 4, i + 12, 16)
self.canvas.update()
self.after(10)
self.canvas.itemconfig(self.switch, fill=final_color)
[docs]
def get(self):
return self.variable.get()
[docs]
def set(self, value):
self.variable.set(value)
self.update_switch()
[docs]
def create_rounded_rectangle(self, x1, y1, x2, y2, radius=9, **kwargs):
points = [x1 + radius, y1,
x1 + radius, y1,
x2 - radius, y1,
x2 - radius, y1,
x2, y1,
x2, y1 + radius,
x2, y1 + radius,
x2, y2 - radius,
x2, y2 - radius,
x2, y2,
x2 - radius, y2,
x2 - radius, y2,
x1 + radius, y2,
x1 + radius, y2,
x1, y2,
x1, y2 - radius,
x1, y2 - radius,
x1, y1 + radius,
x1, y1 + radius,
x1, y1]
return self.canvas.create_polygon(points, **kwargs, smooth=True)
[docs]
class ModifyMaskApp:
def __init__(self, root, folder_path, scale_factor):
self.root = root
self.folder_path = folder_path
self.scale_factor = scale_factor
self.image_filenames = sorted([f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff'))])
self.masks_folder = os.path.join(folder_path, 'masks')
self.current_image_index = 0
self.initialize_flags()
self.canvas_width = self.root.winfo_screenheight() -100
self.canvas_height = self.root.winfo_screenheight() -100
self.root.configure(bg='black')
self.setup_navigation_toolbar()
self.setup_mode_toolbar()
self.setup_function_toolbar()
self.setup_zoom_toolbar()
self.setup_canvas()
self.load_first_image()
####################################################################################################
# Helper functions#
####################################################################################################
[docs]
def update_display(self):
if self.zoom_active:
self.display_zoomed_image()
else:
self.display_image()
[docs]
def update_original_mask_from_zoom(self):
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
zoomed_mask_resized = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
self.mask[y0:y1, x0:x1] = zoomed_mask_resized
[docs]
def update_original_mask(self, zoomed_mask, x0, x1, y0, y1):
actual_mask_region = self.mask[y0:y1, x0:x1]
target_shape = actual_mask_region.shape
resized_mask = resize(zoomed_mask, target_shape, order=0, preserve_range=True).astype(np.uint8)
if resized_mask.shape != actual_mask_region.shape:
raise ValueError(f"Shape mismatch: resized_mask {resized_mask.shape}, actual_mask_region {actual_mask_region.shape}")
self.mask[y0:y1, x0:x1] = np.maximum(actual_mask_region, resized_mask)
self.mask = self.mask.copy()
self.mask[y0:y1, x0:x1] = np.maximum(self.mask[y0:y1, x0:x1], resized_mask)
self.mask = self.mask.copy()
[docs]
def get_scaling_factors(self, img_width, img_height, canvas_width, canvas_height):
x_scale = img_width / canvas_width
y_scale = img_height / canvas_height
return x_scale, y_scale
[docs]
def canvas_to_image(self, x_canvas, y_canvas):
x_scale, y_scale = self.get_scaling_factors(
self.image.shape[1], self.image.shape[0],
self.canvas_width, self.canvas_height
)
x_image = int(x_canvas * x_scale)
y_image = int(y_canvas * y_scale)
return x_image, y_image
[docs]
def apply_zoom_on_enter(self, event):
if self.zoom_active and self.zoom_rectangle_start is not None:
self.set_zoom_rectangle_end(event)
[docs]
def normalize_image(self, image, lower_quantile, upper_quantile):
lower_bound = np.percentile(image, lower_quantile)
upper_bound = np.percentile(image, upper_quantile)
normalized = np.clip(image, lower_bound, upper_bound)
normalized = (normalized - lower_bound) / (upper_bound - lower_bound)
max_value = np.iinfo(image.dtype).max
normalized = (normalized * max_value).astype(image.dtype)
return normalized
[docs]
def resize_arrays(self, img, mask):
original_dtype = img.dtype
scaled_height = int(img.shape[0] * self.scale_factor)
scaled_width = int(img.shape[1] * self.scale_factor)
scaled_img = resize(img, (scaled_height, scaled_width), anti_aliasing=True, preserve_range=True)
scaled_mask = resize(mask, (scaled_height, scaled_width), order=0, anti_aliasing=False, preserve_range=True)
stretched_img = resize(scaled_img, (self.canvas_height, self.canvas_width), anti_aliasing=True, preserve_range=True)
stretched_mask = resize(scaled_mask, (self.canvas_height, self.canvas_width), order=0, anti_aliasing=False, preserve_range=True)
return stretched_img.astype(original_dtype), stretched_mask.astype(original_dtype)
####################################################################################################
#Initiate canvas elements#
####################################################################################################
[docs]
def load_first_image(self):
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
self.original_size = self.image.shape
self.image, self.mask = self.resize_arrays(self.image, self.mask)
self.display_image()
[docs]
def setup_canvas(self):
self.canvas = tk.Canvas(self.root, width=self.canvas_width, height=self.canvas_height, bg='black')
self.canvas.pack()
self.canvas.bind("<Motion>", self.update_mouse_info)
[docs]
def initialize_flags(self):
self.zoom_rectangle_start = None
self.zoom_rectangle_end = None
self.zoom_rectangle_id = None
self.zoom_x0 = None
self.zoom_y0 = None
self.zoom_x1 = None
self.zoom_y1 = None
self.zoom_mask = None
self.zoom_image = None
self.zoom_image_orig = None
self.zoom_scale = 1
self.drawing = False
self.zoom_active = False
self.magic_wand_active = False
self.brush_active = False
self.dividing_line_active = False
self.dividing_line_coords = []
self.current_dividing_line = None
self.lower_quantile = tk.StringVar(value="1.0")
self.upper_quantile = tk.StringVar(value="99.9")
self.magic_wand_tolerance = tk.StringVar(value="1000")
[docs]
def update_mouse_info(self, event):
x, y = event.x, event.y
intensity = "N/A"
mask_value = "N/A"
pixel_count = "N/A"
if self.zoom_active:
if 0 <= x < self.canvas_width and 0 <= y < self.canvas_height:
intensity = self.zoom_image_orig[y, x] if self.zoom_image_orig is not None else "N/A"
mask_value = self.zoom_mask[y, x] if self.zoom_mask is not None else "N/A"
else:
if 0 <= x < self.image.shape[1] and 0 <= y < self.image.shape[0]:
intensity = self.image[y, x]
mask_value = self.mask[y, x]
if mask_value != "N/A" and mask_value != 0:
pixel_count = np.sum(self.mask == mask_value)
self.intensity_label.config(text=f"Intensity: {intensity}")
self.mask_value_label.config(text=f"Mask: {mask_value}, Area: {pixel_count}")
self.mask_value_label.config(text=f"Mask: {mask_value}")
if mask_value != "N/A" and mask_value != 0:
self.pixel_count_label.config(text=f"Area: {pixel_count}")
else:
self.pixel_count_label.config(text="Area: N/A")
[docs]
def load_image_and_mask(self, index):
image_path = os.path.join(self.folder_path, self.image_filenames[index])
image = imageio.imread(image_path)
mask_path = os.path.join(self.masks_folder, self.image_filenames[index])
if os.path.exists(mask_path):
print(f'loading mask:{mask_path} for image: {image_path}')
mask = imageio.imread(mask_path)
if mask.dtype != np.uint8:
mask = (mask / np.max(mask) * 255).astype(np.uint8)
else:
mask = np.zeros(image.shape[:2], dtype=np.uint8)
print(f'loaded new mask for image: {image_path}')
return image, mask
####################################################################################################
# Image Display functions#
####################################################################################################
[docs]
def display_image(self):
if self.zoom_rectangle_id is not None:
self.canvas.delete(self.zoom_rectangle_id)
self.zoom_rectangle_id = None
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
normalized = self.normalize_image(self.image, lower_quantile, upper_quantile)
combined = self.overlay_mask_on_image(normalized, self.mask)
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
[docs]
def display_zoomed_image(self):
if self.zoom_rectangle_start and self.zoom_rectangle_end:
# Convert canvas coordinates to image coordinates
x0, y0 = self.canvas_to_image(*self.zoom_rectangle_start)
x1, y1 = self.canvas_to_image(*self.zoom_rectangle_end)
x0, x1 = min(x0, x1), max(x0, x1)
y0, y1 = min(y0, y1), max(y0, y1)
self.zoom_x0 = x0
self.zoom_y0 = y0
self.zoom_x1 = x1
self.zoom_y1 = y1
# Normalize the entire image
lower_quantile = float(self.lower_quantile.get()) if self.lower_quantile.get() else 1.0
upper_quantile = float(self.upper_quantile.get()) if self.upper_quantile.get() else 99.9
normalized_image = self.normalize_image(self.image, lower_quantile, upper_quantile)
# Extract the zoomed portion of the normalized image and mask
self.zoom_image = normalized_image[y0:y1, x0:x1]
self.zoom_image_orig = self.image[y0:y1, x0:x1]
self.zoom_mask = self.mask[y0:y1, x0:x1]
original_mask_area = self.mask.shape[0] * self.mask.shape[1]
zoom_mask_area = self.zoom_mask.shape[0] * self.zoom_mask.shape[1]
if original_mask_area > 0:
self.zoom_scale = original_mask_area/zoom_mask_area
# Resize the zoomed image and mask to fit the canvas
canvas_height = self.canvas.winfo_height()
canvas_width = self.canvas.winfo_width()
if self.zoom_image.size > 0 and canvas_height > 0 and canvas_width > 0:
self.zoom_image = resize(self.zoom_image, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image.dtype)
self.zoom_image_orig = resize(self.zoom_image_orig, (canvas_height, canvas_width), preserve_range=True).astype(self.zoom_image_orig.dtype)
#self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), preserve_range=True).astype(np.uint8)
self.zoom_mask = resize(self.zoom_mask, (canvas_height, canvas_width), order=0, preserve_range=True).astype(np.uint8)
combined = self.overlay_mask_on_image(self.zoom_image, self.zoom_mask)
self.tk_image = ImageTk.PhotoImage(image=Image.fromarray(combined))
self.canvas.create_image(0, 0, anchor='nw', image=self.tk_image)
[docs]
def overlay_mask_on_image(self, image, mask, alpha=0.5):
if len(image.shape) == 2:
image = np.stack((image,) * 3, axis=-1)
mask = mask.astype(np.int32)
max_label = np.max(mask)
np.random.seed(0)
colors = np.random.randint(0, 255, size=(max_label + 1, 3), dtype=np.uint8)
colors[0] = [0, 0, 0] # background color
colored_mask = colors[mask]
image_8bit = (image / 256).astype(np.uint8)
# Blend the mask and the image with transparency
combined_image = np.where(mask[..., None] > 0,
np.clip(image_8bit * (1 - alpha) + colored_mask * alpha, 0, 255),
image_8bit)
# Convert the final image back to uint8
combined_image = combined_image.astype(np.uint8)
return combined_image
####################################################################################################
# Navigation functions#
####################################################################################################
[docs]
def previous_image(self):
if self.current_image_index > 0:
self.current_image_index -= 1
self.initialize_flags()
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
self.original_size = self.image.shape
self.image, self.mask = self.resize_arrays(self.image, self.mask)
self.display_image()
[docs]
def next_image(self):
if self.current_image_index < len(self.image_filenames) - 1:
self.current_image_index += 1
self.initialize_flags()
self.image, self.mask = self.load_image_and_mask(self.current_image_index)
self.original_size = self.image.shape
self.image, self.mask = self.resize_arrays(self.image, self.mask)
self.display_image()
[docs]
def save_mask(self):
if self.current_image_index < len(self.image_filenames):
original_size = self.original_size
if self.mask.shape != original_size:
resized_mask = resize(self.mask, original_size, order=0, preserve_range=True).astype(np.uint16)
else:
resized_mask = self.mask
resized_mask, _ = label(resized_mask > 0)
save_folder = os.path.join(self.folder_path, 'masks')
if not os.path.exists(save_folder):
os.makedirs(save_folder)
image_filename = os.path.splitext(self.image_filenames[self.current_image_index])[0] + '.tif'
save_path = os.path.join(save_folder, image_filename)
print(f"Saving mask to: {save_path}") # Debug print
imageio.imwrite(save_path, resized_mask)
####################################################################################################
# Zoom Functions #
####################################################################################################
[docs]
def set_zoom_rectangle_start(self, event):
if self.zoom_active:
self.zoom_rectangle_start = (event.x, event.y)
[docs]
def set_zoom_rectangle_end(self, event):
if self.zoom_active:
self.zoom_rectangle_end = (event.x, event.y)
if self.zoom_rectangle_id is not None:
self.canvas.delete(self.zoom_rectangle_id)
self.zoom_rectangle_id = None
self.display_zoomed_image()
self.canvas.unbind("<Motion>")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Button-3>")
self.canvas.bind("<Motion>", self.update_mouse_info)
[docs]
def update_zoom_box(self, event):
if self.zoom_active and self.zoom_rectangle_start is not None:
if self.zoom_rectangle_id is not None:
self.canvas.delete(self.zoom_rectangle_id)
# Assuming event.x and event.y are already in image coordinates
self.zoom_rectangle_end = (event.x, event.y)
x0, y0 = self.zoom_rectangle_start
x1, y1 = self.zoom_rectangle_end
self.zoom_rectangle_id = self.canvas.create_rectangle(x0, y0, x1, y1, outline="red", width=2)
####################################################################################################
# Mode activation#
####################################################################################################
[docs]
def toggle_zoom_mode(self):
if not self.zoom_active:
self.brush_btn.config(text="Brush")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<B3-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<ButtonRelease-3>")
self.zoom_active = True
self.drawing = False
self.magic_wand_active = False
self.erase_active = False
self.brush_active = False
self.dividing_line_active = False
self.draw_btn.config(text="Draw")
self.erase_btn.config(text="Erase")
self.magic_wand_btn.config(text="Magic Wand")
self.zoom_btn.config(text="Zoom ON")
self.dividing_line_btn.config(text="Dividing Line")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Button-3>")
self.canvas.unbind("<Motion>")
self.canvas.bind("<Button-1>", self.set_zoom_rectangle_start)
self.canvas.bind("<Button-3>", self.set_zoom_rectangle_end)
self.canvas.bind("<Motion>", self.update_zoom_box)
else:
self.zoom_active = False
self.zoom_btn.config(text="Zoom")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Button-3>")
self.canvas.unbind("<Motion>")
self.zoom_rectangle_start = self.zoom_rectangle_end = None
self.zoom_rectangle_id = None
self.display_image()
self.canvas.bind("<Motion>", self.update_mouse_info)
self.zoom_rectangle_start = None
self.zoom_rectangle_end = None
self.zoom_rectangle_id = None
self.zoom_x0 = None
self.zoom_y0 = None
self.zoom_x1 = None
self.zoom_y1 = None
self.zoom_mask = None
self.zoom_image = None
self.zoom_image_orig = None
[docs]
def toggle_brush_mode(self):
self.brush_active = not self.brush_active
if self.brush_active:
self.drawing = False
self.magic_wand_active = False
self.erase_active = False
self.brush_btn.config(text="Brush ON")
self.draw_btn.config(text="Draw")
self.erase_btn.config(text="Erase")
self.magic_wand_btn.config(text="Magic Wand")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Button-3>")
self.canvas.unbind("<Motion>")
self.canvas.bind("<B1-Motion>", self.apply_brush) # Left click and drag to apply brush
self.canvas.bind("<B3-Motion>", self.erase_brush) # Right click and drag to erase with brush
self.canvas.bind("<ButtonRelease-1>", self.apply_brush_release) # Left button release
self.canvas.bind("<ButtonRelease-3>", self.erase_brush_release) # Right button release
else:
self.brush_active = False
self.brush_btn.config(text="Brush")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<B3-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<ButtonRelease-3>")
[docs]
def image_to_canvas(self, x_image, y_image):
x_scale, y_scale = self.get_scaling_factors(
self.image.shape[1], self.image.shape[0],
self.canvas_width, self.canvas_height
)
x_canvas = int(x_image / x_scale)
y_canvas = int(y_image / y_scale)
return x_canvas, y_canvas
[docs]
def toggle_dividing_line_mode(self):
self.dividing_line_active = not self.dividing_line_active
if self.dividing_line_active:
self.drawing = False
self.magic_wand_active = False
self.erase_active = False
self.brush_active = False
self.draw_btn.config(text="Draw")
self.erase_btn.config(text="Erase")
self.magic_wand_btn.config(text="Magic Wand")
self.brush_btn.config(text="Brush")
self.dividing_line_btn.config(text="Dividing Line ON")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<Motion>")
self.canvas.bind("<Button-1>", self.start_dividing_line)
self.canvas.bind("<ButtonRelease-1>", self.finish_dividing_line)
self.canvas.bind("<Motion>", self.update_dividing_line_preview)
else:
print("Dividing Line Mode: OFF")
self.dividing_line_active = False
self.dividing_line_btn.config(text="Dividing Line")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<Motion>")
self.display_image()
[docs]
def start_dividing_line(self, event):
if self.dividing_line_active:
self.dividing_line_coords = [(event.x, event.y)]
self.current_dividing_line = self.canvas.create_line(event.x, event.y, event.x, event.y, fill="red", width=2)
[docs]
def finish_dividing_line(self, event):
if self.dividing_line_active:
self.dividing_line_coords.append((event.x, event.y))
if self.zoom_active:
self.dividing_line_coords = [self.canvas_to_image(x, y) for x, y in self.dividing_line_coords]
self.apply_dividing_line()
self.canvas.delete(self.current_dividing_line)
self.current_dividing_line = None
[docs]
def update_dividing_line_preview(self, event):
if self.dividing_line_active and self.dividing_line_coords:
x, y = event.x, event.y
if self.zoom_active:
x, y = self.canvas_to_image(x, y)
self.dividing_line_coords.append((x, y))
canvas_coords = [(self.image_to_canvas(*pt) if self.zoom_active else pt) for pt in self.dividing_line_coords]
flat_canvas_coords = [coord for pt in canvas_coords for coord in pt]
self.canvas.coords(self.current_dividing_line, *flat_canvas_coords)
[docs]
def apply_dividing_line(self):
if self.dividing_line_coords:
coords = self.dividing_line_coords
if self.zoom_active:
coords = [self.canvas_to_image(x, y) for x, y in coords]
rr, cc = [], []
for (x0, y0), (x1, y1) in zip(coords[:-1], coords[1:]):
line_rr, line_cc = line(y0, x0, y1, x1)
rr.extend(line_rr)
cc.extend(line_cc)
rr, cc = np.array(rr), np.array(cc)
mask_copy = self.mask.copy()
if self.zoom_active:
# Update the zoomed mask
self.zoom_mask[rr, cc] = 0
# Reflect changes to the original mask
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
self.mask[y0:y1, x0:x1] = zoomed_mask_resized_back
else:
# Directly update the original mask
mask_copy[rr, cc] = 0
self.mask = mask_copy
labeled_mask, num_labels = label(self.mask > 0)
self.mask = labeled_mask
self.update_display()
self.dividing_line_coords = []
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<Motion>")
self.dividing_line_active = False
self.dividing_line_btn.config(text="Dividing Line")
[docs]
def toggle_draw_mode(self):
self.drawing = not self.drawing
if self.drawing:
self.brush_btn.config(text="Brush")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<B3-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<ButtonRelease-3>")
self.magic_wand_active = False
self.erase_active = False
self.brush_active = False
self.draw_btn.config(text="Draw ON")
self.magic_wand_btn.config(text="Magic Wand")
self.erase_btn.config(text="Erase")
self.draw_coordinates = []
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Motion>")
self.canvas.bind("<B1-Motion>", self.draw)
self.canvas.bind("<ButtonRelease-1>", self.finish_drawing)
else:
self.drawing = False
self.draw_btn.config(text="Draw")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
[docs]
def toggle_magic_wand_mode(self):
self.magic_wand_active = not self.magic_wand_active
if self.magic_wand_active:
self.brush_btn.config(text="Brush")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<B3-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<ButtonRelease-3>")
self.drawing = False
self.erase_active = False
self.brush_active = False
self.draw_btn.config(text="Draw")
self.erase_btn.config(text="Erase")
self.magic_wand_btn.config(text="Magic Wand ON")
self.canvas.bind("<Button-1>", self.use_magic_wand)
self.canvas.bind("<Button-3>", self.use_magic_wand)
else:
self.magic_wand_btn.config(text="Magic Wand")
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Button-3>")
[docs]
def toggle_erase_mode(self):
self.erase_active = not self.erase_active
if self.erase_active:
self.brush_btn.config(text="Brush")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<B3-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.unbind("<ButtonRelease-3>")
self.erase_btn.config(text="Erase ON")
self.canvas.bind("<Button-1>", self.erase_object)
self.drawing = False
self.magic_wand_active = False
self.brush_active = False
self.draw_btn.config(text="Draw")
self.magic_wand_btn.config(text="Magic Wand")
else:
self.erase_active = False
self.erase_btn.config(text="Erase")
self.canvas.unbind("<Button-1>")
####################################################################################################
# Mode functions#
####################################################################################################
[docs]
def apply_brush_release(self, event):
if hasattr(self, 'brush_path'):
for x, y, brush_size in self.brush_path:
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
x0 = max(img_x - brush_size // 2, 0)
y0 = max(img_y - brush_size // 2, 0)
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
if self.zoom_active:
self.zoom_mask[y0:y1, x0:x1] = 255
self.update_original_mask_from_zoom()
else:
self.mask[y0:y1, x0:x1] = 255
del self.brush_path
self.canvas.delete("temp_line")
self.update_display()
[docs]
def erase_brush_release(self, event):
if hasattr(self, 'erase_path'):
for x, y, brush_size in self.erase_path:
img_x, img_y = (x, y) if self.zoom_active else self.canvas_to_image(x, y)
x0 = max(img_x - brush_size // 2, 0)
y0 = max(img_y - brush_size // 2, 0)
x1 = min(img_x + brush_size // 2, self.zoom_mask.shape[1] if self.zoom_active else self.mask.shape[1])
y1 = min(img_y + brush_size // 2, self.zoom_mask.shape[0] if self.zoom_active else self.mask.shape[0])
if self.zoom_active:
self.zoom_mask[y0:y1, x0:x1] = 0
self.update_original_mask_from_zoom()
else:
self.mask[y0:y1, x0:x1] = 0
del self.erase_path
self.canvas.delete("temp_line")
self.update_display()
[docs]
def apply_brush(self, event):
brush_size = int(self.brush_size_entry.get())
x, y = event.x, event.y
if not hasattr(self, 'brush_path'):
self.brush_path = []
self.last_brush_coord = (x, y)
if self.last_brush_coord:
last_x, last_y = self.last_brush_coord
rr, cc = line(last_y, last_x, y, x)
for ry, rx in zip(rr, cc):
self.brush_path.append((rx, ry, brush_size))
self.canvas.create_line(self.last_brush_coord[0], self.last_brush_coord[1], x, y, width=brush_size, fill="blue", tag="temp_line")
self.last_brush_coord = (x, y)
[docs]
def erase_brush(self, event):
brush_size = int(self.brush_size_entry.get())
x, y = event.x, event.y
if not hasattr(self, 'erase_path'):
self.erase_path = []
self.last_erase_coord = (x, y)
if self.last_erase_coord:
last_x, last_y = self.last_erase_coord
rr, cc = line(last_y, last_x, y, x)
for ry, rx in zip(rr, cc):
self.erase_path.append((rx, ry, brush_size))
self.canvas.create_line(self.last_erase_coord[0], self.last_erase_coord[1], x, y, width=brush_size, fill="white", tag="temp_line")
self.last_erase_coord = (x, y)
[docs]
def erase_object(self, event):
x, y = event.x, event.y
if self.zoom_active:
canvas_x, canvas_y = x, y
zoomed_x = int(canvas_x * (self.zoom_image.shape[1] / self.canvas_width))
zoomed_y = int(canvas_y * (self.zoom_image.shape[0] / self.canvas_height))
orig_x = int(zoomed_x * ((self.zoom_x1 - self.zoom_x0) / self.canvas_width) + self.zoom_x0)
orig_y = int(zoomed_y * ((self.zoom_y1 - self.zoom_y0) / self.canvas_height) + self.zoom_y0)
if orig_x < 0 or orig_y < 0 or orig_x >= self.image.shape[1] or orig_y >= self.image.shape[0]:
print("Point is out of bounds in the original image.")
return
else:
orig_x, orig_y = x, y
label_to_remove = self.mask[orig_y, orig_x]
if label_to_remove > 0:
self.mask[self.mask == label_to_remove] = 0
self.update_display()
[docs]
def use_magic_wand(self, event):
x, y = event.x, event.y
tolerance = int(self.magic_wand_tolerance.get())
maximum = int(self.max_pixels_entry.get())
action = 'add' if event.num == 1 else 'erase'
if self.zoom_active:
self.magic_wand_zoomed((x, y), tolerance, action)
else:
self.magic_wand_normal((x, y), tolerance, action)
[docs]
def apply_magic_wand(self, image, mask, seed_point, tolerance, maximum, action='add'):
x, y = seed_point
initial_value = image[y, x].astype(np.float32)
visited = np.zeros_like(image, dtype=bool)
queue = deque([(x, y)])
added_pixels = 0
while queue and added_pixels < maximum:
cx, cy = queue.popleft()
if visited[cy, cx]:
continue
visited[cy, cx] = True
current_value = image[cy, cx].astype(np.float32)
if np.linalg.norm(abs(current_value - initial_value)) <= tolerance:
if mask[cy, cx] == 0:
added_pixels += 1
mask[cy, cx] = 255 if action == 'add' else 0
if added_pixels >= maximum:
break
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cx + dx, cy + dy
if 0 <= nx < image.shape[1] and 0 <= ny < image.shape[0] and not visited[ny, nx]:
queue.append((nx, ny))
return mask
[docs]
def magic_wand_normal(self, seed_point, tolerance, action):
try:
maximum = int(self.max_pixels_entry.get())
except ValueError:
print("Invalid maximum value; using default of 1000")
maximum = 1000
self.mask = self.apply_magic_wand(self.image, self.mask, seed_point, tolerance, maximum, action)
self.display_image()
[docs]
def magic_wand_zoomed(self, seed_point, tolerance, action):
if self.zoom_image_orig is None or self.zoom_mask is None:
print("Zoomed image or mask not initialized")
return
try:
maximum = int(self.max_pixels_entry.get())
maximum = maximum * self.zoom_scale
except ValueError:
print("Invalid maximum value; using default of 1000")
maximum = 1000
canvas_x, canvas_y = seed_point
if canvas_x < 0 or canvas_y < 0 or canvas_x >= self.zoom_image_orig.shape[1] or canvas_y >= self.zoom_image_orig.shape[0]:
print("Selected point is out of bounds in the zoomed image.")
return
self.zoom_mask = self.apply_magic_wand(self.zoom_image_orig, self.zoom_mask, (canvas_x, canvas_y), tolerance, maximum, action)
y0, y1, x0, x1 = self.zoom_y0, self.zoom_y1, self.zoom_x0, self.zoom_x1
zoomed_mask_resized_back = resize(self.zoom_mask, (y1 - y0, x1 - x0), order=0, preserve_range=True).astype(np.uint8)
if action == 'erase':
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back == 0, 0, self.mask[y0:y1, x0:x1])
else:
self.mask[y0:y1, x0:x1] = np.where(zoomed_mask_resized_back > 0, zoomed_mask_resized_back, self.mask[y0:y1, x0:x1])
self.update_display()
[docs]
def draw(self, event):
if self.drawing:
x, y = event.x, event.y
if self.draw_coordinates:
last_x, last_y = self.draw_coordinates[-1]
self.current_line = self.canvas.create_line(last_x, last_y, x, y, fill="yellow", width=3)
self.draw_coordinates.append((x, y))
[docs]
def draw_on_zoomed_mask(self, draw_coordinates):
canvas_height = self.canvas.winfo_height()
canvas_width = self.canvas.winfo_width()
zoomed_mask = np.zeros((canvas_height, canvas_width), dtype=np.uint8)
rr, cc = polygon(np.array(draw_coordinates)[:, 1], np.array(draw_coordinates)[:, 0], shape=zoomed_mask.shape)
zoomed_mask[rr, cc] = 255
return zoomed_mask
[docs]
def finish_drawing(self, event):
if len(self.draw_coordinates) > 2:
self.draw_coordinates.append(self.draw_coordinates[0])
if self.zoom_active:
x0, x1, y0, y1 = self.zoom_x0, self.zoom_x1, self.zoom_y0, self.zoom_y1
zoomed_mask = self.draw_on_zoomed_mask(self.draw_coordinates)
self.update_original_mask(zoomed_mask, x0, x1, y0, y1)
else:
rr, cc = polygon(np.array(self.draw_coordinates)[:, 1], np.array(self.draw_coordinates)[:, 0], shape=self.mask.shape)
self.mask[rr, cc] = np.maximum(self.mask[rr, cc], 255)
self.mask = self.mask.copy()
self.canvas.delete(self.current_line)
self.draw_coordinates.clear()
self.update_display()
[docs]
def finish_drawing_if_active(self, event):
if self.drawing and len(self.draw_coordinates) > 2:
self.finish_drawing(event)
####################################################################################################
# Single function butons#
####################################################################################################
[docs]
def apply_normalization(self):
self.lower_quantile.set(self.lower_entry.get())
self.upper_quantile.set(self.upper_entry.get())
self.update_display()
[docs]
def fill_objects(self):
binary_mask = self.mask > 0
filled_mask = binary_fill_holes(binary_mask)
self.mask = filled_mask.astype(np.uint8) * 255
labeled_mask, _ = label(filled_mask)
self.mask = labeled_mask
self.update_display()
[docs]
def relabel_objects(self):
mask = self.mask
labeled_mask, num_labels = label(mask > 0)
self.mask = labeled_mask
self.update_display()
[docs]
def clear_objects(self):
self.mask = np.zeros_like(self.mask)
self.update_display()
[docs]
def invert_mask(self):
self.mask = np.where(self.mask > 0, 0, 1)
self.relabel_objects()
self.update_display()
[docs]
def remove_small_objects(self):
try:
min_area = int(self.min_area_entry.get())
except ValueError:
print("Invalid minimum area value; using default of 100")
min_area = 100
labeled_mask, num_labels = label(self.mask > 0)
for i in range(1, num_labels + 1): # Skip background
if np.sum(labeled_mask == i) < min_area:
self.mask[labeled_mask == i] = 0 # Remove small objects
self.update_display()
[docs]
class AnnotateApp:
def __init__(self, root, db_path, src, image_type=None, channels=None, image_size=200, annotation_column='annotate', normalize=False, percentiles=(1, 99), measurement=None, threshold=None):
self.root = root
self.db_path = db_path
self.src = src
self.index = 0
if isinstance(image_size, list):
self.image_size = (int(image_size[0]), int(image_size[0]))
elif isinstance(image_size, int):
self.image_size = (image_size, image_size)
else:
raise ValueError("Invalid image size")
self.annotation_column = annotation_column
self.image_type = image_type
self.channels = channels
self.normalize = normalize
self.percentiles = percentiles
self.images = {}
self.pending_updates = {}
self.labels = []
self.adjusted_to_original_paths = {}
self.terminate = False
self.update_queue = Queue()
self.measurement = measurement
self.threshold = threshold
style_out = set_dark_style(ttk.Style())
self.font_loader = style_out['font_loader']
self.font_size = style_out['font_size']
if self.font_loader:
self.font_style = self.font_loader.get_font(size=self.font_size)
else:
self.font_style = ("Arial", 12)
self.root.configure(bg=style_out['inactive_color'])
self.filtered_paths_annotations = []
self.prefilter_paths_annotations()
self.db_update_thread = threading.Thread(target=self.update_database_worker)
self.db_update_thread.start()
# Set the initial window size and make it fit the screen size
self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}")
self.root.update_idletasks()
# Create the status label
self.status_label = Label(root, text="", font=self.font_style, bg=self.root.cget('bg'))
self.status_label.grid(row=2, column=0, padx=10, pady=10, sticky="w")
# Place the buttons at the bottom right
self.button_frame = Frame(root, bg=self.root.cget('bg'))
self.button_frame.grid(row=2, column=1, padx=10, pady=10, sticky="se")
self.next_button = Button(self.button_frame, text="Next", command=self.next_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
self.next_button.pack(side="right", padx=5)
self.previous_button = Button(self.button_frame, text="Back", command=self.previous_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
self.previous_button.pack(side="right", padx=5)
self.exit_button = Button(self.button_frame, text="Exit", command=self.shutdown, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
self.exit_button.pack(side="right", padx=5)
# Calculate grid rows and columns based on the root window size and image size
self.calculate_grid_dimensions()
# Create a frame to hold the image grid
self.grid_frame = Frame(root, bg=self.root.cget('bg'))
self.grid_frame.grid(row=0, column=0, columnspan=2, padx=0, pady=0, sticky="nsew")
for i in range(self.grid_rows * self.grid_cols):
label = Label(self.grid_frame, bg=self.root.cget('bg'))
label.grid(row=i // self.grid_cols, column=i % self.grid_cols, padx=2, pady=2, sticky="nsew")
self.labels.append(label)
# Make the grid frame resize with the window
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=1)
for row in range(self.grid_rows):
self.grid_frame.grid_rowconfigure(row, weight=1)
for col in range(self.grid_cols):
self.grid_frame.grid_columnconfigure(col, weight=1)
[docs]
def calculate_grid_dimensions(self):
window_width = self.root.winfo_width()
window_height = self.root.winfo_height()
self.grid_cols = window_width // (self.image_size[0] + 4)
self.grid_rows = (window_height - self.button_frame.winfo_height() - 4) // (self.image_size[1] + 4)
# Update to make sure grid_rows and grid_cols are at least 1
self.grid_cols = max(1, self.grid_cols)
self.grid_rows = max(1, self.grid_rows)
[docs]
def prefilter_paths_annotations(self):
from .io import _read_and_join_tables
from .utils import is_list_of_lists
if self.measurement and self.threshold is not None:
df = _read_and_join_tables(self.db_path)
df[self.annotation_column] = None
before = len(df)
if is_list_of_lists(self.measurement):
if isinstance(self.threshold, list) or is_list_of_lists(self.threshold):
if len(self.measurement) == len(self.threshold):
for idx, var in enumerate(self.measurement):
df = df[df[var[idx]] > self.threshold[idx]]
after = len(df)
elif len(self.measurement) == len(self.threshold)*2:
th_idx = 0
for idx, var in enumerate(self.measurement):
if idx % 2 != 0:
th_idx += 1
thd = self.threshold
if isinstance(thd, list):
thd = thd[0]
df[f'threshold_measurement_{idx}'] = df[self.measurement[idx]]/df[self.measurement[idx+1]]
print(f"mean threshold_measurement_{idx}: {np.mean(df['threshold_measurement'])}")
print(f"median threshold measurement: {np.median(df[self.measurement])}")
df = df[df[f'threshold_measurement_{idx}'] > thd]
after = len(df)
elif isinstance(self.measurement, list):
df['threshold_measurement'] = df[self.measurement[0]]/df[self.measurement[1]]
print(f"mean threshold measurement: {np.mean(df['threshold_measurement'])}")
print(f"median threshold measurement: {np.median(df[self.measurement])}")
df = df[df['threshold_measurement'] > self.threshold]
after = len(df)
self.measurement = 'threshold_measurement'
print(f'Removed: {before-after} rows, retained {after}')
else:
print(f"mean threshold measurement: {np.mean(df[self.measurement])}")
print(f"median threshold measurement: {np.median(df[self.measurement])}")
before = len(df)
if isinstance(self.threshold, str):
if self.threshold == 'q1':
self.threshold = df[self.measurement].quantile(0.1)
if self.threshold == 'q2':
self.threshold = df[self.measurement].quantile(0.2)
if self.threshold == 'q3':
self.threshold = df[self.measurement].quantile(0.3)
if self.threshold == 'q4':
self.threshold = df[self.measurement].quantile(0.4)
if self.threshold == 'q5':
self.threshold = df[self.measurement].quantile(0.5)
if self.threshold == 'q6':
self.threshold = df[self.measurement].quantile(0.6)
if self.threshold == 'q7':
self.threshold = df[self.measurement].quantile(0.7)
if self.threshold == 'q8':
self.threshold = df[self.measurement].quantile(0.8)
if self.threshold == 'q9':
self.threshold = df[self.measurement].quantile(0.9)
print(f"threshold: {self.threshold}")
df = df[df[self.measurement] > self.threshold]
after = len(df)
print(f'Removed: {before-after} rows, retained {after}')
df = df.dropna(subset=['png_path'])
if self.image_type:
before = len(df)
if isinstance(self.image_type, list):
for tpe in self.image_type:
df = df[df['png_path'].str.contains(tpe)]
else:
df = df[df['png_path'].str.contains(self.image_type)]
after = len(df)
print(f'image_type: Removed: {before-after} rows, retained {after}')
self.filtered_paths_annotations = df[['png_path', self.annotation_column]].values.tolist()
else:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
if self.image_type:
c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list WHERE png_path LIKE ?", (f"%{self.image_type}%",))
else:
c.execute(f"SELECT png_path, {self.annotation_column} FROM png_list")
self.filtered_paths_annotations = c.fetchall()
conn.close()
[docs]
def load_images(self):
for label in self.labels:
label.config(image='')
self.images = {}
paths_annotations = self.filtered_paths_annotations[self.index:self.index + self.grid_rows * self.grid_cols]
adjusted_paths = []
for path, annotation in paths_annotations:
if not path.startswith(self.src):
parts = path.split('/data/')
if len(parts) > 1:
new_path = os.path.join(self.src, 'data', parts[1])
self.adjusted_to_original_paths[new_path] = path
adjusted_paths.append((new_path, annotation))
else:
adjusted_paths.append((path, annotation))
else:
adjusted_paths.append((path, annotation))
with ThreadPoolExecutor() as executor:
loaded_images = list(executor.map(self.load_single_image, adjusted_paths))
for i, (img, annotation) in enumerate(loaded_images):
if annotation:
border_color = 'teal' if annotation == 1 else 'red'
img = self.add_colored_border(img, border_width=5, border_color=border_color)
photo = ImageTk.PhotoImage(img)
label = self.labels[i]
self.images[label] = photo
label.config(image=photo)
path = adjusted_paths[i][0]
label.bind('<Button-1>', self.get_on_image_click(path, label, img))
label.bind('<Button-3>', self.get_on_image_click(path, label, img))
self.root.update()
[docs]
def load_single_image(self, path_annotation_tuple):
path, annotation = path_annotation_tuple
img = Image.open(path)
img = self.normalize_image(img, self.normalize, self.percentiles)
img = img.convert('RGB')
img = self.filter_channels(img)
img = img.resize(self.image_size)
return img, annotation
[docs]
@staticmethod
def normalize_image(img, normalize=False, percentiles=(1, 99)):
img_array = np.array(img)
if normalize:
if img_array.ndim == 2: # Grayscale image
p2, p98 = np.percentile(img_array, percentiles)
img_array = rescale_intensity(img_array, in_range=(p2, p98), out_range=(0, 255))
else: # Color image or multi-channel image
for channel in range(img_array.shape[2]):
p2, p98 = np.percentile(img_array[:, :, channel], percentiles)
img_array[:, :, channel] = rescale_intensity(img_array[:, :, channel], in_range=(p2, p98), out_range=(0, 255))
img_array = np.clip(img_array, 0, 255).astype('uint8')
return Image.fromarray(img_array)
[docs]
def add_colored_border(self, img, border_width, border_color):
top_border = Image.new('RGB', (img.width, border_width), color=border_color)
bottom_border = Image.new('RGB', (img.width, border_width), color=border_color)
left_border = Image.new('RGB', (border_width, img.height), color=border_color)
right_border = Image.new('RGB', (border_width, img.height), color=border_color)
bordered_img = Image.new('RGB', (img.width + 2 * border_width, img.height + 2 * border_width), color='white')
bordered_img.paste(top_border, (border_width, 0))
bordered_img.paste(bottom_border, (border_width, img.height + border_width))
bordered_img.paste(left_border, (0, border_width))
bordered_img.paste(right_border, (img.width + border_width, border_width))
bordered_img.paste(img, (border_width, border_width))
return bordered_img
[docs]
def filter_channels(self, img):
r, g, b = img.split()
if self.channels:
if 'r' not in self.channels:
r = r.point(lambda _: 0)
if 'g' not in self.channels:
g = g.point(lambda _: 0)
if 'b' not in self.channels:
b = b.point(lambda _: 0)
if len(self.channels) == 1:
channel_img = r if 'r' in self.channels else (g if 'g' in self.channels else b)
return ImageOps.grayscale(channel_img)
return Image.merge("RGB", (r, g, b))
[docs]
def get_on_image_click(self, path, label, img):
def on_image_click(event):
new_annotation = 1 if event.num == 1 else (2 if event.num == 3 else None)
original_path = self.adjusted_to_original_paths.get(path, path)
if original_path in self.pending_updates and self.pending_updates[original_path] == new_annotation:
self.pending_updates[original_path] = None
new_annotation = None
else:
self.pending_updates[original_path] = new_annotation
print(f"Image {os.path.split(path)[1]} annotated: {new_annotation}")
img_ = img.crop((5, 5, img.width-5, img.height-5))
border_fill = 'teal' if new_annotation == 1 else ('red' if new_annotation == 2 else None)
img_ = ImageOps.expand(img_, border=5, fill=border_fill) if border_fill else img_
photo = ImageTk.PhotoImage(img_)
self.images[label] = photo
label.config(image=photo)
self.root.update()
return on_image_click
[docs]
@staticmethod
def update_html(text):
display(HTML(f"""
<script>
document.getElementById('unique_id').innerHTML = '{text}';
</script>
"""))
[docs]
def update_database_worker(self):
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
display(HTML("<div id='unique_id'>Initial Text</div>"))
while True:
if self.terminate:
conn.close()
break
if not self.update_queue.empty():
AnnotateApp.update_html("Do not exit, Updating database...")
self.status_label.config(text='Do not exit, Updating database...')
pending_updates = self.update_queue.get()
for path, new_annotation in pending_updates.items():
if new_annotation is None:
c.execute(f'UPDATE png_list SET {self.annotation_column} = NULL WHERE png_path = ?', (path,))
else:
c.execute(f'UPDATE png_list SET {self.annotation_column} = ? WHERE png_path = ?', (new_annotation, path))
conn.commit()
AnnotateApp.update_html('')
self.status_label.config(text='')
self.root.update()
time.sleep(0.1)
[docs]
def update_gui_text(self, text):
self.status_label.config(text=text)
self.root.update()
[docs]
def next_page(self):
if self.pending_updates:
self.update_queue.put(self.pending_updates.copy())
self.pending_updates.clear()
self.index += self.grid_rows * self.grid_cols
self.prefilter_paths_annotations() # Re-fetch annotations from the database
self.load_images()
[docs]
def previous_page(self):
if self.pending_updates:
self.update_queue.put(self.pending_updates.copy())
self.pending_updates.clear()
self.index -= self.grid_rows * self.grid_cols
if self.index < 0:
self.index = 0
self.prefilter_paths_annotations() # Re-fetch annotations from the database
self.load_images()
[docs]
def shutdown(self):
self.terminate = True
self.update_queue.put(self.pending_updates.copy())
if not self.pending_updates:
self.pending_updates.clear()
self.db_update_thread.join()
self.root.quit()
self.root.destroy()
print(f'Quit application')
else:
print('Waiting for pending updates to finish before quitting')