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.
- lazylabel/config/hotkeys.py +2 -2
- lazylabel/core/model_manager.py +91 -16
- lazylabel/models/sam2_model.py +223 -0
- lazylabel/models/sam_model.py +25 -3
- lazylabel/ui/control_panel.py +37 -2
- lazylabel/ui/main_window.py +427 -23
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/METADATA +56 -96
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/RECORD +12 -11
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.9.dist-info → lazylabel_gui-1.2.0.dist-info}/top_level.txt +0 -0
lazylabel/ui/main_window.py
CHANGED
@@ -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
|
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
|
573
|
+
logger.warning("Cannot enter AI mode: No model available")
|
565
574
|
return
|
566
|
-
self._set_mode("
|
567
|
-
# Ensure SAM model is updated when entering
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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:
|
1932
|
-
sam_x =
|
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
|
-
|
2226
|
+
result = self.model_manager.sam_model.predict(
|
1976
2227
|
self.positive_points, self.negative_points
|
1977
2228
|
)
|
1978
|
-
if
|
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
|
-
#
|
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.
|
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
|