Adding auto zoom when result is loaded
This commit is contained in:
@@ -18,7 +18,7 @@ from PySide6.QtGui import (
|
||||
QPaintEvent,
|
||||
QPolygonF,
|
||||
)
|
||||
from PySide6.QtCore import Qt, QEvent, Signal, QPoint, QPointF, QRect
|
||||
from PySide6.QtCore import Qt, QEvent, Signal, QPoint, QPointF, QRect, QTimer
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from src.utils.image import Image, ImageLoadError
|
||||
@@ -79,9 +79,7 @@ def rdp(points: List[Tuple[float, float]], epsilon: float) -> List[Tuple[float,
|
||||
return [start, end]
|
||||
|
||||
|
||||
def simplify_polyline(
|
||||
points: List[Tuple[float, float]], epsilon: float
|
||||
) -> List[Tuple[float, float]]:
|
||||
def simplify_polyline(points: List[Tuple[float, float]], epsilon: float) -> List[Tuple[float, float]]:
|
||||
"""
|
||||
Simplify a polyline with RDP while preserving closure semantics.
|
||||
|
||||
@@ -145,6 +143,10 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self.zoom_step = 0.1
|
||||
self.zoom_wheel_step = 0.15
|
||||
|
||||
# Auto-fit behavior (opt-in): when enabled, newly loaded images (and resizes)
|
||||
# will scale to fill the available viewport while preserving aspect ratio.
|
||||
self._auto_fit_to_view: bool = False
|
||||
|
||||
# Drawing / interaction state
|
||||
self.is_drawing = False
|
||||
self.polyline_enabled = False
|
||||
@@ -175,6 +177,35 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def set_auto_fit_to_view(self, enabled: bool):
|
||||
"""Enable/disable automatic zoom-to-fit behavior."""
|
||||
self._auto_fit_to_view = bool(enabled)
|
||||
if self._auto_fit_to_view and self.original_pixmap is not None:
|
||||
QTimer.singleShot(0, self.fit_to_view)
|
||||
|
||||
def fit_to_view(self, padding_px: int = 6):
|
||||
"""Zoom the image so it fits the scroll area's viewport (aspect preserved)."""
|
||||
if self.original_pixmap is None:
|
||||
return
|
||||
|
||||
viewport = self.scroll_area.viewport().size()
|
||||
available_w = max(1, int(viewport.width()) - int(padding_px))
|
||||
available_h = max(1, int(viewport.height()) - int(padding_px))
|
||||
|
||||
img_w = max(1, int(self.original_pixmap.width()))
|
||||
img_h = max(1, int(self.original_pixmap.height()))
|
||||
|
||||
scale_w = available_w / img_w
|
||||
scale_h = available_h / img_h
|
||||
new_scale = min(scale_w, scale_h)
|
||||
new_scale = max(self.zoom_min, min(self.zoom_max, float(new_scale)))
|
||||
|
||||
if abs(new_scale - self.zoom_scale) < 1e-4:
|
||||
return
|
||||
|
||||
self.zoom_scale = new_scale
|
||||
self._apply_zoom()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup user interface."""
|
||||
layout = QVBoxLayout()
|
||||
@@ -187,9 +218,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
self.canvas_label = QLabel("No image loaded")
|
||||
self.canvas_label.setAlignment(Qt.AlignCenter)
|
||||
self.canvas_label.setStyleSheet(
|
||||
"QLabel { background-color: #2b2b2b; color: #888; }"
|
||||
)
|
||||
self.canvas_label.setStyleSheet("QLabel { background-color: #2b2b2b; color: #888; }")
|
||||
self.canvas_label.setScaledContents(False)
|
||||
self.canvas_label.setMouseTracking(True)
|
||||
|
||||
@@ -212,9 +241,18 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self.zoom_scale = 1.0
|
||||
self.clear_annotations()
|
||||
self._display_image()
|
||||
logger.debug(
|
||||
f"Loaded image into annotation canvas: {image.width}x{image.height}"
|
||||
)
|
||||
|
||||
# Defer fit-to-view until the widget has a valid viewport size.
|
||||
if self._auto_fit_to_view:
|
||||
QTimer.singleShot(0, self.fit_to_view)
|
||||
|
||||
logger.debug(f"Loaded image into annotation canvas: {image.width}x{image.height}")
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Optionally keep the image fitted when the widget is resized."""
|
||||
super().resizeEvent(event)
|
||||
if self._auto_fit_to_view and self.original_pixmap is not None:
|
||||
QTimer.singleShot(0, self.fit_to_view)
|
||||
|
||||
def clear(self):
|
||||
"""Clear the displayed image and all annotations."""
|
||||
@@ -289,22 +327,14 @@ class AnnotationCanvasWidget(QWidget):
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
Qt.KeepAspectRatio,
|
||||
(
|
||||
Qt.SmoothTransformation
|
||||
if self.zoom_scale >= 1.0
|
||||
else Qt.FastTransformation
|
||||
),
|
||||
(Qt.SmoothTransformation if self.zoom_scale >= 1.0 else Qt.FastTransformation),
|
||||
)
|
||||
|
||||
scaled_annotations = self.annotation_pixmap.scaled(
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
Qt.KeepAspectRatio,
|
||||
(
|
||||
Qt.SmoothTransformation
|
||||
if self.zoom_scale >= 1.0
|
||||
else Qt.FastTransformation
|
||||
),
|
||||
(Qt.SmoothTransformation if self.zoom_scale >= 1.0 else Qt.FastTransformation),
|
||||
)
|
||||
|
||||
# Composite image and annotations
|
||||
@@ -390,16 +420,11 @@ class AnnotationCanvasWidget(QWidget):
|
||||
y = (pos.y() - offset_y) / self.zoom_scale
|
||||
|
||||
# Check bounds
|
||||
if (
|
||||
0 <= x < self.original_pixmap.width()
|
||||
and 0 <= y < self.original_pixmap.height()
|
||||
):
|
||||
if 0 <= x < self.original_pixmap.width() and 0 <= y < self.original_pixmap.height():
|
||||
return (int(x), int(y))
|
||||
return None
|
||||
|
||||
def _find_polyline_at(
|
||||
self, img_x: float, img_y: float, threshold_px: float = 5.0
|
||||
) -> Optional[int]:
|
||||
def _find_polyline_at(self, img_x: float, img_y: float, threshold_px: float = 5.0) -> Optional[int]:
|
||||
"""
|
||||
Find index of polyline whose geometry is within threshold_px of (img_x, img_y).
|
||||
Returns the index in self.polylines, or None if none is close enough.
|
||||
@@ -421,9 +446,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
# Precise distance to all segments
|
||||
for (x1, y1), (x2, y2) in zip(polyline[:-1], polyline[1:]):
|
||||
d = perpendicular_distance(
|
||||
(img_x, img_y), (float(x1), float(y1)), (float(x2), float(y2))
|
||||
)
|
||||
d = perpendicular_distance((img_x, img_y), (float(x1), float(y1)), (float(x2), float(y2)))
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
best_index = idx
|
||||
@@ -624,11 +647,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
def mouseMoveEvent(self, event: QMouseEvent):
|
||||
"""Handle mouse move events for drawing."""
|
||||
if (
|
||||
not self.is_drawing
|
||||
or not self.polyline_enabled
|
||||
or self.annotation_pixmap is None
|
||||
):
|
||||
if not self.is_drawing or not self.polyline_enabled or self.annotation_pixmap is None:
|
||||
super().mouseMoveEvent(event)
|
||||
return
|
||||
|
||||
@@ -688,15 +707,10 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
if len(simplified) >= 2:
|
||||
# Store polyline and redraw all annotations
|
||||
self._add_polyline(
|
||||
simplified, self.polyline_pen_color, self.polyline_pen_width
|
||||
)
|
||||
self._add_polyline(simplified, self.polyline_pen_color, self.polyline_pen_width)
|
||||
|
||||
# Convert to normalized coordinates for metadata + signal
|
||||
normalized_stroke = [
|
||||
self._image_to_normalized_coords(int(x), int(y))
|
||||
for (x, y) in simplified
|
||||
]
|
||||
normalized_stroke = [self._image_to_normalized_coords(int(x), int(y)) for (x, y) in simplified]
|
||||
self.all_strokes.append(
|
||||
{
|
||||
"points": normalized_stroke,
|
||||
@@ -709,8 +723,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
# Emit signal with normalized coordinates
|
||||
self.annotation_drawn.emit(normalized_stroke)
|
||||
logger.debug(
|
||||
f"Completed stroke with {len(simplified)} points "
|
||||
f"(normalized len={len(normalized_stroke)})"
|
||||
f"Completed stroke with {len(simplified)} points " f"(normalized len={len(normalized_stroke)})"
|
||||
)
|
||||
|
||||
self.current_stroke = []
|
||||
@@ -750,9 +763,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
# Store polyline as [y_norm, x_norm] to match DB convention and
|
||||
# the expectations of draw_saved_polyline().
|
||||
normalized_polyline = [
|
||||
[y / img_height, x / img_width] for (x, y) in polyline
|
||||
]
|
||||
normalized_polyline = [[y / img_height, x / img_width] for (x, y) in polyline]
|
||||
|
||||
logger.debug(
|
||||
f"Polyline {idx}: {len(polyline)} points, "
|
||||
@@ -772,7 +783,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self,
|
||||
polyline: List[List[float]],
|
||||
color: str,
|
||||
width: int = 3,
|
||||
width: int = 1,
|
||||
annotation_id: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
@@ -810,17 +821,13 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
# Store and redraw using common pipeline
|
||||
pen_color = QColor(color)
|
||||
pen_color.setAlpha(128) # Add semi-transparency
|
||||
pen_color.setAlpha(255) # Add semi-transparency
|
||||
self._add_polyline(img_coords, pen_color, width, annotation_id=annotation_id)
|
||||
|
||||
# Store in all_strokes for consistency (uses normalized coordinates)
|
||||
self.all_strokes.append(
|
||||
{"points": polyline, "color": color, "alpha": 128, "width": width}
|
||||
)
|
||||
self.all_strokes.append({"points": polyline, "color": color, "alpha": 128, "width": width})
|
||||
|
||||
logger.debug(
|
||||
f"Drew saved polyline with {len(polyline)} points in color {color}"
|
||||
)
|
||||
logger.debug(f"Drew saved polyline with {len(polyline)} points in color {color}")
|
||||
|
||||
def draw_saved_bbox(
|
||||
self,
|
||||
@@ -844,9 +851,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
return
|
||||
|
||||
if len(bbox) != 4:
|
||||
logger.warning(
|
||||
f"Invalid bounding box format: expected 4 values, got {len(bbox)}"
|
||||
)
|
||||
logger.warning(f"Invalid bounding box format: expected 4 values, got {len(bbox)}")
|
||||
return
|
||||
|
||||
# Convert normalized coordinates to image coordinates (for logging/debug)
|
||||
@@ -867,15 +872,11 @@ class AnnotationCanvasWidget(QWidget):
|
||||
# in _redraw_annotations() together with all polylines.
|
||||
pen_color = QColor(color)
|
||||
pen_color.setAlpha(128) # Add semi-transparency
|
||||
self.bboxes.append(
|
||||
[float(x_min_norm), float(y_min_norm), float(x_max_norm), float(y_max_norm)]
|
||||
)
|
||||
self.bboxes.append([float(x_min_norm), float(y_min_norm), float(x_max_norm), float(y_max_norm)])
|
||||
self.bbox_meta.append({"color": pen_color, "width": int(width), "label": label})
|
||||
|
||||
# Store in all_strokes for consistency
|
||||
self.all_strokes.append(
|
||||
{"bbox": bbox, "color": color, "alpha": 128, "width": width, "label": label}
|
||||
)
|
||||
self.all_strokes.append({"bbox": bbox, "color": color, "alpha": 128, "width": width, "label": label})
|
||||
|
||||
# Redraw overlay (polylines + all bounding boxes)
|
||||
self._redraw_annotations()
|
||||
|
||||
Reference in New Issue
Block a user