lazylabel-gui 1.1.9__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -245,6 +245,11 @@ class MainWindow(QMainWindow):
245
245
  self.rubber_band_line = None
246
246
  self.rubber_band_rect = None # New attribute for bounding box
247
247
  self.preview_mask_item = None
248
+
249
+ # AI mode state
250
+ self.ai_click_start_pos = None
251
+ self.ai_click_time = 0
252
+ self.ai_rubber_band_rect = None
248
253
  self.segments, self.segment_items, self.highlight_items = [], {}, []
249
254
  self.edit_handles = []
250
255
  self.is_dragging_polygon, self.drag_start_pos, self.drag_initial_vertices = (
@@ -292,6 +297,10 @@ class MainWindow(QMainWindow):
292
297
  self.sam_embedding_cache = {} # Cache SAM embeddings by content hash
293
298
  self.current_sam_hash = None # Hash of currently loaded SAM image
294
299
 
300
+ # Add bounding box preview state
301
+ self.ai_bbox_preview_mask = None
302
+ self.ai_bbox_preview_rect = None
303
+
295
304
  self._setup_ui()
296
305
  self._setup_model()
297
306
  self._setup_connections()
@@ -391,8 +400,8 @@ class MainWindow(QMainWindow):
391
400
  def _enable_sam_functionality(self, enabled: bool):
392
401
  """Enable or disable SAM point functionality."""
393
402
  self.control_panel.set_sam_mode_enabled(enabled)
394
- if not enabled and self.mode == "sam_points":
395
- # Switch to polygon mode if SAM is disabled and we're in SAM mode
403
+ if not enabled and self.mode in ["sam_points", "ai"]:
404
+ # Switch to polygon mode if SAM is disabled and we're in SAM/AI mode
396
405
  self.set_polygon_mode()
397
406
 
398
407
  def _setup_connections(self):
@@ -559,12 +568,12 @@ class MainWindow(QMainWindow):
559
568
 
560
569
  # Mode management methods
561
570
  def set_sam_mode(self):
562
- """Set mode to SAM points."""
571
+ """Set mode to AI (combines SAM points and bounding box)."""
563
572
  if not self.model_manager.is_model_available():
564
- logger.warning("Cannot enter SAM mode: No model available")
573
+ logger.warning("Cannot enter AI mode: No model available")
565
574
  return
566
- self._set_mode("sam_points")
567
- # Ensure SAM model is updated when entering SAM mode (lazy update)
575
+ self._set_mode("ai")
576
+ # Ensure SAM model is updated when entering AI mode (lazy update)
568
577
  self._ensure_sam_updated()
569
578
 
570
579
  def set_polygon_mode(self):
@@ -625,6 +634,7 @@ class MainWindow(QMainWindow):
625
634
  # Set cursor and drag mode based on mode
626
635
  cursor_map = {
627
636
  "sam_points": Qt.CursorShape.CrossCursor,
637
+ "ai": Qt.CursorShape.CrossCursor,
628
638
  "polygon": Qt.CursorShape.CrossCursor,
629
639
  "bbox": Qt.CursorShape.CrossCursor,
630
640
  "selection": Qt.CursorShape.ArrowCursor,
@@ -690,6 +700,9 @@ class MainWindow(QMainWindow):
690
700
  self.control_panel.set_current_model("Loading model...")
691
701
  QApplication.processEvents()
692
702
 
703
+ # CRITICAL FIX: Reset SAM state before switching models
704
+ self._reset_sam_state_for_model_switch()
705
+
693
706
  try:
694
707
  success = self.model_manager.load_custom_model(model_path)
695
708
  if success:
@@ -698,6 +711,9 @@ class MainWindow(QMainWindow):
698
711
  if self.model_manager.sam_model:
699
712
  device_text = str(self.model_manager.sam_model.device).upper()
700
713
  self.status_bar.set_permanent_message(f"Device: {device_text}")
714
+
715
+ # Mark SAM as dirty to force update with new model
716
+ self._mark_sam_dirty()
701
717
  else:
702
718
  self.control_panel.set_current_model("Current: Default SAM Model")
703
719
  self._show_error_notification(
@@ -1171,6 +1187,20 @@ class MainWindow(QMainWindow):
1171
1187
  """Handle escape key press."""
1172
1188
  self.right_panel.clear_selections()
1173
1189
  self.clear_all_points()
1190
+
1191
+ # Clear bounding box preview state if active
1192
+ if (
1193
+ hasattr(self, "ai_bbox_preview_mask")
1194
+ and self.ai_bbox_preview_mask is not None
1195
+ ):
1196
+ self.ai_bbox_preview_mask = None
1197
+ self.ai_bbox_preview_rect = None
1198
+
1199
+ # Clear preview
1200
+ if hasattr(self, "preview_mask_item") and self.preview_mask_item:
1201
+ self.viewer.scene().removeItem(self.preview_mask_item)
1202
+ self.preview_mask_item = None
1203
+
1174
1204
  self.viewer.setFocus()
1175
1205
 
1176
1206
  def _handle_space_press(self):
@@ -1190,17 +1220,68 @@ class MainWindow(QMainWindow):
1190
1220
  def _save_current_segment(self):
1191
1221
  """Save current SAM segment with fragment threshold filtering."""
1192
1222
  if (
1193
- self.mode != "sam_points"
1194
- or not hasattr(self, "preview_mask_item")
1195
- or not self.preview_mask_item
1223
+ self.mode not in ["sam_points", "ai"]
1196
1224
  or not self.model_manager.is_model_available()
1197
1225
  ):
1198
1226
  return
1199
1227
 
1200
- mask = self.model_manager.sam_model.predict(
1228
+ # Check if we have a bounding box preview to save
1229
+ if (
1230
+ hasattr(self, "ai_bbox_preview_mask")
1231
+ and self.ai_bbox_preview_mask is not None
1232
+ ):
1233
+ # Save bounding box preview
1234
+ mask = self.ai_bbox_preview_mask
1235
+
1236
+ # Apply fragment threshold filtering if enabled
1237
+ filtered_mask = self._apply_fragment_threshold(mask)
1238
+ if filtered_mask is not None:
1239
+ new_segment = {
1240
+ "mask": filtered_mask,
1241
+ "type": "SAM",
1242
+ "vertices": None,
1243
+ }
1244
+ self.segment_manager.add_segment(new_segment)
1245
+ # Record the action for undo
1246
+ self.action_history.append(
1247
+ {
1248
+ "type": "add_segment",
1249
+ "segment_index": len(self.segment_manager.segments) - 1,
1250
+ }
1251
+ )
1252
+ # Clear redo history when a new action is performed
1253
+ self.redo_history.clear()
1254
+ self._update_all_lists()
1255
+ self._show_success_notification("AI bounding box segmentation saved!")
1256
+ else:
1257
+ self._show_warning_notification(
1258
+ "All segments filtered out by fragment threshold"
1259
+ )
1260
+
1261
+ # Clear bounding box preview state
1262
+ self.ai_bbox_preview_mask = None
1263
+ self.ai_bbox_preview_rect = None
1264
+
1265
+ # Clear preview
1266
+ if hasattr(self, "preview_mask_item") and self.preview_mask_item:
1267
+ self.viewer.scene().removeItem(self.preview_mask_item)
1268
+ self.preview_mask_item = None
1269
+ return
1270
+
1271
+ # Handle point-based predictions (existing behavior)
1272
+ if not hasattr(self, "preview_mask_item") or not self.preview_mask_item:
1273
+ return
1274
+
1275
+ result = self.model_manager.sam_model.predict(
1201
1276
  self.positive_points, self.negative_points
1202
1277
  )
1203
- if mask is not None:
1278
+ if result is not None:
1279
+ mask, scores, logits = result
1280
+
1281
+ # Ensure mask is boolean (SAM models can return float masks)
1282
+ if mask.dtype != bool:
1283
+ mask = mask > 0.5 # Convert float mask to boolean
1284
+
1204
1285
  # COORDINATE TRANSFORMATION FIX: Scale mask back up to display size if needed
1205
1286
  if (
1206
1287
  self.sam_scale_factor != 1.0
@@ -1702,6 +1783,14 @@ class MainWindow(QMainWindow):
1702
1783
  self.crop_start_pos = None
1703
1784
  self.current_crop_coords = None
1704
1785
 
1786
+ # Reset AI mode state
1787
+ self.ai_click_start_pos = None
1788
+ self.ai_click_time = 0
1789
+ if hasattr(self, "ai_rubber_band_rect") and self.ai_rubber_band_rect:
1790
+ if self.ai_rubber_band_rect.scene():
1791
+ self.viewer.scene().removeItem(self.ai_rubber_band_rect)
1792
+ self.ai_rubber_band_rect = None
1793
+
1705
1794
  items_to_remove = [
1706
1795
  item
1707
1796
  for item in self.viewer.scene().items()
@@ -1714,6 +1803,10 @@ class MainWindow(QMainWindow):
1714
1803
  self.action_history.clear()
1715
1804
  self.redo_history.clear()
1716
1805
 
1806
+ # Add bounding box preview state
1807
+ self.ai_bbox_preview_mask = None
1808
+ self.ai_bbox_preview_rect = None
1809
+
1717
1810
  def _scene_mouse_press(self, event):
1718
1811
  """Handle mouse press events in the scene."""
1719
1812
  # Map scene coordinates to the view so items() works correctly.
@@ -1763,10 +1856,19 @@ class MainWindow(QMainWindow):
1763
1856
  elif self.mode == "sam_points":
1764
1857
  if event.button() == Qt.MouseButton.LeftButton:
1765
1858
  self._add_point(pos, positive=True)
1766
- self._update_segmentation()
1767
1859
  elif event.button() == Qt.MouseButton.RightButton:
1768
1860
  self._add_point(pos, positive=False)
1769
- self._update_segmentation()
1861
+ elif self.mode == "ai":
1862
+ if event.button() == Qt.MouseButton.LeftButton:
1863
+ # AI mode: single click adds point, drag creates bounding box
1864
+ self.ai_click_start_pos = pos
1865
+ self.ai_click_time = (
1866
+ event.timestamp() if hasattr(event, "timestamp") else 0
1867
+ )
1868
+ # We'll determine if it's a click or drag in mouse_release
1869
+ elif event.button() == Qt.MouseButton.RightButton:
1870
+ # Right-click adds negative point in AI mode
1871
+ self._add_point(pos, positive=False, update_segmentation=True)
1770
1872
  elif self.mode == "polygon":
1771
1873
  if event.button() == Qt.MouseButton.LeftButton:
1772
1874
  self._handle_polygon_click(pos)
@@ -1816,6 +1918,40 @@ class MainWindow(QMainWindow):
1816
1918
  event.accept()
1817
1919
  return
1818
1920
 
1921
+ if (
1922
+ self.mode == "ai"
1923
+ and hasattr(self, "ai_click_start_pos")
1924
+ and self.ai_click_start_pos
1925
+ ):
1926
+ current_pos = event.scenePos()
1927
+ # Check if we've moved enough to consider this a drag
1928
+ drag_distance = (
1929
+ (current_pos.x() - self.ai_click_start_pos.x()) ** 2
1930
+ + (current_pos.y() - self.ai_click_start_pos.y()) ** 2
1931
+ ) ** 0.5
1932
+
1933
+ if drag_distance > 5: # Minimum drag distance
1934
+ # Create rubber band if not exists
1935
+ if (
1936
+ not hasattr(self, "ai_rubber_band_rect")
1937
+ or not self.ai_rubber_band_rect
1938
+ ):
1939
+ self.ai_rubber_band_rect = QGraphicsRectItem()
1940
+ self.ai_rubber_band_rect.setPen(
1941
+ QPen(
1942
+ Qt.GlobalColor.cyan,
1943
+ self.line_thickness,
1944
+ Qt.PenStyle.DashLine,
1945
+ )
1946
+ )
1947
+ self.viewer.scene().addItem(self.ai_rubber_band_rect)
1948
+
1949
+ # Update rubber band
1950
+ rect = QRectF(self.ai_click_start_pos, current_pos).normalized()
1951
+ self.ai_rubber_band_rect.setRect(rect)
1952
+ event.accept()
1953
+ return
1954
+
1819
1955
  if self.mode == "crop" and self.crop_rect_item and self.crop_start_pos:
1820
1956
  current_pos = event.scenePos()
1821
1957
  rect = QRectF(self.crop_start_pos, current_pos).normalized()
@@ -1852,6 +1988,42 @@ class MainWindow(QMainWindow):
1852
1988
 
1853
1989
  if self.mode == "pan":
1854
1990
  self.viewer.set_cursor(Qt.CursorShape.OpenHandCursor)
1991
+ elif (
1992
+ self.mode == "ai"
1993
+ and hasattr(self, "ai_click_start_pos")
1994
+ and self.ai_click_start_pos
1995
+ ):
1996
+ current_pos = event.scenePos()
1997
+ # Calculate drag distance
1998
+ drag_distance = (
1999
+ (current_pos.x() - self.ai_click_start_pos.x()) ** 2
2000
+ + (current_pos.y() - self.ai_click_start_pos.y()) ** 2
2001
+ ) ** 0.5
2002
+
2003
+ if (
2004
+ hasattr(self, "ai_rubber_band_rect")
2005
+ and self.ai_rubber_band_rect
2006
+ and drag_distance > 5
2007
+ ):
2008
+ # This was a drag - use SAM bounding box prediction
2009
+ rect = self.ai_rubber_band_rect.rect()
2010
+ self.viewer.scene().removeItem(self.ai_rubber_band_rect)
2011
+ self.ai_rubber_band_rect = None
2012
+ self.ai_click_start_pos = None
2013
+
2014
+ if rect.width() > 10 and rect.height() > 10: # Minimum box size
2015
+ self._handle_ai_bounding_box(rect)
2016
+ else:
2017
+ # This was a click - add positive point
2018
+ self.ai_click_start_pos = None
2019
+ if hasattr(self, "ai_rubber_band_rect") and self.ai_rubber_band_rect:
2020
+ self.viewer.scene().removeItem(self.ai_rubber_band_rect)
2021
+ self.ai_rubber_band_rect = None
2022
+
2023
+ self._add_point(current_pos, positive=True, update_segmentation=True)
2024
+
2025
+ event.accept()
2026
+ return
1855
2027
  elif self.mode == "bbox" and self.rubber_band_rect:
1856
2028
  self.viewer.scene().removeItem(self.rubber_band_rect)
1857
2029
  rect = self.rubber_band_rect.rect()
@@ -1909,14 +2081,88 @@ class MainWindow(QMainWindow):
1909
2081
 
1910
2082
  self._original_mouse_release(event)
1911
2083
 
1912
- def _add_point(self, pos, positive):
2084
+ def _handle_ai_bounding_box(self, rect):
2085
+ """Handle AI mode bounding box by using SAM's predict_from_box to create a preview."""
2086
+ if not self.model_manager.is_model_available():
2087
+ self._show_warning_notification("AI model not available", 2000)
2088
+ return
2089
+
2090
+ # Quick check - if currently updating, skip but don't block future attempts
2091
+ if self.sam_is_updating:
2092
+ self._show_warning_notification(
2093
+ "AI model is updating, please wait...", 2000
2094
+ )
2095
+ return
2096
+
2097
+ # Convert QRectF to SAM box format [x1, y1, x2, y2]
2098
+ # COORDINATE TRANSFORMATION FIX: Use proper coordinate mapping based on operate_on_view setting
2099
+ from PyQt6.QtCore import QPointF
2100
+
2101
+ top_left = QPointF(rect.left(), rect.top())
2102
+ bottom_right = QPointF(rect.right(), rect.bottom())
2103
+
2104
+ sam_x1, sam_y1 = self._transform_display_coords_to_sam_coords(top_left)
2105
+ sam_x2, sam_y2 = self._transform_display_coords_to_sam_coords(bottom_right)
2106
+
2107
+ box = [sam_x1, sam_y1, sam_x2, sam_y2]
2108
+
2109
+ try:
2110
+ result = self.model_manager.sam_model.predict_from_box(box)
2111
+ if result is not None:
2112
+ mask, scores, logits = result
2113
+
2114
+ # Ensure mask is boolean (SAM models can return float masks)
2115
+ if mask.dtype != bool:
2116
+ mask = mask > 0.5 # Convert float mask to boolean
2117
+
2118
+ # COORDINATE TRANSFORMATION FIX: Scale mask back up to display size if needed
2119
+ if (
2120
+ self.sam_scale_factor != 1.0
2121
+ and self.viewer._pixmap_item
2122
+ and not self.viewer._pixmap_item.pixmap().isNull()
2123
+ ):
2124
+ # Get original image dimensions
2125
+ original_height = self.viewer._pixmap_item.pixmap().height()
2126
+ original_width = self.viewer._pixmap_item.pixmap().width()
2127
+
2128
+ # Resize mask back to original dimensions for saving
2129
+ mask_resized = cv2.resize(
2130
+ mask.astype(np.uint8),
2131
+ (original_width, original_height),
2132
+ interpolation=cv2.INTER_NEAREST,
2133
+ ).astype(bool)
2134
+ mask = mask_resized
2135
+
2136
+ # Store the preview mask and rect for later confirmation
2137
+ self.ai_bbox_preview_mask = mask
2138
+ self.ai_bbox_preview_rect = rect
2139
+
2140
+ # Clear any existing preview
2141
+ if hasattr(self, "preview_mask_item") and self.preview_mask_item:
2142
+ self.viewer.scene().removeItem(self.preview_mask_item)
2143
+
2144
+ # Show preview with yellow color
2145
+ pixmap = mask_to_pixmap(mask, (255, 255, 0))
2146
+ self.preview_mask_item = self.viewer.scene().addPixmap(pixmap)
2147
+ self.preview_mask_item.setZValue(50)
2148
+
2149
+ self._show_success_notification(
2150
+ "AI bounding box preview ready - press Space to confirm!"
2151
+ )
2152
+ else:
2153
+ self._show_warning_notification("No prediction result from AI model")
2154
+ except Exception as e:
2155
+ logger.error(f"Error during AI bounding box prediction: {e}")
2156
+ self._show_error_notification("AI prediction failed")
2157
+
2158
+ def _add_point(self, pos, positive, update_segmentation=True):
1913
2159
  """Add a point for SAM segmentation."""
1914
2160
  # RACE CONDITION FIX: Block clicks during SAM updates
1915
2161
  if self.sam_is_updating:
1916
2162
  self._show_warning_notification(
1917
2163
  "AI model is updating, please wait...", 2000
1918
2164
  )
1919
- return
2165
+ return False
1920
2166
 
1921
2167
  # Ensure SAM is updated before using it
1922
2168
  self._ensure_sam_updated()
@@ -1926,11 +2172,10 @@ class MainWindow(QMainWindow):
1926
2172
  self._show_warning_notification(
1927
2173
  "AI model is updating, please wait...", 2000
1928
2174
  )
1929
- return
2175
+ return False
1930
2176
 
1931
- # COORDINATE TRANSFORMATION FIX: Scale coordinates for SAM model
1932
- sam_x = int(pos.x() * self.sam_scale_factor)
1933
- sam_y = int(pos.y() * self.sam_scale_factor)
2177
+ # COORDINATE TRANSFORMATION FIX: Use proper coordinate mapping based on operate_on_view setting
2178
+ sam_x, sam_y = self._transform_display_coords_to_sam_coords(pos)
1934
2179
 
1935
2180
  point_list = self.positive_points if positive else self.negative_points
1936
2181
  point_list.append([sam_x, sam_y])
@@ -1965,6 +2210,12 @@ class MainWindow(QMainWindow):
1965
2210
  # Clear redo history when a new action is performed
1966
2211
  self.redo_history.clear()
1967
2212
 
2213
+ # Update segmentation if requested and not currently updating
2214
+ if update_segmentation and not self.sam_is_updating:
2215
+ self._update_segmentation()
2216
+
2217
+ return True
2218
+
1968
2219
  def _update_segmentation(self):
1969
2220
  """Update SAM segmentation preview."""
1970
2221
  if hasattr(self, "preview_mask_item") and self.preview_mask_item:
@@ -1972,10 +2223,16 @@ class MainWindow(QMainWindow):
1972
2223
  if not self.positive_points or not self.model_manager.is_model_available():
1973
2224
  return
1974
2225
 
1975
- mask = self.model_manager.sam_model.predict(
2226
+ result = self.model_manager.sam_model.predict(
1976
2227
  self.positive_points, self.negative_points
1977
2228
  )
1978
- if mask is not None:
2229
+ if result is not None:
2230
+ mask, scores, logits = result
2231
+
2232
+ # Ensure mask is boolean (SAM models can return float masks)
2233
+ if mask.dtype != bool:
2234
+ mask = mask > 0.5 # Convert float mask to boolean
2235
+
1979
2236
  # COORDINATE TRANSFORMATION FIX: Scale mask back up to display size if needed
1980
2237
  if (
1981
2238
  self.sam_scale_factor != 1.0
@@ -2428,10 +2685,21 @@ class MainWindow(QMainWindow):
2428
2685
  self.sam_is_dirty = False
2429
2686
  return
2430
2687
 
2431
- # Stop any existing worker
2688
+ # IMPROVED: More robust worker thread cleanup
2432
2689
  if self.sam_worker_thread and self.sam_worker_thread.isRunning():
2433
2690
  self.sam_worker_thread.stop()
2434
- self.sam_worker_thread.wait(1000) # Wait up to 1 second
2691
+ self.sam_worker_thread.terminate()
2692
+ # Wait longer for proper cleanup
2693
+ self.sam_worker_thread.wait(5000) # Wait up to 5 seconds
2694
+ if self.sam_worker_thread.isRunning():
2695
+ # Force kill if still running
2696
+ self.sam_worker_thread.quit()
2697
+ self.sam_worker_thread.wait(2000)
2698
+
2699
+ # Clean up old worker thread
2700
+ if self.sam_worker_thread:
2701
+ self.sam_worker_thread.deleteLater()
2702
+ self.sam_worker_thread = None
2435
2703
 
2436
2704
  # Show status message
2437
2705
  if hasattr(self, "status_bar"):
@@ -2741,6 +3009,9 @@ class MainWindow(QMainWindow):
2741
3009
  # Update the FFT threshold widget
2742
3010
  self.control_panel.update_fft_threshold_for_image(image_array)
2743
3011
 
3012
+ # Auto-collapse FFT threshold panel if image is not black and white
3013
+ self.control_panel.auto_collapse_fft_threshold_for_image(image_array)
3014
+
2744
3015
  # Border crop methods
2745
3016
  def _start_crop_drawing(self):
2746
3017
  """Start crop drawing mode."""
@@ -3083,3 +3354,136 @@ class MainWindow(QMainWindow):
3083
3354
  """Update SAM model image after debounce delay."""
3084
3355
  # This is called after the user stops interacting with sliders
3085
3356
  self._update_sam_model_image()
3357
+
3358
+ def _reset_sam_state_for_model_switch(self):
3359
+ """Reset SAM state completely when switching models to prevent worker thread conflicts."""
3360
+
3361
+ # CRITICAL: Force terminate any running SAM worker thread
3362
+ if self.sam_worker_thread and self.sam_worker_thread.isRunning():
3363
+ self.sam_worker_thread.stop()
3364
+ self.sam_worker_thread.terminate()
3365
+ self.sam_worker_thread.wait(3000) # Wait up to 3 seconds
3366
+ if self.sam_worker_thread.isRunning():
3367
+ # Force kill if still running
3368
+ self.sam_worker_thread.quit()
3369
+ self.sam_worker_thread.wait(1000)
3370
+
3371
+ # Clean up worker thread reference
3372
+ if self.sam_worker_thread:
3373
+ self.sam_worker_thread.deleteLater()
3374
+ self.sam_worker_thread = None
3375
+
3376
+ # Reset SAM update flags
3377
+ self.sam_is_updating = False
3378
+ self.sam_is_dirty = True # Force update with new model
3379
+ self.current_sam_hash = None # Invalidate cache
3380
+ self.sam_scale_factor = 1.0
3381
+
3382
+ # Clear all points and segments
3383
+ self.clear_all_points()
3384
+ self.segment_manager.clear()
3385
+ self._update_all_lists()
3386
+
3387
+ # Clear preview items
3388
+ if hasattr(self, "preview_mask_item") and self.preview_mask_item:
3389
+ if self.preview_mask_item.scene():
3390
+ self.viewer.scene().removeItem(self.preview_mask_item)
3391
+ self.preview_mask_item = None
3392
+
3393
+ # Clean up crop visuals
3394
+ self._remove_crop_visual()
3395
+ self._remove_crop_hover_overlay()
3396
+ self._remove_crop_hover_effect()
3397
+
3398
+ # Reset crop state
3399
+ self.crop_mode = False
3400
+ self.crop_start_pos = None
3401
+ self.current_crop_coords = None
3402
+
3403
+ # Reset AI mode state
3404
+ self.ai_click_start_pos = None
3405
+ self.ai_click_time = 0
3406
+ if hasattr(self, "ai_rubber_band_rect") and self.ai_rubber_band_rect:
3407
+ if self.ai_rubber_band_rect.scene():
3408
+ self.viewer.scene().removeItem(self.ai_rubber_band_rect)
3409
+ self.ai_rubber_band_rect = None
3410
+
3411
+ # Clear all graphics items except the main image
3412
+ items_to_remove = [
3413
+ item
3414
+ for item in self.viewer.scene().items()
3415
+ if item is not self.viewer._pixmap_item
3416
+ ]
3417
+ for item in items_to_remove:
3418
+ self.viewer.scene().removeItem(item)
3419
+
3420
+ # Reset all collections
3421
+ self.segment_items.clear()
3422
+ self.highlight_items.clear()
3423
+ self.action_history.clear()
3424
+ self.redo_history.clear()
3425
+
3426
+ # Reset bounding box preview state
3427
+ self.ai_bbox_preview_mask = None
3428
+ self.ai_bbox_preview_rect = None
3429
+
3430
+ # Clear status bar messages
3431
+ if hasattr(self, "status_bar"):
3432
+ self.status_bar.clear_message()
3433
+
3434
+ def _transform_display_coords_to_sam_coords(self, pos):
3435
+ """Transform display coordinates to SAM model coordinates.
3436
+
3437
+ When 'operate on view' is ON: SAM processes the displayed image
3438
+ When 'operate on view' is OFF: SAM processes the original image
3439
+ """
3440
+ if self.settings.operate_on_view:
3441
+ # Simple case: SAM processes the same image the user sees
3442
+ sam_x = int(pos.x() * self.sam_scale_factor)
3443
+ sam_y = int(pos.y() * self.sam_scale_factor)
3444
+ else:
3445
+ # Complex case: Map display coordinates to original image coordinates
3446
+ # then scale for SAM processing
3447
+
3448
+ # Get displayed image dimensions (may include adjustments)
3449
+ if (
3450
+ not self.viewer._pixmap_item
3451
+ or self.viewer._pixmap_item.pixmap().isNull()
3452
+ ):
3453
+ # Fallback: use simple scaling
3454
+ sam_x = int(pos.x() * self.sam_scale_factor)
3455
+ sam_y = int(pos.y() * self.sam_scale_factor)
3456
+ else:
3457
+ display_width = self.viewer._pixmap_item.pixmap().width()
3458
+ display_height = self.viewer._pixmap_item.pixmap().height()
3459
+
3460
+ # Get original image dimensions
3461
+ if not self.current_image_path:
3462
+ # Fallback: use simple scaling
3463
+ sam_x = int(pos.x() * self.sam_scale_factor)
3464
+ sam_y = int(pos.y() * self.sam_scale_factor)
3465
+ else:
3466
+ # Load original image to get true dimensions
3467
+ original_pixmap = QPixmap(self.current_image_path)
3468
+ if original_pixmap.isNull():
3469
+ # Fallback: use simple scaling
3470
+ sam_x = int(pos.x() * self.sam_scale_factor)
3471
+ sam_y = int(pos.y() * self.sam_scale_factor)
3472
+ else:
3473
+ original_width = original_pixmap.width()
3474
+ original_height = original_pixmap.height()
3475
+
3476
+ # Map display coordinates to original image coordinates
3477
+ if display_width > 0 and display_height > 0:
3478
+ original_x = pos.x() * (original_width / display_width)
3479
+ original_y = pos.y() * (original_height / display_height)
3480
+
3481
+ # Apply SAM scale factor to original coordinates
3482
+ sam_x = int(original_x * self.sam_scale_factor)
3483
+ sam_y = int(original_y * self.sam_scale_factor)
3484
+ else:
3485
+ # Fallback: use simple scaling
3486
+ sam_x = int(pos.x() * self.sam_scale_factor)
3487
+ sam_y = int(pos.y() * self.sam_scale_factor)
3488
+
3489
+ return sam_x, sam_y