labelimgplusplus 3.2.0__tar.gz → 3.3.0__tar.gz

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.
Files changed (107) hide show
  1. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/HISTORY.rst +50 -0
  2. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/PKG-INFO +15 -3
  3. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/README.rst +14 -2
  4. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelImgPlusPlus.py +132 -65
  5. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/PKG-INFO +15 -3
  6. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/SOURCES.txt +2 -0
  7. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/__init__.py +1 -1
  8. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/commands.py +19 -1
  9. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/sam_controller.py +15 -11
  10. labelimgplusplus-3.3.0/libs/core/sam_types.py +21 -0
  11. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/integrations/mask_to_polygon.py +42 -9
  12. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/resources.py +172 -96
  13. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/constants.py +1 -0
  14. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/__init__.py +2 -1
  15. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/canvas.py +77 -5
  16. labelimgplusplus-3.3.0/libs/widgets/inlineClassPicker.py +171 -0
  17. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/workspacePages.py +53 -2
  18. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/pyproject.toml +1 -1
  19. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/resources.qrc +1 -0
  20. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/setup.cfg +1 -1
  21. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/CONTRIBUTING.rst +0 -0
  22. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/LICENSE +0 -0
  23. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/MANIFEST.in +0 -0
  24. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/data/predefined_classes.txt +0 -0
  25. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus/__init__.py +0 -0
  26. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus/plugins.py +0 -0
  27. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/dependency_links.txt +0 -0
  28. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/entry_points.txt +0 -0
  29. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/requires.txt +0 -0
  30. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/labelimgplusplus.egg-info/top_level.txt +0 -0
  31. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/__init__.py +0 -0
  32. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/annotation_catalog.py +0 -0
  33. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/dataset.py +0 -0
  34. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/image_pipeline.py +0 -0
  35. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/keypoint_config.py +0 -0
  36. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/plugin_discovery.py +0 -0
  37. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/plugin_manager.py +0 -0
  38. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/plugin_registry.py +0 -0
  39. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/profiling.py +0 -0
  40. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/save_pipeline.py +0 -0
  41. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/settings.py +0 -0
  42. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/shape.py +0 -0
  43. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/shortcut_config.py +0 -0
  44. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/task_coordinator.py +0 -0
  45. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/ultralytics_export.py +0 -0
  46. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_decoder.py +0 -0
  47. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_export.py +0 -0
  48. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_model.py +0 -0
  49. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_project.py +0 -0
  50. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_session.py +0 -0
  51. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_tracking.py +0 -0
  52. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/video_types.py +0 -0
  53. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/core/workspace_settings.py +0 -0
  54. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/data/predefined_classes.txt +0 -0
  55. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/__init__.py +0 -0
  56. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/annotation_loader.py +0 -0
  57. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/annotation_paths.py +0 -0
  58. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/annotation_probe.py +0 -0
  59. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/coco_io.py +0 -0
  60. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/create_ml_io.py +0 -0
  61. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/format_metadata.py +0 -0
  62. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/labelFile.py +0 -0
  63. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/pascal_voc_io.py +0 -0
  64. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/yolo_io.py +0 -0
  65. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/formats/yolo_seg_io.py +0 -0
  66. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/integrations/__init__.py +0 -0
  67. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/integrations/image_convert.py +0 -0
  68. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/integrations/model_cache.py +0 -0
  69. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/integrations/segmentation.py +0 -0
  70. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/tools/__init__.py +0 -0
  71. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/tools/batch_verify.py +0 -0
  72. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/tools/dataset_splitter.py +0 -0
  73. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/tools/label_checker.py +0 -0
  74. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/__init__.py +0 -0
  75. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/dpi.py +0 -0
  76. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/hashableQListWidgetItem.py +0 -0
  77. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/stringBundle.py +0 -0
  78. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/styles.py +0 -0
  79. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/ustr.py +0 -0
  80. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/utils/utils.py +0 -0
  81. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/annotationInspector.py +0 -0
  82. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/batchVerifyDialog.py +0 -0
  83. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/colorDialog.py +0 -0
  84. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/combobox.py +0 -0
  85. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/commandBar.py +0 -0
  86. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/default_label_combobox.py +0 -0
  87. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/galleryWidget.py +0 -0
  88. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/gallery_status_controller.py +0 -0
  89. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/keypointPanel.py +0 -0
  90. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/labelCheckerDialog.py +0 -0
  91. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/labelDialog.py +0 -0
  92. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/lightWidget.py +0 -0
  93. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/pluginManagerDialog.py +0 -0
  94. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/sam_settings_dialog.py +0 -0
  95. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/shortcutsDialog.py +0 -0
  96. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/splitDialog.py +0 -0
  97. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/statsWidget.py +0 -0
  98. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/stats_controller.py +0 -0
  99. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/toolBar.py +0 -0
  100. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/toolRail.py +0 -0
  101. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/ultralyticsExportDialog.py +0 -0
  102. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/videoExportDialog.py +0 -0
  103. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/videoTimelineWidget.py +0 -0
  104. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/view_scaling.py +0 -0
  105. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/workspaceInspector.py +0 -0
  106. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/libs/widgets/zoomWidget.py +0 -0
  107. {labelimgplusplus-3.2.0 → labelimgplusplus-3.3.0}/resources/icons/feather/LICENSE.txt +0 -0
@@ -1,6 +1,56 @@
1
1
  History
2
2
  =======
3
3
 
4
+ 3.3.0 (2026-08-12)
5
+ ------------------
6
+
7
+ Faster-annotation release adding non-modal class confirmation and selectable
8
+ Smart Select geometry while preserving the 3.2 workspace, annotation formats,
9
+ filenames, command-line entry points, plugin APIs, shortcut identifiers, and
10
+ Python 3.8 through 3.13 support.
11
+
12
+ Inline Class Confirmation
13
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
14
+
15
+ * Keep completed Box, Polygon, Smart Select, and video geometry in a transient
16
+ provisional layer until its class is confirmed. Provisional geometry is
17
+ excluded from canonical canvas shapes, the Objects inspector, dirty state,
18
+ saves, plugin snapshots, and undo history.
19
+ * Open a non-modal, screen-clamped class picker beside new geometry with
20
+ filtering, arrow-key navigation, valid new-class entry, Enter confirmation,
21
+ and Escape cancellation. Confirmation preserves the active tool and canvas
22
+ focus so repeated annotation remains uninterrupted.
23
+ * Commit confirmed image or video geometry through one ``CreateShapeCommand``.
24
+ Video confirmation creates an accepted manual anchor; cancellation leaves
25
+ both the in-memory model and durable project unchanged.
26
+ * Retain session-only last-class memory. Default-label mode bypasses the picker,
27
+ while single-class mode bypasses it only after the first class is confirmed
28
+ in the current session.
29
+
30
+ Smart Select Output Modes
31
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
32
+
33
+ * Add the frozen plain-data ``SamResult(polygon, bounds)`` contract. SAM workers
34
+ return a simplified polygon and tight, non-zero, half-open bounds for the
35
+ same largest mask component without mutating Qt or document state.
36
+ * Add a contextual Box/Polygon selector that appears only while Smart Select is
37
+ active. Its validated ``sam/outputMode`` setting defaults to ``polygon`` and
38
+ persists independently of model settings.
39
+ * Route both output types through the provisional picker and the same image and
40
+ video undo path. Existing stale-generation and cancellation guards remain in
41
+ place, and ONNX Runtime, NumPy, OpenCV, and model loading stay lazy.
42
+
43
+ Qualification
44
+ ~~~~~~~~~~~~~
45
+
46
+ * Cover image and video confirmation, cancellation, focus, filtering, repeated
47
+ annotation, bypass modes, output bounds, simplification, persistence, stale
48
+ SAM results, manual video anchors, and one-step undo.
49
+ * Qualify the picker and Smart Select selector in light and dark themes at 1x
50
+ and true 2x DPI using 1366x768 and 1440x900 logical approval sizes.
51
+ * Make queued Gallery and annotation-catalog refreshes inert after window
52
+ shutdown so late Qt callbacks cannot submit work to a stopped coordinator.
53
+
4
54
  3.2.0 (2026-08-11)
5
55
  ------------------
6
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labelimgplusplus
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: labelImg++ is an enhanced graphical image annotation tool with Gallery Mode for browsing and labeling images
5
5
  Author-email: Abhik Sarkar <abhiksark@gmail.com>
6
6
  License: Copyright (c) <2015-Present> Tzutalin
@@ -78,7 +78,7 @@ polygons, and keypoints, designed for machine learning and computer vision
78
78
  projects. It is forked from the original LabelImg with significant
79
79
  enhancements.
80
80
 
81
- **Version 3.2.0 (stable).** Install with
81
+ **Version 3.3.0 (stable).** Install with
82
82
  ``pip install labelimgplusplus``.
83
83
 
84
84
  .. image:: https://raw.githubusercontent.com/abhiksark/labelImg-plus-plus/c7fbd5fc08a561206b210706143a50023c82a782/resources/demo/demo.gif
@@ -132,6 +132,18 @@ Workflow and Interface
132
132
  annotation-status filter close at hand, and the inspector can be collapsed
133
133
  when maximum canvas space is needed.
134
134
 
135
+ **Faster Class Confirmation**
136
+ New boxes, polygons, Smart Select results, and video geometry remain
137
+ provisional while a non-modal class picker opens beside the annotation.
138
+ Filter or enter a class, press **Enter** to commit one undoable annotation,
139
+ or press **Escape** to discard it without changing the document. Default
140
+ labels and established single-class sessions bypass the picker.
141
+
142
+ **Smart Select Output Modes**
143
+ While Smart Select is active, choose **Box** or **Polygon** from the compact
144
+ canvas control. The choice persists between sessions, and either result
145
+ follows the same provisional class-confirmation and undo workflow.
146
+
135
147
  **Direct Ultralytics Dataset Export**
136
148
  Choose **Tools → Export Ultralytics Dataset…** to create a ready-to-train
137
149
  YOLO detection dataset with ``images/{train,val,test}``, matching
@@ -401,7 +413,7 @@ Or use **Menu > File > Reset All**
401
413
  Release History
402
414
  ---------------
403
415
 
404
- This source tree identifies itself as **3.2.0**, the stable 3.2 release. See
416
+ This source tree identifies itself as **3.3.0**, the stable 3.3 release. See
405
417
  the `release history
406
418
  <https://github.com/abhiksark/labelImg-plus-plus/blob/master/HISTORY.rst>`__ for
407
419
  version-by-version changes and `GitHub Releases
@@ -26,7 +26,7 @@ polygons, and keypoints, designed for machine learning and computer vision
26
26
  projects. It is forked from the original LabelImg with significant
27
27
  enhancements.
28
28
 
29
- **Version 3.2.0 (stable).** Install with
29
+ **Version 3.3.0 (stable).** Install with
30
30
  ``pip install labelimgplusplus``.
31
31
 
32
32
  .. image:: https://raw.githubusercontent.com/abhiksark/labelImg-plus-plus/c7fbd5fc08a561206b210706143a50023c82a782/resources/demo/demo.gif
@@ -80,6 +80,18 @@ Workflow and Interface
80
80
  annotation-status filter close at hand, and the inspector can be collapsed
81
81
  when maximum canvas space is needed.
82
82
 
83
+ **Faster Class Confirmation**
84
+ New boxes, polygons, Smart Select results, and video geometry remain
85
+ provisional while a non-modal class picker opens beside the annotation.
86
+ Filter or enter a class, press **Enter** to commit one undoable annotation,
87
+ or press **Escape** to discard it without changing the document. Default
88
+ labels and established single-class sessions bypass the picker.
89
+
90
+ **Smart Select Output Modes**
91
+ While Smart Select is active, choose **Box** or **Polygon** from the compact
92
+ canvas control. The choice persists between sessions, and either result
93
+ follows the same provisional class-confirmation and undo workflow.
94
+
83
95
  **Direct Ultralytics Dataset Export**
84
96
  Choose **Tools → Export Ultralytics Dataset…** to create a ready-to-train
85
97
  YOLO detection dataset with ``images/{train,val,test}``, matching
@@ -349,7 +361,7 @@ Or use **Menu > File > Reset All**
349
361
  Release History
350
362
  ---------------
351
363
 
352
- This source tree identifies itself as **3.2.0**, the stable 3.2 release. See
364
+ This source tree identifies itself as **3.3.0**, the stable 3.3 release. See
353
365
  the `release history
354
366
  <https://github.com/abhiksark/labelImg-plus-plus/blob/master/HISTORY.rst>`__ for
355
367
  version-by-version changes and `GitHub Releases
@@ -71,6 +71,7 @@ from libs.widgets.annotationInspector import (
71
71
  UnifiedAnnotationView,
72
72
  )
73
73
  from libs.widgets.workspacePages import WorkspacePages
74
+ from libs.widgets.inlineClassPicker import InlineClassPicker
74
75
 
75
76
  # Core
76
77
  from libs.core.shape import Shape, ShapeType, DEFAULT_LINE_COLOR, DEFAULT_FILL_COLOR
@@ -85,6 +86,7 @@ from libs.core.workspace_settings import (
85
86
  clamp_inspector_width, load_workspace_settings,
86
87
  )
87
88
  from libs.core.sam_controller import SamController
89
+ from libs.core.sam_types import normalize_sam_output_mode
88
90
  from libs.core.annotation_catalog import AnnotationCatalog
89
91
  from libs.core.dataset import DatasetSnapshot
90
92
  from libs.core.image_pipeline import FrameCache, load_image_result
@@ -144,7 +146,7 @@ from libs.utils.constants import (
144
146
  SETTING_WIN_POSE, SETTING_WIN_SIZE,
145
147
  FORMAT_PASCALVOC, FORMAT_YOLO, FORMAT_CREATEML,
146
148
  FORMAT_COCO, FORMAT_YOLO_SEG,
147
- SETTING_SAM_ENCODER, SETTING_SAM_DECODER,
149
+ SETTING_SAM_ENCODER, SETTING_SAM_DECODER, SETTING_SAM_OUTPUT_MODE,
148
150
  )
149
151
  from libs.utils.utils import (
150
152
  new_icon, themed_icon, new_action, add_actions, format_shortcut, Struct,
@@ -203,6 +205,8 @@ class MainWindow(QMainWindow, WindowMixin):
203
205
  self.settings.load()
204
206
  settings = self.settings
205
207
  self.workspace_settings = load_workspace_settings(settings)
208
+ self.sam_output_mode = normalize_sam_output_mode(
209
+ settings.get(SETTING_SAM_OUTPUT_MODE, 'polygon'))
206
210
 
207
211
  self.shortcut_config = ShortcutConfig()
208
212
  if settings.get(SETTING_SHORTCUTS):
@@ -317,6 +321,8 @@ class MainWindow(QMainWindow, WindowMixin):
317
321
  self.label_dialog = LabelDialog(parent=self, list_item=self.label_hist)
318
322
 
319
323
  self.prev_label_text = ''
324
+ self._session_last_class = None
325
+ self._pending_provisional_shape = None
320
326
 
321
327
  list_layout = QVBoxLayout()
322
328
  list_layout.setContentsMargins(0, 0, 0, 0)
@@ -445,6 +451,9 @@ class MainWindow(QMainWindow, WindowMixin):
445
451
  self.canvas.scrollRequest.connect(self.scroll_request)
446
452
 
447
453
  self.canvas.newShape.connect(self.new_shape)
454
+ self.class_picker = InlineClassPicker(self)
455
+ self.class_picker.accepted.connect(self._commit_provisional_shape)
456
+ self.class_picker.cancelled.connect(self._cancel_provisional_shape)
448
457
  self.sam_controller = SamController(self)
449
458
  self.canvas.samClicked.connect(self.sam_controller.segment_at)
450
459
  self._sam_available = segmentation.sam_available()
@@ -624,7 +633,7 @@ class MainWindow(QMainWindow, WindowMixin):
624
633
  self.toggle_sam_mode,
625
634
  self.shortcut_config.get('sam_mode'),
626
635
  'tool-smart-select',
627
- 'Click an object to auto-generate a polygon '
636
+ 'Click an object to auto-generate the selected geometry '
628
637
  '(requires: pip install labelimgplusplus[sam])',
629
638
  enabled=False)
630
639
  sam_settings_action = action(
@@ -1124,6 +1133,9 @@ class MainWindow(QMainWindow, WindowMixin):
1124
1133
  self.label_zoom, self.label_coordinates),
1125
1134
  self.actions, self.zoom_widget, self)
1126
1135
  self.workspace_pages = canvas_column
1136
+ self.workspace_pages.sam_output_toggle.set_mode(self.sam_output_mode)
1137
+ self.workspace_pages.sam_output_toggle.modeChanged.connect(
1138
+ self._set_sam_output_mode)
1127
1139
  self.workspace_pages.empty_page.recentActivated.connect(
1128
1140
  self._open_workspace_recent)
1129
1141
  for tool_action in (
@@ -1640,6 +1652,7 @@ class MainWindow(QMainWindow, WindowMixin):
1640
1652
  self._update_save_status_style(saved)
1641
1653
 
1642
1654
  def reset_state(self):
1655
+ self._dismiss_class_picker()
1643
1656
  self._plugin_document_ready = False
1644
1657
  self._restart_workers_if_needed()
1645
1658
  self._close_video_decoder()
@@ -2040,8 +2053,19 @@ class MainWindow(QMainWindow, WindowMixin):
2040
2053
  }
2041
2054
  active = mapping.get(self.canvas.mode, self.actions.editMode)
2042
2055
  active.setChecked(True)
2056
+ if hasattr(self, 'workspace_pages'):
2057
+ self.workspace_pages.sam_output_toggle.setVisible(
2058
+ self.canvas.mode == self.canvas.CREATE_SAM)
2043
2059
  self._update_active_tool_status()
2044
2060
 
2061
+ def _set_sam_output_mode(self, mode):
2062
+ """Persist the contextual Smart Select geometry choice."""
2063
+ self.sam_output_mode = normalize_sam_output_mode(mode)
2064
+ self.workspace_pages.sam_output_toggle.set_mode(self.sam_output_mode)
2065
+ self.settings[SETTING_SAM_OUTPUT_MODE] = self.sam_output_mode
2066
+ self.settings.save()
2067
+ self._restore_canvas_focus()
2068
+
2045
2069
  def _update_active_tool_status(self):
2046
2070
  if not hasattr(self, 'label_active_tool'):
2047
2071
  return
@@ -2163,14 +2187,11 @@ class MainWindow(QMainWindow, WindowMixin):
2163
2187
  self.pause_video()
2164
2188
  editable = self._video_editable()
2165
2189
  self.actions.editMode.setEnabled(not drawing and editable)
2190
+ self.actions.create.setEnabled(not drawing and editable)
2166
2191
  self.actions.create_polygon.setEnabled(not drawing and editable)
2167
- if not drawing and self.beginner():
2168
- # Cancel creation.
2169
- print('Cancel creation.')
2170
- self.canvas.set_editing(True)
2192
+ if not drawing:
2171
2193
  self.canvas.restore_cursor()
2172
- self.actions.create.setEnabled(editable)
2173
- self.actions.create_polygon.setEnabled(editable)
2194
+ self._sync_tool_actions()
2174
2195
 
2175
2196
  def toggle_draw_mode(self, edit=True):
2176
2197
  if (self.document_kind == DocumentKind.VIDEO
@@ -2352,13 +2373,17 @@ class MainWindow(QMainWindow, WindowMixin):
2352
2373
  return None
2353
2374
 
2354
2375
  def _ensure_annotation_catalog(self):
2376
+ coordinator = getattr(self, 'task_coordinator', None)
2377
+ if coordinator is None or coordinator.is_shutting_down:
2378
+ return False
2355
2379
  snapshot = getattr(self, 'dataset_snapshot', None)
2356
2380
  if snapshot is None or not snapshot.image_paths:
2357
- return
2381
+ return False
2358
2382
  if (self.annotation_catalog.snapshot is not snapshot
2359
2383
  or (not self.annotation_catalog.entries
2360
2384
  and self.annotation_catalog._handle is None)):
2361
2385
  self.annotation_catalog.start(snapshot)
2386
+ return True
2362
2387
 
2363
2388
  def _on_catalog_batch(self, statuses):
2364
2389
  converted = {
@@ -2873,69 +2898,105 @@ class MainWindow(QMainWindow, WindowMixin):
2873
2898
 
2874
2899
  # Callback functions:
2875
2900
  def new_shape(self):
2876
- """Pop-up and give focus to the label editor.
2877
-
2878
- position MUST be in global coordinates.
2879
- """
2901
+ """Stage completed geometry until its class is confirmed."""
2880
2902
  if (self.document_kind == DocumentKind.VIDEO
2881
2903
  and not self._ensure_video_editable()):
2882
- self.canvas.reset_all_lines()
2883
- return
2884
- if not self.use_default_label_checkbox.isChecked():
2885
- if len(self.label_hist) > 0:
2886
- self.label_dialog = LabelDialog(
2887
- parent=self, list_item=self.label_hist)
2888
- if hasattr(self, '_current_theme'):
2889
- self.label_dialog.apply_theme(self._current_theme)
2890
-
2891
- # Sync single class mode from PR#106
2892
- if self.single_class_mode.isChecked() and self.lastLabel:
2893
- text = self.lastLabel
2904
+ self._cancel_provisional_shape()
2905
+ return
2906
+ shape = self.canvas.provisional_shape
2907
+ if shape is None:
2908
+ return
2909
+ if (self._pending_provisional_shape is not None
2910
+ and self._pending_provisional_shape is not shape):
2911
+ self._dismiss_class_picker(discard=False)
2912
+ self._pending_provisional_shape = shape
2913
+
2914
+ if self.use_default_label_checkbox.isChecked():
2915
+ text = getattr(self, 'default_label', '')
2916
+ if text:
2917
+ self._commit_provisional_shape(text)
2894
2918
  else:
2895
- text = self.label_dialog.pop_up(text=self.prev_label_text)
2896
- self.lastLabel = text
2919
+ self._cancel_provisional_shape()
2920
+ return
2921
+ if self.single_class_mode.isChecked() and self._session_last_class:
2922
+ self._commit_provisional_shape(self._session_last_class)
2923
+ return
2924
+
2925
+ self.class_picker.open_at(
2926
+ self.label_hist,
2927
+ self._session_last_class or self.prev_label_text,
2928
+ self._provisional_picker_anchor(shape))
2929
+
2930
+ def _provisional_picker_anchor(self, shape):
2931
+ """Return a global logical-pixel anchor beside provisional geometry."""
2932
+ xs = [point.x() for point in shape.points]
2933
+ ys = [point.y() for point in shape.points]
2934
+ offset = self.canvas.offset_to_center()
2935
+ point = QPoint(
2936
+ int(round((max(xs) + offset.x()) * self.canvas.scale)),
2937
+ int(round((min(ys) + offset.y()) * self.canvas.scale)))
2938
+ return self.canvas.mapToGlobal(point)
2939
+
2940
+ def _commit_provisional_shape(self, text):
2941
+ text = str(text).strip()
2942
+ pending = self._pending_provisional_shape
2943
+ if (not text or pending is None
2944
+ or self.canvas.provisional_shape is not pending):
2945
+ self._cancel_provisional_shape()
2946
+ return
2947
+
2948
+ video_before = (self.video_model.snapshot_state()
2949
+ if self.document_kind == DocumentKind.VIDEO else None)
2950
+ shape = self.canvas.commit_provisional_shape(
2951
+ text, generate_color_by_text(text))
2952
+ self._pending_provisional_shape = None
2953
+ if shape is None:
2954
+ return
2955
+ self.add_label(shape)
2956
+
2957
+ if self.document_kind == DocumentKind.VIDEO:
2958
+ track_id = self._store_video_shape_as_manual(shape)
2959
+ self._selected_video_track_id = track_id
2960
+ video_after = self.video_model.snapshot_state()
2961
+ self.undo_stack.push(CreateShapeCommand(
2962
+ self, shape, video_before=video_before,
2963
+ video_after=video_after))
2964
+ self._on_video_model_mutation()
2965
+ self._materialize_video_frame(self.current_video_frame_ref.pts)
2897
2966
  else:
2898
- text = self.default_label
2967
+ self.undo_stack.push(CreateShapeCommand(self, shape))
2968
+ self.set_dirty()
2899
2969
 
2900
- # Add Chris
2901
- self.diffc_button.setChecked(False)
2902
- if text is not None:
2903
- self.prev_label_text = text
2904
- generate_color = generate_color_by_text(text)
2905
- shape = self.canvas.set_last_label(text, generate_color)
2906
- self.add_label(shape)
2970
+ self.prev_label_text = text
2971
+ self.lastLabel = text
2972
+ self._session_last_class = text
2973
+ if text not in self.label_hist:
2974
+ self.label_hist.append(text)
2975
+ self._update_current_image_stats()
2976
+ self._sync_tool_actions()
2977
+ self._restore_canvas_focus()
2907
2978
 
2908
- if self.document_kind == DocumentKind.VIDEO:
2909
- before = self.video_model.snapshot_state()
2910
- track_id = self._store_video_shape_as_manual(shape)
2911
- self._selected_video_track_id = track_id
2912
- after = self.video_model.snapshot_state()
2913
- self.undo_stack.push(VideoModelCommand(
2914
- self, before, after, 'Create video track'))
2915
- self._on_video_model_mutation()
2916
- self._materialize_video_frame(
2917
- self.current_video_frame_ref.pts)
2918
- else:
2919
- # Shape already exists, so record without re-executing it.
2920
- cmd = CreateShapeCommand(self, shape)
2921
- self.undo_stack.push(cmd)
2979
+ def _cancel_provisional_shape(self):
2980
+ self._pending_provisional_shape = None
2981
+ self.canvas.discard_provisional_shape()
2982
+ self._sync_tool_actions()
2983
+ self._restore_canvas_focus()
2922
2984
 
2923
- if self.beginner(): # Switch to edit mode.
2924
- self.canvas.set_editing(True)
2925
- self.actions.create.setEnabled(True)
2926
- self.actions.create_polygon.setEnabled(True)
2927
- else:
2928
- self.actions.editMode.setEnabled(True)
2929
- self.actions.create_polygon.setEnabled(True)
2930
- if self.document_kind != DocumentKind.VIDEO:
2931
- self.set_dirty()
2932
- self._update_current_image_stats()
2985
+ def _restore_canvas_focus(self):
2986
+ QApplication.setActiveWindow(self)
2987
+ self.activateWindow()
2988
+ self.setFocusProxy(self.canvas)
2989
+ self.canvas.setFocus(Qt.OtherFocusReason)
2933
2990
 
2934
- if text not in self.label_hist:
2935
- self.label_hist.append(text)
2936
- else:
2937
- # self.canvas.undoLastLine()
2938
- self.canvas.reset_all_lines()
2991
+ def _dismiss_class_picker(self, discard=True):
2992
+ picker = getattr(self, 'class_picker', None)
2993
+ if picker is not None:
2994
+ blocked = picker.blockSignals(True)
2995
+ picker.hide()
2996
+ picker.blockSignals(blocked)
2997
+ self._pending_provisional_shape = None
2998
+ if discard and hasattr(self, 'canvas'):
2999
+ self.canvas.discard_provisional_shape()
2939
3000
 
2940
3001
  def scroll_request(self, delta, orientation):
2941
3002
  units = - delta / (8 * 15)
@@ -4886,6 +4947,7 @@ class MainWindow(QMainWindow, WindowMixin):
4886
4947
  settings[SETTING_GRID_SIZE] = self.canvas._grid_size if self.canvas else 32
4887
4948
  settings[SETTING_EDGE_ALIGNMENT] = self.edge_alignment_option.isChecked()
4888
4949
  settings[SETTING_SHORTCUTS] = self.shortcut_config.to_dict()
4950
+ settings[SETTING_SAM_OUTPUT_MODE] = self.sam_output_mode
4889
4951
  settings.save()
4890
4952
  self._shutdown_workers()
4891
4953
 
@@ -4896,6 +4958,7 @@ class MainWindow(QMainWindow, WindowMixin):
4896
4958
  QTimer.singleShot(0, self.close)
4897
4959
 
4898
4960
  def _shutdown_workers(self):
4961
+ self._dismiss_class_picker()
4899
4962
  self._plugin_document_ready = False
4900
4963
  self._publish_plugin_document(new_generation=True, force=True)
4901
4964
  if hasattr(self, 'plugin_manager'):
@@ -6527,7 +6590,11 @@ class MainWindow(QMainWindow, WindowMixin):
6527
6590
  # Statistics methods (Issue #19) - Stats shown in gallery mode
6528
6591
  def _refresh_all_statistics(self):
6529
6592
  """Complete label data in the shared catalog, then aggregate it."""
6530
- self._ensure_annotation_catalog()
6593
+ # A refresh may already be queued when closeEvent shuts the worker
6594
+ # lanes down. Submitting from that late Qt callback raises inside a
6595
+ # slot and aborts the process, so closed windows must be inert.
6596
+ if not self._ensure_annotation_catalog():
6597
+ return
6531
6598
  self.annotation_catalog.request_statistics()
6532
6599
 
6533
6600
  def _update_current_image_stats(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labelimgplusplus
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: labelImg++ is an enhanced graphical image annotation tool with Gallery Mode for browsing and labeling images
5
5
  Author-email: Abhik Sarkar <abhiksark@gmail.com>
6
6
  License: Copyright (c) <2015-Present> Tzutalin
@@ -78,7 +78,7 @@ polygons, and keypoints, designed for machine learning and computer vision
78
78
  projects. It is forked from the original LabelImg with significant
79
79
  enhancements.
80
80
 
81
- **Version 3.2.0 (stable).** Install with
81
+ **Version 3.3.0 (stable).** Install with
82
82
  ``pip install labelimgplusplus``.
83
83
 
84
84
  .. image:: https://raw.githubusercontent.com/abhiksark/labelImg-plus-plus/c7fbd5fc08a561206b210706143a50023c82a782/resources/demo/demo.gif
@@ -132,6 +132,18 @@ Workflow and Interface
132
132
  annotation-status filter close at hand, and the inspector can be collapsed
133
133
  when maximum canvas space is needed.
134
134
 
135
+ **Faster Class Confirmation**
136
+ New boxes, polygons, Smart Select results, and video geometry remain
137
+ provisional while a non-modal class picker opens beside the annotation.
138
+ Filter or enter a class, press **Enter** to commit one undoable annotation,
139
+ or press **Escape** to discard it without changing the document. Default
140
+ labels and established single-class sessions bypass the picker.
141
+
142
+ **Smart Select Output Modes**
143
+ While Smart Select is active, choose **Box** or **Polygon** from the compact
144
+ canvas control. The choice persists between sessions, and either result
145
+ follows the same provisional class-confirmation and undo workflow.
146
+
135
147
  **Direct Ultralytics Dataset Export**
136
148
  Choose **Tools → Export Ultralytics Dataset…** to create a ready-to-train
137
149
  YOLO detection dataset with ``images/{train,val,test}``, matching
@@ -401,7 +413,7 @@ Or use **Menu > File > Reset All**
401
413
  Release History
402
414
  ---------------
403
415
 
404
- This source tree identifies itself as **3.2.0**, the stable 3.2 release. See
416
+ This source tree identifies itself as **3.3.0**, the stable 3.3 release. See
405
417
  the `release history
406
418
  <https://github.com/abhiksark/labelImg-plus-plus/blob/master/HISTORY.rst>`__ for
407
419
  version-by-version changes and `GitHub Releases
@@ -29,6 +29,7 @@ libs/core/plugin_manager.py
29
29
  libs/core/plugin_registry.py
30
30
  libs/core/profiling.py
31
31
  libs/core/sam_controller.py
32
+ libs/core/sam_types.py
32
33
  libs/core/save_pipeline.py
33
34
  libs/core/settings.py
34
35
  libs/core/shape.py
@@ -82,6 +83,7 @@ libs/widgets/commandBar.py
82
83
  libs/widgets/default_label_combobox.py
83
84
  libs/widgets/galleryWidget.py
84
85
  libs/widgets/gallery_status_controller.py
86
+ libs/widgets/inlineClassPicker.py
85
87
  libs/widgets/keypointPanel.py
86
88
  libs/widgets/labelCheckerDialog.py
87
89
  libs/widgets/labelDialog.py
@@ -1,7 +1,7 @@
1
1
  # libs/__init__.py
2
2
  """LabelImg++ library package."""
3
3
 
4
- __version__ = '3.2.0'
4
+ __version__ = '3.3.0'
5
5
  __version_info__ = tuple(__version__.split('.'))
6
6
 
7
7
  # Re-export subpackages for convenient access
@@ -75,7 +75,8 @@ class CreateShapeCommand(Command):
75
75
  Undo removes the shape from canvas and label list.
76
76
  """
77
77
 
78
- def __init__(self, main_window, shape):
78
+ def __init__(self, main_window, shape, video_before=None,
79
+ video_after=None):
79
80
  """Initialize with reference to main window and the created shape.
80
81
 
81
82
  Args:
@@ -84,9 +85,21 @@ class CreateShapeCommand(Command):
84
85
  """
85
86
  self.main_window = main_window
86
87
  self.shape = shape
88
+ self.video_before = video_before
89
+ self.video_after = video_after
90
+
91
+ def _restore_video(self, state):
92
+ self.main_window.video_model.restore_state(state)
93
+ self.main_window._on_video_model_mutation()
94
+ frame_ref = self.main_window.current_video_frame_ref
95
+ if frame_ref is not None:
96
+ self.main_window._materialize_video_frame(frame_ref.pts)
87
97
 
88
98
  def execute(self):
89
99
  """Add the shape to canvas and label list."""
100
+ if self.video_after is not None:
101
+ self._restore_video(self.video_after)
102
+ return
90
103
  self.main_window.canvas.shapes.append(self.shape)
91
104
  _rebuild_spatial(self.main_window.canvas)
92
105
  self.main_window.add_label(self.shape)
@@ -94,6 +107,9 @@ class CreateShapeCommand(Command):
94
107
 
95
108
  def undo(self):
96
109
  """Remove the shape from canvas and label list."""
110
+ if self.video_before is not None:
111
+ self._restore_video(self.video_before)
112
+ return
97
113
  if self.shape in self.main_window.canvas.shapes:
98
114
  self.main_window.canvas.shapes.remove(self.shape)
99
115
  _rebuild_spatial(self.main_window.canvas)
@@ -104,6 +120,8 @@ class CreateShapeCommand(Command):
104
120
 
105
121
  @property
106
122
  def description(self):
123
+ if self.video_after is not None:
124
+ return f"Create video track '{self.shape.label}'"
107
125
  return f"Create shape '{self.shape.label}'"
108
126
 
109
127
 
@@ -1,5 +1,5 @@
1
1
  # libs/core/sam_controller.py
2
- """Orchestrates SAM-assisted polygon creation.
2
+ """Orchestrates SAM-assisted polygon or rectangle creation.
3
3
 
4
4
  Bridges canvas clicks to worker-thread inference and routes results back to the
5
5
  canvas on the main thread. Top-level imports are Qt + stdlib only; numpy and the
@@ -19,7 +19,7 @@ from libs.core.profiling import recorder as trace_recorder
19
19
 
20
20
 
21
21
  class _SamSignals(QObject):
22
- # (generation, points or None, backend created this run or None)
22
+ # (generation, SamResult or None, backend created this run or None)
23
23
  finished = pyqtSignal(int, object, object)
24
24
  failed = pyqtSignal(int, str) # (generation, message)
25
25
 
@@ -41,15 +41,15 @@ class _SamTask(QRunnable):
41
41
 
42
42
  def run(self):
43
43
  try:
44
- generation, points, created = self.execute()
44
+ generation, result, created = self.execute()
45
45
  if self._signals is not None:
46
- self._signals.finished.emit(generation, points, created)
46
+ self._signals.finished.emit(generation, result, created)
47
47
  except Exception as exc:
48
48
  if self._signals is not None:
49
49
  self._signals.failed.emit(self._generation, str(exc))
50
50
 
51
51
  def execute(self):
52
- from libs.integrations.mask_to_polygon import mask_to_polygon
52
+ from libs.integrations.mask_to_polygon import mask_to_sam_result
53
53
  backend = self._backend
54
54
  created = None
55
55
  if backend is None:
@@ -72,16 +72,16 @@ class _SamTask(QRunnable):
72
72
  if trace_recorder is not None:
73
73
  trace_recorder.complete('sam.image.embed', started)
74
74
  if self._point is None:
75
- return self._generation, [], created
75
+ return self._generation, None, created
76
76
  if trace_recorder is not None:
77
77
  import time
78
78
  started = time.perf_counter_ns()
79
79
  mask = backend.predict(
80
80
  [(self._point.x(), self._point.y())], [1])
81
- points = mask_to_polygon(mask)
81
+ result = mask_to_sam_result(mask)
82
82
  if trace_recorder is not None:
83
83
  trace_recorder.complete('sam.inference', started)
84
- return self._generation, points, created
84
+ return self._generation, result, created
85
85
 
86
86
 
87
87
  class SamController:
@@ -197,7 +197,7 @@ class SamController:
197
197
  self._embedded_key = None
198
198
  self._start_pending()
199
199
 
200
- def _on_finished(self, generation, points, created):
200
+ def _on_finished(self, generation, result, created):
201
201
  # Only one task is ever in flight (segment_at's _busy guard, and neither
202
202
  # cancel nor on_image_changed releases _busy), so the task completing here
203
203
  # is always the sole in-flight one: clearing _busy is unconditional/safe.
@@ -209,12 +209,16 @@ class SamController:
209
209
  if generation != self._gen:
210
210
  self._start_pending()
211
211
  return
212
- if not points:
212
+ if result is None:
213
213
  if self._active_point is not None:
214
214
  self.mw.status("No object found, try another point")
215
215
  self._start_pending()
216
216
  return
217
- self.mw.canvas.commit_polygon(points)
217
+ output_mode = getattr(self.mw, 'sam_output_mode', 'polygon')
218
+ if output_mode == 'box':
219
+ self.mw.canvas.commit_rectangle(result.bounds)
220
+ else:
221
+ self.mw.canvas.commit_polygon(result.polygon)
218
222
  self._start_pending()
219
223
 
220
224
  def _on_failed(self, generation, message):