Compare commits
No commits in common. "VerticalStitch" and "master" have entirely different histories.
VerticalSt
...
master
2 changed files with 40 additions and 1093 deletions
229
src/gui.py
229
src/gui.py
|
|
@ -280,6 +280,10 @@ class AppGUI:
|
||||||
)
|
)
|
||||||
self.scan_stop_btn.pack(side=tk.LEFT, padx=2)
|
self.scan_stop_btn.pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Test button
|
||||||
|
ttk.Button(btn_frame, text="Test", width=4,
|
||||||
|
command=self._test_displacement).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
# Mosaic button
|
# Mosaic button
|
||||||
ttk.Button(sf, text="Mosaic", width=6,
|
ttk.Button(sf, text="Mosaic", width=6,
|
||||||
command=self._show_mosaic_window).pack(side=tk.RIGHT, padx=2)
|
command=self._show_mosaic_window).pack(side=tk.RIGHT, padx=2)
|
||||||
|
|
@ -349,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=960)
|
self.max_width_var = tk.IntVar(value=5000)
|
||||||
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=1280)
|
self.max_height_var = tk.IntVar(value=5000)
|
||||||
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())
|
||||||
|
|
@ -852,10 +856,6 @@ 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,30 +867,9 @@ 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)
|
||||||
|
|
||||||
# Canvas for zoomable/pannable mosaic
|
self.mosaic_label = ttk.Label(self.mosaic_window, text="No mosaic yet")
|
||||||
self.mosaic_canvas = tk.Canvas(self.mosaic_window, bg='#333333', highlightthickness=0)
|
self.mosaic_label.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
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))
|
||||||
|
|
||||||
|
|
@ -898,204 +877,31 @@ 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 (full resolution for zooming)
|
# Get mosaic from appropriate scanner
|
||||||
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.mosaic # Get full resolution
|
mosaic = self.stitch_scanner.get_mosaic_preview(max_size=580)
|
||||||
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.build_mosaic(scale=1.0)
|
mosaic = self.scanner.get_mosaic_preview(max_size=580)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# Store full image and render
|
mosaic_rgb = cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)
|
||||||
self.mosaic_full_image = mosaic.copy()
|
img = Image.fromarray(mosaic_rgb)
|
||||||
|
imgtk = ImageTk.PhotoImage(image=img)
|
||||||
|
|
||||||
# If first time or image size changed significantly, reset view
|
self.mosaic_label.imgtk = imgtk
|
||||||
if not hasattr(self, '_last_mosaic_size') or self._last_mosaic_size != mosaic.shape[:2]:
|
self.mosaic_label.config(image=imgtk, text="")
|
||||||
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:
|
||||||
|
|
@ -1106,9 +912,6 @@ 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(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue