Skip to content

Commit

Permalink
Tkinter import in FigureAnnotator (#75)
Browse files Browse the repository at this point in the history
* gracefully handle absence of tkinter on user's system in figure_annotator

* bump version to 0.16.2
  • Loading branch information
nklimov23 authored Jan 3, 2025
1 parent e2639fa commit e80df6a
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 53 deletions.
2 changes: 1 addition & 1 deletion degirum_tools/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
#

# >>> increment version here vvv
__version_info__ = ("0", "16", "1")
__version_info__ = ("0", "16", "2")
__version__ = ".".join(__version_info__)
124 changes: 73 additions & 51 deletions degirum_tools/figure_annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
# and the driver for this utility.
#

import tkinter as tk
from tkinter import filedialog, messagebox, ttk, Toplevel, Scrollbar, Text
import tkinter.font as tkFont
from PIL import Image, ImageTk
import json
import argparse
Expand All @@ -20,6 +17,7 @@
from typing import Tuple, List, Dict, Union, Optional
from copy import deepcopy
from pathlib import Path
from . import environment as env


help_message_line = """
Expand Down Expand Up @@ -515,7 +513,27 @@ def __init__(
self.lighter_theme_color = "lightblue"

if not test_mode:
self.root = tk.Tk()
self.tk = env.import_optional_package(
"tkinter",
custom_message="'tkinter' is not available. Hint: install tkinter with "
+ "'sudo apt install python3-tk' (Linux) or 'brew install tcl-tk' (macOS)",
)
self.tkFont = env.import_optional_package(
"tkinter.font",
custom_message="'tkinter' is not available. Hint: install tkinter with "
+ "'sudo apt install python3-tk' (Linux) or 'brew install tcl-tk' (macOS)",
)
self.ttk = env.import_optional_package(
"tkinter.ttk",
custom_message="'tkinter' is not available. Hint: install tkinter with "
+ "'sudo apt install python3-tk' (Linux) or 'brew install tcl-tk' (macOS)",
)
self.tkFiledialog = env.import_optional_package(
"tkinter.filedialog",
custom_message="'tkinter' is not available. Hint: install tkinter with "
+ "'sudo apt install python3-tk' (Linux) or 'brew install tcl-tk' (macOS)",
)
self.root = self.tk.Tk()
self.root.title(f"{self.figure_type.capitalize()} Annotator")
self.root.geometry("700x410" if self.with_grid else "700x375")
self.root.resizable(False, False)
Expand All @@ -530,16 +548,16 @@ def __init__(
self.root.iconphoto(True, self.icon_image) # type: ignore

# Set font
self.font = tkFont.Font(family="courier 10 pitch", size=12)
self.font = self.tkFont.Font(family="courier 10 pitch", size=12)

# Create the main frame to hold the menu and canvas
self.main_frame = tk.Frame(self.root, bg=self.lighter_theme_color)
self.main_frame.pack(fill=tk.BOTH, expand=True)
self.main_frame = self.tk.Frame(self.root, bg=self.lighter_theme_color)
self.main_frame.pack(fill=self.tk.BOTH, expand=True)

self.menu_bar = tk.Menu(self.root, bg=self.darker_theme_color)
self.menu_bar = self.tk.Menu(self.root, bg=self.darker_theme_color)
self.root.config(menu=self.menu_bar)

self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
self.file_menu = self.tk.Menu(self.menu_bar, tearoff=0)
self.file_menu.add_command(
label="Open Image...",
font=self.font,
Expand All @@ -551,57 +569,57 @@ def __init__(
font=self.font,
command=self.save,
accelerator="Ctrl-S",
state=tk.DISABLED,
state=self.tk.DISABLED,
)
self.file_menu.add_command(
label="Save JSON As...",
font=self.font,
command=self.save_to_file,
accelerator="Ctrl-Shift-S",
state=tk.DISABLED,
state=self.tk.DISABLED,
)
self.menu_bar.add_cascade(label="File", font=self.font, menu=self.file_menu)

self.edit_menu = tk.Menu(self.menu_bar, tearoff=0)
self.edit_menu = self.tk.Menu(self.menu_bar, tearoff=0)
if self.with_grid:
self.edit_menu.add_command(
label="Add Grid",
font=self.font,
command=self.add_grid,
accelerator="Ctrl-A",
state=tk.DISABLED,
state=self.tk.DISABLED,
)
self.edit_menu.add_command(
label="Remove Grid",
font=self.font,
command=self.remove_grid,
accelerator="Ctrl-D",
state=tk.DISABLED,
state=self.tk.DISABLED,
)
self.edit_menu.add_command(
label="Undo",
font=self.font,
command=self.undo,
accelerator="Ctrl-Z",
state=tk.DISABLED,
state=self.tk.DISABLED,
)
self.menu_bar.add_cascade(label="Edit", font=self.font, menu=self.edit_menu)

self.help_menu = tk.Menu(self.menu_bar, tearoff=0)
self.help_menu = self.tk.Menu(self.menu_bar, tearoff=0)
self.help_menu.add_command(
label="Help", font=self.font, command=self.show_help
)
self.menu_bar.add_cascade(label="Help", font=self.font, menu=self.help_menu)

if self.with_grid:
# Create a frame for the second menu and "Current Selection" OptionMenu
self.grid_selection_frame = tk.Frame(
self.grid_selection_frame = self.tk.Frame(
self.main_frame, bg=self.lighter_theme_color
)
self.grid_selection_frame.pack(fill=tk.X, pady=5)
self.grid_selection_frame.pack(fill=self.tk.X, pady=5)

# Add "Active Grid" ComboBox to the grid_selection_frame
self.grid_selection_menu_label = tk.Label(
self.grid_selection_menu_label = self.tk.Label(
self.grid_selection_frame,
text="Active Grid",
font=self.font,
Expand All @@ -610,13 +628,13 @@ def __init__(
self.grid_selection_menu_label.grid(row=0, column=0)
self.grid_selection_default_value = "Non-grid mode"
self.added_grid_id = ""
self.grid_selection_var = tk.StringVar(self.grid_selection_frame)
self.grid_selection_var = self.tk.StringVar(self.grid_selection_frame)
self.grid_selection_var.set(
self.grid_selection_default_value
) # Default value
self.grid_selection_options = [self.grid_selection_default_value]

self.grid_selection_menu = ttk.Combobox(
self.grid_selection_menu = self.ttk.Combobox(
self.grid_selection_frame,
textvariable=self.grid_selection_var,
values=self.grid_selection_options,
Expand All @@ -627,20 +645,20 @@ def __init__(

self.open_image_frame = None
if not self.image_path:
self.open_image_frame = tk.Frame(self.main_frame)
self.open_image_frame.pack(fill=tk.NONE, padx=150, pady=150)
self.open_button = tk.Button(
self.open_image_frame = self.tk.Frame(self.main_frame)
self.open_image_frame.pack(fill=self.tk.NONE, padx=150, pady=150)
self.open_button = self.tk.Button(
self.open_image_frame,
text="Open Image",
command=self.open_image,
font=tkFont.Font(family="courier 10 pitch", size=28),
font=self.tk.font.Font(family="courier 10 pitch", size=28),
bg=self.darker_theme_color,
fg="black",
)
self.open_button.grid(row=0, column=3)

self.canvas = tk.Canvas(self.main_frame, cursor="cross")
self.canvas.pack(fill=tk.BOTH, expand=True)
self.canvas = self.tk.Canvas(self.main_frame, cursor="cross")
self.canvas.pack(fill=self.tk.BOTH, expand=True)

self.original_image: Optional[Image.Image] = None # Store the original image
self.image_tk: Optional[ImageTk.PhotoImage] = None
Expand Down Expand Up @@ -707,7 +725,7 @@ def __init__(

def open_image(self, event=None):
"""Opens image."""
self.image_path = filedialog.askopenfilename(
self.image_path = self.tkFiledialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png")]
)
if self.image_path:
Expand Down Expand Up @@ -744,17 +762,17 @@ def load_image(self):
self.root.geometry(f"{self.original_width}x{self.original_height}")

self.update_image(self.original_image)
self.file_menu.entryconfig("Save JSON", state=tk.NORMAL)
self.file_menu.entryconfig("Save JSON As...", state=tk.NORMAL)
self.file_menu.entryconfig("Save JSON", state=self.tk.NORMAL)
self.file_menu.entryconfig("Save JSON As...", state=self.tk.NORMAL)
if self.with_grid:
self.edit_menu.entryconfig("Add Grid", state=tk.NORMAL)
self.edit_menu.entryconfig("Add Grid", state=self.tk.NORMAL)

def update_image(self, image):
"""Helper function to update the image on the canvas."""
self.current_width, self.current_height = image.size
self.image_tk = ImageTk.PhotoImage(image)
self.canvas.config(width=self.current_width, height=self.current_height)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.image_tk)
self.canvas.create_image(0, 0, anchor=self.tk.NW, image=self.image_tk)

# After updating the image, redraw the points and lines with scaled coordinates
self.update_displayed_points()
Expand Down Expand Up @@ -804,7 +822,7 @@ def on_resize(self, event):

# Resize the image to the new width and height
resized_image = self.original_image.resize(
(new_width, new_height), Image.BILINEAR # type: ignore
(new_width, new_height), Image.BILINEAR # type: ignore
)
self.update_image(resized_image)

Expand Down Expand Up @@ -865,7 +883,7 @@ def on_click(self, event):
self.draw_polygon(self.displayed_points[-self.num_vertices :])

if self.points or self.with_grid and self.grids:
self.edit_menu.entryconfig("Undo", state=tk.NORMAL)
self.edit_menu.entryconfig("Undo", state=self.tk.NORMAL)

def on_motion(self, event):
"""Processes actions related to mouse movement."""
Expand Down Expand Up @@ -1186,7 +1204,7 @@ def add_grid(self, event=None):
self.grids[new_grid_id] = Grid(str(new_grid_id))
self.added_grid_id = self.grid_idx_to_key(new_grid_id)
self.update_grid_menu()
self.edit_menu.entryconfig("Remove Grid", state=tk.NORMAL)
self.edit_menu.entryconfig("Remove Grid", state=self.tk.NORMAL)

def remove_grid(self, event=None):
"""Remove grid."""
Expand All @@ -1211,9 +1229,9 @@ def remove_grid(self, event=None):
self.grids.pop(cur_sel)
self.update_grid_menu()
if len(self.grids.values()) == 0:
self.edit_menu.entryconfig("Remove Grid", state=tk.DISABLED)
self.edit_menu.entryconfig("Remove Grid", state=self.tk.DISABLED)
if self.figures_empty():
self.edit_menu.entryconfig("Undo", state=tk.DISABLED)
self.edit_menu.entryconfig("Undo", state=self.tk.DISABLED)

def draw_grid_label(self, grid: Grid):
ref_point = grid.displayed_points[0]
Expand Down Expand Up @@ -1293,7 +1311,7 @@ def process_esc(self, event=None):
self.update_displayed_points()
self.redraw_polygons()
if self.figures_empty():
self.edit_menu.entryconfig("Undo", state=tk.DISABLED)
self.edit_menu.entryconfig("Undo", state=self.tk.DISABLED)

def undo(self, event=None):
"""Processes point deletion."""
Expand All @@ -1307,7 +1325,9 @@ def undo(self, event=None):
self.grids.pop(cur_sel)
self.update_grid_menu()
if len(self.grids.values()) == 0:
self.edit_menu.entryconfig("Remove Grid", state=tk.DISABLED)
self.edit_menu.entryconfig(
"Remove Grid", state=self.tk.DISABLED
)
else:
if grid.complete():
self.canvas.delete(
Expand Down Expand Up @@ -1371,7 +1391,7 @@ def undo(self, event=None):
)

if self.figures_empty():
self.edit_menu.entryconfig("Undo", state=tk.DISABLED)
self.edit_menu.entryconfig("Undo", state=self.tk.DISABLED)

def figures_complete(self):
return (
Expand All @@ -1398,7 +1418,9 @@ def check_completeness_on_save(self):
and any([not grid.complete() for grid in self.grids.values()])
)
):
messagebox.showerror("Error", "No points or insufficient points to save.")
self.tk.messagebox.showerror(
"Error", "No points or insufficient points to save."
)
return False
return True

Expand Down Expand Up @@ -1464,7 +1486,7 @@ def save_to_file(self, event=None):
if not self.check_completeness_on_save():
return

self.save_path = filedialog.asksaveasfilename(
self.save_path = self.tkFiledialog.asksaveasfilename(
initialfile=self.results_file_name,
defaultextension=".json",
filetypes=[("JSON files", "*.json")],
Expand All @@ -1475,7 +1497,7 @@ def save_to_file(self, event=None):
def on_close(self):
"""Prompt the user before closing the window if there are unsaved changes."""
if not self.figures_empty() and self.data_updated():
response = messagebox.askyesnocancel(
response = self.tk.messagebox.askyesnocancel(
"Unsaved Changes", "You have unsaved changes. Save before exiting?"
)
if response: # Yes: Save the changes
Expand All @@ -1500,20 +1522,20 @@ def get_help_message(self):
return help_message_polygon

def show_help(self):
help_window = Toplevel(self.root)
help_window = self.tk.Toplevel(self.root)
help_window.title(f"About {self.figure_type.capitalize()} Annotator")
help_window.geometry("1100x500") # Set the size of the window

scrollbar = Scrollbar(help_window)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
scrollbar = self.tk.Scrollbar(help_window)
scrollbar.pack(side=self.tk.RIGHT, fill=self.tk.Y)

help_text = Text(
help_window, wrap=tk.WORD, yscrollcommand=scrollbar.set, font=self.font
help_text = self.tk.Text(
help_window, wrap=self.tk.WORD, yscrollcommand=scrollbar.set, font=self.font
)

help_text.insert(tk.END, self.get_help_message())
help_text.config(state=tk.DISABLED)
help_text.pack(expand=True, fill=tk.BOTH)
help_text.insert(self.tk.END, self.get_help_message())
help_text.config(state=self.tk.DISABLED)
help_text.pack(expand=True, fill=self.tk.BOTH)
scrollbar.config(command=help_text.yview)


Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@ ffmpegcv>=0.3.15;platform_system!='Windows'
typing-extensions
jsonschema
apprise
tk
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"build": ["build"],
# external notifications
"notifications": ["apprise", "minio"],
# annotation tool
"annotator": ["tk"],
},
include_package_data=True,
)

0 comments on commit e80df6a

Please sign in to comment.