improved UI
This commit is contained in:
@ -585,12 +585,119 @@ class TkAnnotationApp:
|
||||
return corner
|
||||
return None
|
||||
|
||||
def _find_handle_at_point(self, x: float, y: float) -> tuple[int, str] | None:
|
||||
"""Return (box_index, corner) if point hits a resize handle.
|
||||
|
||||
Unlike _find_box_at_point, this can hit even slightly outside the box
|
||||
bounds (since the drawn handle squares extend beyond the rectangle).
|
||||
When handles overlap, choose the smallest box by area.
|
||||
"""
|
||||
|
||||
if self.current_image_path is None:
|
||||
return None
|
||||
boxes = self.annotations.get(self.current_image_path.name, []) or []
|
||||
|
||||
handle_size = 10
|
||||
candidates: list[tuple[float, int, str]] = [] # (area, index, corner)
|
||||
for i, box in enumerate(boxes):
|
||||
bbox = box.get("bbox") if isinstance(box, dict) else None
|
||||
if not bbox or len(bbox) != 4:
|
||||
continue
|
||||
x1, y1, x2, y2 = bbox
|
||||
area = (x2 - x1) * (y2 - y1)
|
||||
dx1, dy1 = self._img_to_disp(x1, y1)
|
||||
dx2, dy2 = self._img_to_disp(x2, y2)
|
||||
corners = {
|
||||
"nw": (dx1, dy1),
|
||||
"ne": (dx2, dy1),
|
||||
"sw": (dx1, dy2),
|
||||
"se": (dx2, dy2),
|
||||
}
|
||||
for corner, (cx, cy) in corners.items():
|
||||
if cx - handle_size <= x <= cx + handle_size and cy - handle_size <= y <= cy + handle_size:
|
||||
candidates.append((area, i, corner))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda t: t[0])
|
||||
_, idx, corner = candidates[0]
|
||||
return idx, corner
|
||||
|
||||
def _apply_resize_drag(self, event_x: float, event_y: float) -> None:
|
||||
if (
|
||||
not self.dragging
|
||||
or self.drag_mode != "resize"
|
||||
or self.resize_corner is None
|
||||
or self.selected_box_index is None
|
||||
or self.drag_start is None
|
||||
or self.current_image_path is None
|
||||
):
|
||||
return
|
||||
|
||||
boxes = self.annotations.get(self.current_image_path.name, []) or []
|
||||
if self.selected_box_index >= len(boxes):
|
||||
return
|
||||
|
||||
bbox = boxes[self.selected_box_index].get("bbox")
|
||||
if not bbox or len(bbox) != 4:
|
||||
return
|
||||
|
||||
x1, y1, x2, y2 = bbox
|
||||
dx = event_x - self.drag_start[0]
|
||||
dy = event_y - self.drag_start[1]
|
||||
|
||||
# Convert to display coords
|
||||
dx1, dy1 = self._img_to_disp(x1, y1)
|
||||
dx2, dy2 = self._img_to_disp(x2, y2)
|
||||
|
||||
if "n" in self.resize_corner:
|
||||
dy1 += dy
|
||||
if "s" in self.resize_corner:
|
||||
dy2 += dy
|
||||
if "w" in self.resize_corner:
|
||||
dx1 += dx
|
||||
if "e" in self.resize_corner:
|
||||
dx2 += dx
|
||||
|
||||
# Convert back to image coords
|
||||
ix1, iy1 = self._disp_to_img(dx1, dy1)
|
||||
ix2, iy2 = self._disp_to_img(dx2, dy2)
|
||||
|
||||
# Ensure min size
|
||||
if abs(ix2 - ix1) < 2:
|
||||
ix2 = ix1 + 2 if ix2 > ix1 else ix1 - 2
|
||||
if abs(iy2 - iy1) < 2:
|
||||
iy2 = iy1 + 2 if iy2 > iy1 else iy1 - 2
|
||||
|
||||
boxes[self.selected_box_index]["bbox"] = [min(ix1, ix2), min(iy1, iy2), max(ix1, ix2), max(iy1, iy2)]
|
||||
self.drag_start = (event_x, event_y)
|
||||
self._redraw_boxes()
|
||||
|
||||
# ------------------------- Mouse interactions -------------------------
|
||||
|
||||
def _on_mouse_down(self, event: tk.Event) -> None:
|
||||
if self.current_image is None or self.current_image_path is None or self.transform is None:
|
||||
return
|
||||
|
||||
# First priority: if cursor is over a corner handle, start resizing (left-drag)
|
||||
handle_hit = self._find_handle_at_point(event.x, event.y)
|
||||
if handle_hit is not None:
|
||||
box_index, corner = handle_hit
|
||||
self.selected_box_index = box_index
|
||||
self.dragging = True
|
||||
self.drag_mode = "resize"
|
||||
self.resize_corner = corner
|
||||
self.drag_start = (event.x, event.y)
|
||||
self._is_selecting = False
|
||||
self._potential_select = None
|
||||
self._mouse_moved = False
|
||||
self._refresh_box_list()
|
||||
self._redraw_boxes()
|
||||
return
|
||||
|
||||
# Find which box (if any) is under the cursor
|
||||
box_index = self._find_box_at_point(event.x, event.y)
|
||||
|
||||
# Check if Ctrl is held for moving or resizing boxes
|
||||
if event.state & 0x4: # Ctrl key
|
||||
# First, check if clicking on a corner of the selected box for resizing
|
||||
@ -601,10 +708,11 @@ class TkAnnotationApp:
|
||||
self.drag_mode = 'resize'
|
||||
self.resize_corner = corner
|
||||
self.drag_start = (event.x, event.y)
|
||||
self._refresh_box_list()
|
||||
self._redraw_boxes()
|
||||
return
|
||||
|
||||
# Otherwise, select and move a box
|
||||
box_index = self._find_box_at_point(event.x, event.y)
|
||||
if box_index is not None:
|
||||
self.selected_box_index = box_index
|
||||
self.dragging = True
|
||||
@ -614,18 +722,7 @@ class TkAnnotationApp:
|
||||
self._redraw_boxes()
|
||||
return
|
||||
|
||||
# Normal mode: check if clicking on corner of selected box for resizing
|
||||
if self.selected_box_index is not None:
|
||||
corner = self._find_resize_corner(event.x, event.y, self.selected_box_index)
|
||||
if corner:
|
||||
self.dragging = True
|
||||
self.drag_mode = 'resize'
|
||||
self.resize_corner = corner
|
||||
self.drag_start = (event.x, event.y)
|
||||
return
|
||||
|
||||
# Normal mode: check if clicking inside a box to potentially select it
|
||||
box_index = self._find_box_at_point(event.x, event.y)
|
||||
if box_index is not None:
|
||||
self._potential_select = box_index
|
||||
self._is_selecting = True
|
||||
@ -640,6 +737,10 @@ class TkAnnotationApp:
|
||||
self._preview_rect_id = None
|
||||
|
||||
def _on_mouse_move(self, event: tk.Event) -> None:
|
||||
if self.dragging and self.drag_mode == "resize":
|
||||
self._apply_resize_drag(event.x, event.y)
|
||||
return
|
||||
|
||||
if self.dragging and self.drag_mode == 'move' and self.drag_start and self.selected_box_index is not None:
|
||||
# Move the box
|
||||
dx = event.x - self.drag_start[0]
|
||||
@ -687,7 +788,10 @@ class TkAnnotationApp:
|
||||
self.dragging = False
|
||||
self.drag_mode = None
|
||||
self.drag_start = None
|
||||
self.resize_corner = None
|
||||
self._save_annotations()
|
||||
self._refresh_box_list()
|
||||
self._redraw_boxes()
|
||||
return
|
||||
|
||||
if self._is_selecting:
|
||||
@ -753,44 +857,7 @@ class TkAnnotationApp:
|
||||
self._redraw_boxes()
|
||||
|
||||
def _on_right_mouse_move(self, event: tk.Event) -> None:
|
||||
if not self.dragging or self.drag_mode != 'resize' or self.resize_corner is None or self.selected_box_index is None or self.drag_start is None:
|
||||
return
|
||||
if self.current_image_path is None:
|
||||
return
|
||||
boxes = self.annotations.get(self.current_image_path.name, []) or []
|
||||
if self.selected_box_index >= len(boxes):
|
||||
return
|
||||
bbox = boxes[self.selected_box_index]["bbox"]
|
||||
x1, y1, x2, y2 = bbox
|
||||
dx = event.x - self.drag_start[0]
|
||||
dy = event.y - self.drag_start[1]
|
||||
|
||||
# Convert to display coords
|
||||
dx1, dy1 = self._img_to_disp(x1, y1)
|
||||
dx2, dy2 = self._img_to_disp(x2, y2)
|
||||
|
||||
if 'n' in self.resize_corner:
|
||||
dy1 += dy
|
||||
if 's' in self.resize_corner:
|
||||
dy2 += dy
|
||||
if 'w' in self.resize_corner:
|
||||
dx1 += dx
|
||||
if 'e' in self.resize_corner:
|
||||
dx2 += dx
|
||||
|
||||
# Convert back to image coords
|
||||
ix1, iy1 = self._disp_to_img(dx1, dy1)
|
||||
ix2, iy2 = self._disp_to_img(dx2, dy2)
|
||||
|
||||
# Ensure min size
|
||||
if abs(ix2 - ix1) < 2:
|
||||
ix2 = ix1 + 2 if ix2 > ix1 else ix1 - 2
|
||||
if abs(iy2 - iy1) < 2:
|
||||
iy2 = iy1 + 2 if iy2 > iy1 else iy1 - 2
|
||||
|
||||
boxes[self.selected_box_index]["bbox"] = [min(ix1, ix2), min(iy1, iy2), max(ix1, ix2), max(iy1, iy2)]
|
||||
self.drag_start = (event.x, event.y)
|
||||
self._redraw_boxes()
|
||||
self._apply_resize_drag(event.x, event.y)
|
||||
|
||||
def _on_right_mouse_up(self, event: tk.Event) -> None:
|
||||
if self.dragging and self.drag_mode == 'resize':
|
||||
@ -799,6 +866,8 @@ class TkAnnotationApp:
|
||||
self.resize_corner = None
|
||||
self.drag_start = None
|
||||
self._save_annotations()
|
||||
self._refresh_box_list()
|
||||
self._redraw_boxes()
|
||||
|
||||
def _on_delete_key(self, event: tk.Event) -> None:
|
||||
self.delete_selected_box()
|
||||
|
||||
Reference in New Issue
Block a user