diff --git a/README.md b/README.md
index 508be8f..348f7de 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ Simple COCO Objects Viewer in Tkinter. Allows quick viewing on local machine.
| M, Ctrl + M | Toggle **M**asks |
| Ctrl + S | Save Current Image |
| Ctrl + Q, Ctrl + W | Exit Viewer |
+| Ctrl + + | Zoom In |
+| Ctrl + - | Zoom Out |
## Requirements
`python3` `PIL`
diff --git a/cocoviewer.py b/cocoviewer.py
index 470a4be..83b6efc 100644
--- a/cocoviewer.py
+++ b/cocoviewer.py
@@ -180,7 +180,9 @@ def draw_bboxes(draw, objects, labels, obj_categories, ignore, width, label_size
# TODO: Implement notification message as popup window
font = ImageFont.load_default()
- tw, th = draw.textsize(text, font)
+ _, _, tw, th = draw.textbbox((0, 0), text, font=font)
+ _, descent = font.getmetrics()
+ th += descent
tx0 = b[0]
ty0 = b[1] - th
@@ -458,6 +460,11 @@ def view_menu(self):
menu.colormenu.add_radiobutton(label="Objects", value=True)
menu.add_cascade(label="Coloring", menu=menu.colormenu)
+
+ menu.add_separator()
+ menu.add_command(label="Zoom In", accelerator="Ctrl++")
+ menu.add_command(label="Zoom Out", accelerator="Ctrl+-")
+
return menu
@@ -543,6 +550,8 @@ def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, slid
self.menu.view.entryconfigure("BBoxes", variable=self.bboxes_on_global, command=self.menu_view_bboxes)
self.menu.view.entryconfigure("Labels", variable=self.labels_on_global, command=self.menu_view_labels)
self.menu.view.entryconfigure("Masks", variable=self.masks_on_global, command=self.menu_view_masks)
+ self.menu.view.entryconfigure("Zoom In", command=self.zoom_in)
+ self.menu.view.entryconfigure("Zoom Out", command=self.zoom_out)
self.menu.view.colormenu.entryconfigure(
"Categories",
variable=self.coloring_on_global,
@@ -582,11 +591,14 @@ def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, slid
self.bind_events()
# Compose the very first image
+ self.zoom_factor = 1.0
self.current_composed_image = None
self.current_img_obj_categories = None
self.current_img_categories = None
self.update_img()
+ ZOOM_STEP = 1.1
+
def set_locals(self):
self.bboxes_on_local = self.bboxes_on_global.get()
self.labels_on_local = self.labels_on_global.get()
@@ -662,9 +674,13 @@ def update_img(self, local=True, width=None, alpha=None, label_size=None):
label_size=label_size,
)
- # Prepare PIL image for Tkinter
+ # Scale PIL image according to zoom_factor and prepare for Tkinter
img = self.current_composed_image
w, h = img.size
+ if self.zoom_factor != 1.0:
+ w = int(w * self.zoom_factor)
+ h = int(h * self.zoom_factor)
+ img = img.resize((w, h), Image.LANCZOS)
img = ImageTk.PhotoImage(img)
# Set image as current
@@ -839,6 +855,14 @@ def label_slider_status_update(self):
def masks_slider_status_update(self):
self.sliders.mask_slider.configure(state=tk.NORMAL if self.masks_on_local else tk.DISABLED)
+ def zoom_in(self, event=None):
+ self.zoom_factor *= self.ZOOM_STEP
+ self.update_img(local=False)
+
+ def zoom_out(self, event=None):
+ self.zoom_factor /= self.ZOOM_STEP
+ self.update_img(local=False)
+
def bind_events(self):
"""Binds events."""
# Navigation
@@ -866,23 +890,40 @@ def bind_events(self):
self.objects_panel.object_box.bind("<>", self.select_object)
self.image_panel.bind("", lambda e: self.image_panel.focus_set())
+ # Zoom
+ self.root.bind("", self.zoom_in)
+ self.root.bind("", self.zoom_out)
+
def print_info(message: str):
logging.info(message)
+def exit_with_error(message: str, root=None):
+ root.geometry("300x150") # app size when no data is provided
+ messagebox.showwarning("Warning!", message)
+ print_info("Exiting because of error: " + message)
+ root.destroy()
+ exit(1)
+
+
def main():
print_info("Starting...")
args = parser.parse_args()
root = tk.Tk()
root.title("COCO Viewer")
- if not args.images or not args.annotations:
- root.geometry("300x150") # app size when no data is provided
- messagebox.showwarning("Warning!", "Nothing to show.\nPlease specify a path to the COCO dataset!")
- print_info("Exiting...")
- root.destroy()
- return
+ if not args.annotations:
+ exit_with_error("No annotations file provided!", root)
+ elif not os.path.exists(args.annotations) or not os.path.isfile(args.annotations):
+ exit_with_error("Invalid annotations file path!", root)
+ elif not args.images:
+ try:
+ # Use image root from the annotations file if not provided via command line
+ args.images = json.loads(open(args.annotations).read())["image_root"]
+ except KeyError:
+ exit_with_error("No images folder provided neither via the annotations file "
+ "nor as a command line argument!", root)
data = Data(args.images, args.annotations)
statusbar = StatusBar(root)