zoomable mosiac
This commit is contained in:
parent
d7321c5ef4
commit
a7c2d89cc0
2 changed files with 215 additions and 14 deletions
225
src/gui.py
225
src/gui.py
|
|
@ -353,14 +353,14 @@ class AppGUI:
|
||||||
|
|
||||||
# Max Width
|
# Max Width
|
||||||
ttk.Label(inner2, text="Max W:").pack(side=tk.LEFT)
|
ttk.Label(inner2, text="Max W:").pack(side=tk.LEFT)
|
||||||
self.max_width_var = tk.IntVar(value=1000)
|
self.max_width_var = tk.IntVar(value=840)
|
||||||
self.max_width_entry = ttk.Entry(inner2, textvariable=self.max_width_var, width=7)
|
self.max_width_entry = ttk.Entry(inner2, textvariable=self.max_width_var, width=7)
|
||||||
self.max_width_entry.pack(side=tk.LEFT, padx=(2, 5))
|
self.max_width_entry.pack(side=tk.LEFT, padx=(2, 5))
|
||||||
self.max_width_entry.bind('<Return>', lambda e: self._update_stitch_config())
|
self.max_width_entry.bind('<Return>', lambda e: self._update_stitch_config())
|
||||||
|
|
||||||
# Max Height
|
# Max Height
|
||||||
ttk.Label(inner2, text="Max H:").pack(side=tk.LEFT)
|
ttk.Label(inner2, text="Max H:").pack(side=tk.LEFT)
|
||||||
self.max_height_var = tk.IntVar(value=1000)
|
self.max_height_var = tk.IntVar(value=1280)
|
||||||
self.max_height_entry = ttk.Entry(inner2, textvariable=self.max_height_var, width=7)
|
self.max_height_entry = ttk.Entry(inner2, textvariable=self.max_height_var, width=7)
|
||||||
self.max_height_entry.pack(side=tk.LEFT, padx=(2, 5))
|
self.max_height_entry.pack(side=tk.LEFT, padx=(2, 5))
|
||||||
self.max_height_entry.bind('<Return>', lambda e: self._update_stitch_config())
|
self.max_height_entry.bind('<Return>', lambda e: self._update_stitch_config())
|
||||||
|
|
@ -856,6 +856,10 @@ class AppGUI:
|
||||||
# Mosaic Window
|
# Mosaic Window
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Mosaic Window
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
def _show_mosaic_window(self):
|
def _show_mosaic_window(self):
|
||||||
if self.mosaic_window is not None:
|
if self.mosaic_window is not None:
|
||||||
self.mosaic_window.lift()
|
self.mosaic_window.lift()
|
||||||
|
|
@ -867,9 +871,30 @@ class AppGUI:
|
||||||
self.mosaic_window.geometry("600x800")
|
self.mosaic_window.geometry("600x800")
|
||||||
self.mosaic_window.protocol("WM_DELETE_WINDOW", self._close_mosaic_window)
|
self.mosaic_window.protocol("WM_DELETE_WINDOW", self._close_mosaic_window)
|
||||||
|
|
||||||
self.mosaic_label = ttk.Label(self.mosaic_window, text="No mosaic yet")
|
# Canvas for zoomable/pannable mosaic
|
||||||
self.mosaic_label.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
self.mosaic_canvas = tk.Canvas(self.mosaic_window, bg='#333333', highlightthickness=0)
|
||||||
|
self.mosaic_canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Zoom and pan state
|
||||||
|
self.mosaic_zoom = 1.0
|
||||||
|
self.mosaic_pan_x = 0
|
||||||
|
self.mosaic_pan_y = 0
|
||||||
|
self.mosaic_drag_start = None
|
||||||
|
self.mosaic_full_image = None # Store full resolution for zooming
|
||||||
|
|
||||||
|
# Bind mouse events
|
||||||
|
self.mosaic_canvas.bind('<MouseWheel>', self._on_mosaic_scroll) # Windows
|
||||||
|
self.mosaic_canvas.bind('<Button-4>', self._on_mosaic_scroll) # Linux scroll up
|
||||||
|
self.mosaic_canvas.bind('<Button-5>', self._on_mosaic_scroll) # Linux scroll down
|
||||||
|
self.mosaic_canvas.bind('<ButtonPress-1>', self._on_mosaic_drag_start)
|
||||||
|
self.mosaic_canvas.bind('<B1-Motion>', self._on_mosaic_drag)
|
||||||
|
self.mosaic_canvas.bind('<ButtonRelease-1>', self._on_mosaic_drag_end)
|
||||||
|
self.mosaic_canvas.bind('<Configure>', lambda e: self._render_mosaic())
|
||||||
|
|
||||||
|
# Double-click to reset view
|
||||||
|
self.mosaic_canvas.bind('<Double-Button-1>', self._reset_mosaic_view)
|
||||||
|
|
||||||
|
# Button frame
|
||||||
btn_frame = ttk.Frame(self.mosaic_window)
|
btn_frame = ttk.Frame(self.mosaic_window)
|
||||||
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
|
@ -877,31 +902,204 @@ class AppGUI:
|
||||||
command=self._save_mosaic).pack(side=tk.LEFT, padx=5)
|
command=self._save_mosaic).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(btn_frame, text="Refresh",
|
ttk.Button(btn_frame, text="Refresh",
|
||||||
command=self._update_mosaic_window).pack(side=tk.LEFT, padx=5)
|
command=self._update_mosaic_window).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(btn_frame, text="Fit",
|
||||||
|
command=self._reset_mosaic_view).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(btn_frame, text="1:1",
|
||||||
|
command=self._mosaic_zoom_100).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(btn_frame, text="Clear",
|
ttk.Button(btn_frame, text="Clear",
|
||||||
command=self._clear_mosaic).pack(side=tk.LEFT, padx=5)
|
command=self._clear_mosaic).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# Zoom label
|
||||||
|
self.mosaic_zoom_label = ttk.Label(btn_frame, text="100%", width=6)
|
||||||
|
self.mosaic_zoom_label.pack(side=tk.RIGHT, padx=5)
|
||||||
|
ttk.Label(btn_frame, text="Zoom:").pack(side=tk.RIGHT)
|
||||||
|
|
||||||
self._update_mosaic_window()
|
self._update_mosaic_window()
|
||||||
|
|
||||||
|
def _on_mosaic_scroll(self, event):
|
||||||
|
"""Handle mouse wheel zoom"""
|
||||||
|
if self.mosaic_full_image is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get mouse position relative to canvas
|
||||||
|
canvas_x = self.mosaic_canvas.canvasx(event.x)
|
||||||
|
canvas_y = self.mosaic_canvas.canvasy(event.y)
|
||||||
|
|
||||||
|
# Determine zoom direction
|
||||||
|
if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0):
|
||||||
|
scale_factor = 1.15
|
||||||
|
elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0):
|
||||||
|
scale_factor = 1 / 1.15
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_zoom = self.mosaic_zoom
|
||||||
|
self.mosaic_zoom *= scale_factor
|
||||||
|
|
||||||
|
# Clamp zoom
|
||||||
|
self.mosaic_zoom = max(0.1, min(10.0, self.mosaic_zoom))
|
||||||
|
|
||||||
|
# Adjust pan to zoom toward mouse position
|
||||||
|
if old_zoom != self.mosaic_zoom:
|
||||||
|
# Calculate the point in image space under the mouse
|
||||||
|
img_x = (canvas_x - self.mosaic_pan_x) / old_zoom
|
||||||
|
img_y = (canvas_y - self.mosaic_pan_y) / old_zoom
|
||||||
|
|
||||||
|
# Adjust pan so that same image point stays under mouse
|
||||||
|
self.mosaic_pan_x = canvas_x - img_x * self.mosaic_zoom
|
||||||
|
self.mosaic_pan_y = canvas_y - img_y * self.mosaic_zoom
|
||||||
|
|
||||||
|
self._render_mosaic()
|
||||||
|
|
||||||
|
def _on_mosaic_drag_start(self, event):
|
||||||
|
"""Start panning"""
|
||||||
|
self.mosaic_drag_start = (event.x, event.y)
|
||||||
|
self.mosaic_canvas.config(cursor='fleur')
|
||||||
|
|
||||||
|
def _on_mosaic_drag(self, event):
|
||||||
|
"""Handle panning"""
|
||||||
|
if self.mosaic_drag_start is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
dx = event.x - self.mosaic_drag_start[0]
|
||||||
|
dy = event.y - self.mosaic_drag_start[1]
|
||||||
|
|
||||||
|
self.mosaic_pan_x += dx
|
||||||
|
self.mosaic_pan_y += dy
|
||||||
|
|
||||||
|
self.mosaic_drag_start = (event.x, event.y)
|
||||||
|
self._render_mosaic()
|
||||||
|
|
||||||
|
def _on_mosaic_drag_end(self, event):
|
||||||
|
"""End panning"""
|
||||||
|
self.mosaic_drag_start = None
|
||||||
|
self.mosaic_canvas.config(cursor='')
|
||||||
|
|
||||||
|
def _reset_mosaic_view(self, event=None):
|
||||||
|
"""Reset zoom and pan to fit image in canvas"""
|
||||||
|
if self.mosaic_full_image is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
canvas_w = self.mosaic_canvas.winfo_width()
|
||||||
|
canvas_h = self.mosaic_canvas.winfo_height()
|
||||||
|
img_h, img_w = self.mosaic_full_image.shape[:2]
|
||||||
|
|
||||||
|
if img_w == 0 or img_h == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate zoom to fit
|
||||||
|
self.mosaic_zoom = min(canvas_w / img_w, canvas_h / img_h)
|
||||||
|
|
||||||
|
# Center the image
|
||||||
|
scaled_w = img_w * self.mosaic_zoom
|
||||||
|
scaled_h = img_h * self.mosaic_zoom
|
||||||
|
self.mosaic_pan_x = (canvas_w - scaled_w) / 2
|
||||||
|
self.mosaic_pan_y = (canvas_h - scaled_h) / 2
|
||||||
|
|
||||||
|
self._render_mosaic()
|
||||||
|
|
||||||
|
def _mosaic_zoom_100(self):
|
||||||
|
"""Set zoom to 100% (1:1 pixels)"""
|
||||||
|
if self.mosaic_full_image is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
canvas_w = self.mosaic_canvas.winfo_width()
|
||||||
|
canvas_h = self.mosaic_canvas.winfo_height()
|
||||||
|
img_h, img_w = self.mosaic_full_image.shape[:2]
|
||||||
|
|
||||||
|
self.mosaic_zoom = 1.0
|
||||||
|
|
||||||
|
# Center the image
|
||||||
|
self.mosaic_pan_x = (canvas_w - img_w) / 2
|
||||||
|
self.mosaic_pan_y = (canvas_h - img_h) / 2
|
||||||
|
|
||||||
|
self._render_mosaic()
|
||||||
|
|
||||||
|
def _render_mosaic(self):
|
||||||
|
"""Render the mosaic at current zoom/pan"""
|
||||||
|
if self.mosaic_window is None or self.mosaic_full_image is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
canvas_w = self.mosaic_canvas.winfo_width()
|
||||||
|
canvas_h = self.mosaic_canvas.winfo_height()
|
||||||
|
|
||||||
|
if canvas_w <= 1 or canvas_h <= 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
img_h, img_w = self.mosaic_full_image.shape[:2]
|
||||||
|
|
||||||
|
# Calculate visible region in image space
|
||||||
|
view_x1 = max(0, int(-self.mosaic_pan_x / self.mosaic_zoom))
|
||||||
|
view_y1 = max(0, int(-self.mosaic_pan_y / self.mosaic_zoom))
|
||||||
|
view_x2 = min(img_w, int((canvas_w - self.mosaic_pan_x) / self.mosaic_zoom))
|
||||||
|
view_y2 = min(img_h, int((canvas_h - self.mosaic_pan_y) / self.mosaic_zoom))
|
||||||
|
|
||||||
|
if view_x2 <= view_x1 or view_y2 <= view_y1:
|
||||||
|
self.mosaic_canvas.delete('all')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract visible region
|
||||||
|
visible = self.mosaic_full_image[view_y1:view_y2, view_x1:view_x2]
|
||||||
|
|
||||||
|
# Scale the visible region
|
||||||
|
new_w = int((view_x2 - view_x1) * self.mosaic_zoom)
|
||||||
|
new_h = int((view_y2 - view_y1) * self.mosaic_zoom)
|
||||||
|
|
||||||
|
if new_w <= 0 or new_h <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use appropriate interpolation
|
||||||
|
interp = cv2.INTER_NEAREST if self.mosaic_zoom > 1 else cv2.INTER_AREA
|
||||||
|
scaled = cv2.resize(visible, (new_w, new_h), interpolation=interp)
|
||||||
|
|
||||||
|
# Convert to PhotoImage
|
||||||
|
rgb = cv2.cvtColor(scaled, cv2.COLOR_BGR2RGB)
|
||||||
|
img = Image.fromarray(rgb)
|
||||||
|
self.mosaic_photo = ImageTk.PhotoImage(image=img)
|
||||||
|
|
||||||
|
# Calculate position on canvas
|
||||||
|
pos_x = max(0, self.mosaic_pan_x + view_x1 * self.mosaic_zoom)
|
||||||
|
pos_y = max(0, self.mosaic_pan_y + view_y1 * self.mosaic_zoom)
|
||||||
|
|
||||||
|
# Update canvas
|
||||||
|
self.mosaic_canvas.delete('all')
|
||||||
|
self.mosaic_canvas.create_image(pos_x, pos_y, anchor=tk.NW, image=self.mosaic_photo)
|
||||||
|
|
||||||
|
# Update zoom label
|
||||||
|
self.mosaic_zoom_label.config(text=f"{self.mosaic_zoom * 100:.0f}%")
|
||||||
|
|
||||||
def _update_mosaic_window(self):
|
def _update_mosaic_window(self):
|
||||||
if self.mosaic_window is None:
|
if self.mosaic_window is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get mosaic from appropriate scanner
|
# Get mosaic from appropriate scanner (full resolution for zooming)
|
||||||
mosaic = None
|
mosaic = None
|
||||||
if self.scanner_mode == 'stitch' and self.stitch_scanner:
|
if self.scanner_mode == 'stitch' and self.stitch_scanner:
|
||||||
mosaic = self.stitch_scanner.get_mosaic_preview(max_size=580)
|
mosaic = self.stitch_scanner.mosaic # Get full resolution
|
||||||
|
if mosaic is None:
|
||||||
|
# Try preview if no full mosaic
|
||||||
|
mosaic = self.stitch_scanner.get_mosaic_preview(max_size=2000)
|
||||||
elif self.scanner and self.scanner.tiles:
|
elif self.scanner and self.scanner.tiles:
|
||||||
mosaic = self.scanner.get_mosaic_preview(max_size=580)
|
mosaic = self.scanner.build_mosaic(scale=1.0)
|
||||||
|
|
||||||
if mosaic is None:
|
if mosaic is None:
|
||||||
|
self.mosaic_canvas.delete('all')
|
||||||
|
self.mosaic_canvas.create_text(
|
||||||
|
self.mosaic_canvas.winfo_width() // 2,
|
||||||
|
self.mosaic_canvas.winfo_height() // 2,
|
||||||
|
text="No mosaic yet", fill='white', font=('Arial', 14)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
mosaic_rgb = cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)
|
# Store full image and render
|
||||||
img = Image.fromarray(mosaic_rgb)
|
self.mosaic_full_image = mosaic.copy()
|
||||||
imgtk = ImageTk.PhotoImage(image=img)
|
|
||||||
|
|
||||||
self.mosaic_label.imgtk = imgtk
|
# If first time or image size changed significantly, reset view
|
||||||
self.mosaic_label.config(image=imgtk, text="")
|
if not hasattr(self, '_last_mosaic_size') or self._last_mosaic_size != mosaic.shape[:2]:
|
||||||
|
self._last_mosaic_size = mosaic.shape[:2]
|
||||||
|
self.mosaic_window.after(10, self._reset_mosaic_view)
|
||||||
|
else:
|
||||||
|
self._render_mosaic()
|
||||||
|
|
||||||
# Update size label
|
# Update size label
|
||||||
if self.stitch_scanner:
|
if self.stitch_scanner:
|
||||||
|
|
@ -912,6 +1110,9 @@ class AppGUI:
|
||||||
if self.mosaic_window:
|
if self.mosaic_window:
|
||||||
self.mosaic_window.destroy()
|
self.mosaic_window.destroy()
|
||||||
self.mosaic_window = None
|
self.mosaic_window = None
|
||||||
|
self.mosaic_full_image = None
|
||||||
|
if hasattr(self, '_last_mosaic_size'):
|
||||||
|
del self._last_mosaic_size
|
||||||
|
|
||||||
def _save_mosaic(self):
|
def _save_mosaic(self):
|
||||||
filename = filedialog.asksaveasfilename(
|
filename = filedialog.asksaveasfilename(
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ class StitchingScanner:
|
||||||
blend_w = min(blend_width, w_strip, w_base)
|
blend_w = min(blend_width, w_strip, w_base)
|
||||||
|
|
||||||
|
|
||||||
DEBUG_SHIFT_RIGHT = 0 # Positive = shift strip right
|
DEBUG_SHIFT_RIGHT = -10 # Positive = shift strip right
|
||||||
DEBUG_SHIFT_UP = 0 # Positive = shift strip up
|
DEBUG_SHIFT_UP = -50 # Positive = shift strip up
|
||||||
|
|
||||||
if append_right:
|
if append_right:
|
||||||
# Expand mosaic to the right
|
# Expand mosaic to the right
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue