labelimgplusplus 2.3.0__tar.gz → 2.3.2__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 (58) hide show
  1. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/HISTORY.rst +40 -0
  2. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/PKG-INFO +1 -1
  3. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelImgPlusPlus.py +44 -14
  4. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/PKG-INFO +1 -1
  5. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/__init__.py +2 -2
  6. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/commands.py +76 -0
  7. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/create_ml_io.py +3 -4
  8. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/yolo_io.py +21 -3
  9. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/yolo_seg_io.py +4 -2
  10. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/resources.py +43 -43
  11. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/label_checker.py +0 -2
  12. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/canvas.py +81 -0
  13. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/pyproject.toml +1 -1
  14. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/CONTRIBUTING.rst +0 -0
  15. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/LICENSE +0 -0
  16. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/MANIFEST.in +0 -0
  17. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/README.rst +0 -0
  18. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/data/predefined_classes.txt +0 -0
  19. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/SOURCES.txt +0 -0
  20. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/dependency_links.txt +0 -0
  21. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/entry_points.txt +0 -0
  22. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/requires.txt +0 -0
  23. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/top_level.txt +0 -0
  24. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/__init__.py +0 -0
  25. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/keypoint_config.py +0 -0
  26. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/settings.py +0 -0
  27. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/shape.py +0 -0
  28. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/shortcut_config.py +0 -0
  29. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/__init__.py +0 -0
  30. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/coco_io.py +0 -0
  31. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/labelFile.py +0 -0
  32. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/pascal_voc_io.py +0 -0
  33. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/__init__.py +0 -0
  34. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/dataset_splitter.py +0 -0
  35. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/__init__.py +0 -0
  36. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/constants.py +0 -0
  37. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/hashableQListWidgetItem.py +0 -0
  38. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/stringBundle.py +0 -0
  39. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/styles.py +0 -0
  40. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/ustr.py +0 -0
  41. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/utils.py +0 -0
  42. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/__init__.py +0 -0
  43. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/batchVerifyDialog.py +0 -0
  44. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/colorDialog.py +0 -0
  45. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/combobox.py +0 -0
  46. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/default_label_combobox.py +0 -0
  47. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/galleryWidget.py +0 -0
  48. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/keypointPanel.py +0 -0
  49. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/labelCheckerDialog.py +0 -0
  50. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/labelDialog.py +0 -0
  51. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/lightWidget.py +0 -0
  52. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/shortcutsDialog.py +0 -0
  53. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/splitDialog.py +0 -0
  54. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/statsWidget.py +0 -0
  55. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/toolBar.py +0 -0
  56. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/zoomWidget.py +0 -0
  57. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/resources.qrc +0 -0
  58. {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/setup.cfg +0 -0
@@ -1,6 +1,46 @@
1
1
  History
2
2
  =======
3
3
 
4
+ 2.3.2 (2026-05-20)
5
+ ------------------
6
+
7
+ Patch release hardening the annotation format loaders against malformed
8
+ input. Slightly corrupt or hand-edited label files previously crashed the
9
+ load or silently dropped valid annotations.
10
+
11
+ * Skip individual malformed YOLO lines instead of discarding the whole
12
+ file — a line with non-numeric values no longer raises an error that
13
+ drops every annotation in that file
14
+ * Report corrupt CreateML JSON through an error dialog instead of
15
+ silently loading zero annotations, and defer the format switch until
16
+ the reader succeeds so a bad file no longer flips the saved format
17
+ * Fix the CreateML branch of label extraction, which passed the wrong
18
+ number of constructor arguments and could never return labels
19
+ * Replace a mutable default argument in the YOLO writer that could leak
20
+ class names across files and corrupt class indices in ``classes.txt``
21
+ * Guard ``load_predefined_classes`` against a missing class-file path
22
+
23
+ Internal: tolerate a PyQt5/coverage shutdown segfault in CI, ignore
24
+ coverage and local working-directory artifacts, and add 8 regression
25
+ tests (full suite now 471 passing).
26
+
27
+
28
+ 2.3.1 (2026-05-14)
29
+ ------------------
30
+
31
+ Patch release fixing five blocker-tier issues identified in the v2.3.0 review.
32
+
33
+ * Restore Ctrl-Z for polygon vertex insert, vertex delete, vertex drag-move, and
34
+ keypoint placement — these previously silently undid the prior rectangle action
35
+ * Clear polygon/keypoint mode state when loading a new file, preventing
36
+ stale-reference crashes after switching images mid-annotation
37
+ * Exit keypoint mode safely when the active keypoint shape is deleted
38
+ * Defer format switch in COCO and YOLO-seg loaders until the reader succeeds,
39
+ so a malformed annotation file no longer silently flips the saved format
40
+ * Clamp YOLO-seg normalized coordinates to ``[0.0, 1.0]`` to prevent
41
+ out-of-range values when a vertex is dragged past the image edge
42
+
43
+
4
44
  2.3.0 (2026-03-27)
5
45
  ------------------
6
46
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labelimgplusplus
3
- Version: 2.3.0
3
+ Version: 2.3.2
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
@@ -59,7 +59,10 @@ from libs.widgets.keypointPanel import KeypointPanel
59
59
  # Core
60
60
  from libs.core.shape import Shape, ShapeType, DEFAULT_LINE_COLOR, DEFAULT_FILL_COLOR
61
61
  from libs.core.settings import Settings
62
- from libs.core.commands import UndoStack, CreateShapeCommand, DeleteShapeCommand, MoveShapeCommand, EditLabelCommand
62
+ from libs.core.commands import (
63
+ UndoStack, CreateShapeCommand, DeleteShapeCommand, MoveShapeCommand,
64
+ EditLabelCommand, EditPolygonVerticesCommand, EditKeypointsCommand,
65
+ )
63
66
  from libs.core.shortcut_config import ShortcutConfig
64
67
 
65
68
  # Formats
@@ -593,6 +596,9 @@ class MainWindow(QMainWindow, WindowMixin):
593
596
  self.canvas.newShape.connect(self.new_shape)
594
597
  self.canvas.shapeMoved.connect(self.set_dirty)
595
598
  self.canvas.shapeMoved.connect(self._on_shape_moved_keypoints)
599
+ self.canvas.polygonVerticesEdited.connect(
600
+ self._on_polygon_vertices_edited)
601
+ self.canvas.keypointsEdited.connect(self._on_keypoints_edited)
596
602
  self.canvas.selectionChanged.connect(self.shape_selection_changed)
597
603
  self.canvas.drawingPolygon.connect(self.toggle_drawing_sensitive)
598
604
 
@@ -1692,6 +1698,21 @@ class MainWindow(QMainWindow, WindowMixin):
1692
1698
  self.keypoint_panel.set_current_index(
1693
1699
  self.canvas._keypoint_index)
1694
1700
 
1701
+ def _on_polygon_vertices_edited(self, shape, old_points):
1702
+ """Capture polygon vertex edits for undo support."""
1703
+ cmd = EditPolygonVerticesCommand(
1704
+ self, shape, old_points, list(shape.points))
1705
+ self.undo_stack.push(cmd)
1706
+ self.set_dirty()
1707
+
1708
+ def _on_keypoints_edited(self, shape, old_keypoints):
1709
+ """Capture keypoint mutations for undo support."""
1710
+ cmd = EditKeypointsCommand(
1711
+ self, shape, old_keypoints,
1712
+ list(shape.keypoints) if shape.keypoints else None)
1713
+ self.undo_stack.push(cmd)
1714
+ self.set_dirty()
1715
+
1695
1716
  def toggle_drawing_sensitive(self, drawing=True):
1696
1717
  """In the middle of drawing, toggling between modes should be disabled."""
1697
1718
  self.actions.editMode.setEnabled(not drawing)
@@ -3363,7 +3384,7 @@ class MainWindow(QMainWindow, WindowMixin):
3363
3384
  self.set_dirty()
3364
3385
 
3365
3386
  def load_predefined_classes(self, predef_classes_file):
3366
- if os.path.exists(predef_classes_file) is True:
3387
+ if predef_classes_file and os.path.exists(predef_classes_file):
3367
3388
  with codecs.open(predef_classes_file, 'r', 'utf8') as f:
3368
3389
  for line in f:
3369
3390
  line = line.strip()
@@ -3424,10 +3445,17 @@ class MainWindow(QMainWindow, WindowMixin):
3424
3445
  if os.path.isfile(json_path) is False:
3425
3446
  return
3426
3447
 
3427
- self.set_format(FORMAT_CREATEML)
3448
+ try:
3449
+ create_ml_parse_reader = CreateMLReader(json_path, file_path)
3450
+ shapes = create_ml_parse_reader.get_shapes()
3451
+ except Exception as e:
3452
+ self.error_message(
3453
+ 'Annotation Error',
3454
+ f'Error loading CreateML annotations from '
3455
+ f'{os.path.basename(json_path)}: {e}')
3456
+ return
3428
3457
 
3429
- create_ml_parse_reader = CreateMLReader(json_path, file_path)
3430
- shapes = create_ml_parse_reader.get_shapes()
3458
+ self.set_format(FORMAT_CREATEML)
3431
3459
  self.load_labels(shapes)
3432
3460
  self.canvas.verified = create_ml_parse_reader.verified
3433
3461
 
@@ -3443,19 +3471,20 @@ class MainWindow(QMainWindow, WindowMixin):
3443
3471
  if not os.path.isfile(json_path):
3444
3472
  return
3445
3473
 
3446
- self.set_format(FORMAT_COCO)
3447
-
3448
3474
  image_filename = os.path.basename(file_path)
3449
3475
  try:
3450
3476
  reader = COCOReader(json_path, image_filename)
3451
3477
  shapes = reader.get_shapes()
3452
- self.load_labels(shapes)
3453
- self.canvas.verified = reader.verified
3454
3478
  except Exception as e:
3455
3479
  self.error_message(
3456
3480
  'Annotation Error',
3457
3481
  f'Error loading COCO annotations from '
3458
3482
  f'{os.path.basename(json_path)}: {e}')
3483
+ return
3484
+
3485
+ self.set_format(FORMAT_COCO)
3486
+ self.load_labels(shapes)
3487
+ self.canvas.verified = reader.verified
3459
3488
 
3460
3489
  def load_yolo_seg_by_filename(self, txt_path):
3461
3490
  """Load annotations from a YOLO-seg text file.
@@ -3468,8 +3497,6 @@ class MainWindow(QMainWindow, WindowMixin):
3468
3497
  if not os.path.isfile(txt_path):
3469
3498
  return
3470
3499
 
3471
- self.set_format(FORMAT_YOLO_SEG)
3472
-
3473
3500
  try:
3474
3501
  if hasattr(self, '_original_image_size') and self._original_image_size is not None:
3475
3502
  class MockImage:
@@ -3488,13 +3515,16 @@ class MainWindow(QMainWindow, WindowMixin):
3488
3515
  else:
3489
3516
  reader = YOLOSegReader(txt_path, self.image)
3490
3517
  shapes = reader.get_shapes()
3491
- self.load_labels(shapes)
3492
- self.canvas.verified = reader.verified
3493
3518
  except Exception as e:
3494
3519
  self.error_message(
3495
3520
  'Annotation Error',
3496
3521
  f'Error loading YOLO-seg annotations for '
3497
3522
  f'{os.path.basename(txt_path)}: {e}')
3523
+ return
3524
+
3525
+ self.set_format(FORMAT_YOLO_SEG)
3526
+ self.load_labels(shapes)
3527
+ self.canvas.verified = reader.verified
3498
3528
 
3499
3529
  def copy_previous_bounding_boxes(self):
3500
3530
  current_index = self._path_to_idx.get(self.file_path, 0)
@@ -3809,7 +3839,7 @@ class MainWindow(QMainWindow, WindowMixin):
3809
3839
  json_path = base_path + JSON_EXT
3810
3840
  if os.path.exists(json_path):
3811
3841
  try:
3812
- reader = CreateMLReader(json_path)
3842
+ reader = CreateMLReader(json_path, img_path)
3813
3843
  shapes = reader.get_shapes()
3814
3844
  labels = [shape[0] for shape in shapes]
3815
3845
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: labelimgplusplus
3
- Version: 2.3.0
3
+ Version: 2.3.2
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
@@ -1,8 +1,8 @@
1
1
  # libs/__init__.py
2
2
  """LabelImg++ library package."""
3
3
 
4
- __version_info__ = ('2', '1', '1')
5
- __version__ = '2.1.1'
4
+ __version_info__ = ('2', '3', '2')
5
+ __version__ = '2.3.2'
6
6
 
7
7
  # Re-export subpackages for convenient access
8
8
  from libs import core, formats, widgets, utils
@@ -189,6 +189,82 @@ class EditLabelCommand(Command):
189
189
  return f"Edit label '{self.old_label}' -> '{self.new_label}'"
190
190
 
191
191
 
192
+ class EditPolygonVerticesCommand(Command):
193
+ """Command for polygon vertex insert / remove / move operations.
194
+
195
+ Snapshots the full points list before and after the mutation;
196
+ undo restores the snapshot.
197
+ """
198
+
199
+ def __init__(self, main_window, shape, old_points, new_points):
200
+ """Initialize with shape and its point lists.
201
+
202
+ Args:
203
+ main_window: The MainWindow instance.
204
+ shape: The Shape object being edited.
205
+ old_points: List of QPointF before the mutation.
206
+ new_points: List of QPointF after the mutation.
207
+ """
208
+ self.main_window = main_window
209
+ self.shape = shape
210
+ self.old_points = [QPointF(p.x(), p.y()) for p in old_points]
211
+ self.new_points = [QPointF(p.x(), p.y()) for p in new_points]
212
+
213
+ def execute(self):
214
+ """Apply the post-mutation point list."""
215
+ self.shape.points = [QPointF(p.x(), p.y()) for p in self.new_points]
216
+ self.main_window.canvas.update()
217
+
218
+ def undo(self):
219
+ """Restore the pre-mutation point list."""
220
+ self.shape.points = [QPointF(p.x(), p.y()) for p in self.old_points]
221
+ self.main_window.canvas.update()
222
+
223
+ @property
224
+ def description(self):
225
+ return (
226
+ f"Edit polygon vertices "
227
+ f"({len(self.old_points)} -> {len(self.new_points)})"
228
+ )
229
+
230
+
231
+ class EditKeypointsCommand(Command):
232
+ """Command for keypoint placement / clear operations.
233
+
234
+ Snapshots the keypoints list before and after; undo restores.
235
+ """
236
+
237
+ def __init__(self, main_window, shape, old_keypoints, new_keypoints):
238
+ """Initialize with shape and keypoint snapshots.
239
+
240
+ Args:
241
+ main_window: The MainWindow instance.
242
+ shape: The Shape object being edited.
243
+ old_keypoints: List of keypoint tuples before mutation (or None).
244
+ new_keypoints: List of keypoint tuples after mutation (or None).
245
+ """
246
+ self.main_window = main_window
247
+ self.shape = shape
248
+ self.old_keypoints = list(old_keypoints) if old_keypoints else None
249
+ self.new_keypoints = list(new_keypoints) if new_keypoints else None
250
+
251
+ def execute(self):
252
+ """Apply the post-mutation keypoint list."""
253
+ self.shape.keypoints = (
254
+ list(self.new_keypoints) if self.new_keypoints else None)
255
+ self.main_window.canvas.update()
256
+
257
+ def undo(self):
258
+ """Restore the pre-mutation keypoint list."""
259
+ self.shape.keypoints = (
260
+ list(self.old_keypoints) if self.old_keypoints else None)
261
+ self.main_window.canvas.update()
262
+
263
+ @property
264
+ def description(self):
265
+ return f"Edit keypoints on '{self.shape.label}'"
266
+
267
+
192
268
  class UndoStack:
193
269
  """Manages command history for undo/redo operations.
194
270
 
@@ -87,10 +87,9 @@ class CreateMLReader:
87
87
  self.shapes = []
88
88
  self.verified = False
89
89
  self.filename = os.path.basename(file_path)
90
- try:
91
- self.parse_json()
92
- except ValueError:
93
- print("JSON decoding failed")
90
+ # Let parse failures propagate so callers can surface them, instead
91
+ # of silently swallowing a malformed file into an empty shape list.
92
+ self.parse_json()
94
93
 
95
94
  def parse_json(self):
96
95
  with open(self.json_path, "r") as file:
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf8 -*-
3
+ # libs/formats/yolo_io.py
3
4
  import codecs
4
5
  import os
5
6
 
@@ -25,7 +26,10 @@ class YOLOWriter:
25
26
  bnd_box['difficult'] = difficult
26
27
  self.box_list.append(bnd_box)
27
28
 
28
- def bnd_box_to_yolo_line(self, box, class_list=[]):
29
+ def bnd_box_to_yolo_line(self, box, class_list=None):
30
+ if class_list is None:
31
+ class_list = []
32
+
29
33
  x_min = box['xmin']
30
34
  x_max = box['xmax']
31
35
  y_min = box['ymin']
@@ -46,7 +50,10 @@ class YOLOWriter:
46
50
 
47
51
  return class_index, x_center, y_center, w, h
48
52
 
49
- def save(self, class_list=[], target_file=None):
53
+ def save(self, class_list=None, target_file=None):
54
+ if class_list is None:
55
+ class_list = []
56
+
50
57
  if target_file is None:
51
58
  out_path = self.filename + TXT_EXT
52
59
  else:
@@ -140,8 +147,19 @@ class YoloReader:
140
147
  continue # Skip malformed lines
141
148
  class_index, x_center, y_center, w, h = parts
142
149
 
150
+ # Validate that all tokens are numeric - a line can have the
151
+ # right token count yet still hold garbage. Skip gracefully
152
+ # instead of raising ValueError out of the reader.
153
+ try:
154
+ idx = int(class_index)
155
+ x_center, y_center, w, h = (
156
+ float(x_center), float(y_center), float(w), float(h))
157
+ except ValueError:
158
+ print(f"Warning: Skipping invalid annotation in {self.file_path}: "
159
+ f"line {line_num} has non-numeric values")
160
+ continue # Skip this line, continue with next
161
+
143
162
  # Validate class index - skip invalid lines gracefully
144
- idx = int(class_index)
145
163
  if idx < 0 or idx >= len(self.classes):
146
164
  print(f"Warning: Skipping invalid annotation in {self.file_path}: "
147
165
  f"class index {idx} at line {line_num} is out of range "
@@ -53,8 +53,10 @@ class YOLOSegWriter:
53
53
 
54
54
  coords = []
55
55
  for x, y in entry['points']:
56
- coords.append('%.6f' % (x / w))
57
- coords.append('%.6f' % (y / h))
56
+ nx = max(0.0, min(1.0, x / w))
57
+ ny = max(0.0, min(1.0, y / h))
58
+ coords.append('%.6f' % nx)
59
+ coords.append('%.6f' % ny)
58
60
 
59
61
  f.write('%d %s\n' % (class_idx, ' '.join(coords)))
60
62
 
@@ -4195,91 +4195,91 @@ qt_resource_struct_v2 = b"\
4195
4195
  \x00\x00\x00\x00\x00\x02\x00\x00\x00\x2b\x00\x00\x00\x01\
4196
4196
  \x00\x00\x00\x00\x00\x00\x00\x00\
4197
4197
  \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
4198
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4198
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa4\
4199
4199
  \x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x7a\x97\
4200
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4200
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4201
4201
  \x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x7b\xd5\
4202
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4202
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4203
4203
  \x00\x00\x00\x24\x00\x00\x00\x00\x00\x01\x00\x00\x7e\x0a\
4204
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4204
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4205
4205
  \x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x3f\
4206
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4206
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4207
4207
  \x00\x00\x00\x40\x00\x00\x00\x00\x00\x01\x00\x00\x80\x1c\
4208
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4208
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4209
4209
  \x00\x00\x00\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x81\x2d\
4210
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4210
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4211
4211
  \x00\x00\x00\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x82\x50\
4212
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4212
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4213
4213
  \x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x83\x84\
4214
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4214
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4215
4215
  \x00\x00\x00\x78\x00\x00\x00\x00\x00\x01\x00\x00\x85\x23\
4216
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4216
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4217
4217
  \x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x86\x00\
4218
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4218
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4219
4219
  \x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x87\x11\
4220
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4220
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4221
4221
  \x00\x00\x00\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x87\xef\
4222
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4222
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4223
4223
  \x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x88\xfe\
4224
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4224
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4225
4225
  \x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x8a\x36\
4226
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4226
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4227
4227
  \x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x8b\x86\
4228
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4228
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4229
4229
  \x00\x00\x00\xda\x00\x00\x00\x00\x00\x01\x00\x00\x8c\x8f\
4230
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4230
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4231
4231
  \x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x8d\x95\
4232
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4232
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4233
4233
  \x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x99\
4234
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4234
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4235
4235
  \x00\x00\x01\x08\x00\x00\x00\x00\x00\x01\x00\x00\x8f\x84\
4236
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4236
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4237
4237
  \x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x00\x90\xd6\
4238
- \x00\x00\x01\x9d\x2c\x32\x27\x27\
4238
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa8\
4239
4239
  \x00\x00\x01\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x9b\x2a\
4240
- \x00\x00\x01\x9d\x2c\x32\x27\x27\
4240
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa8\
4241
4241
  \x00\x00\x01\x5c\x00\x00\x00\x00\x00\x01\x00\x00\xa4\x77\
4242
- \x00\x00\x01\x9d\x2c\x32\x27\x26\
4242
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa8\
4243
4243
  \x00\x00\x01\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xb0\xcb\
4244
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4244
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4245
4245
  \x00\x00\x01\x96\x00\x00\x00\x00\x00\x01\x00\x00\xb3\xe1\
4246
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4246
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4247
4247
  \x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\xb5\x2d\
4248
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4248
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4249
4249
  \x00\x00\x01\xc6\x00\x00\x00\x00\x00\x01\x00\x00\xb6\x59\
4250
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4250
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4251
4251
  \x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\xb7\x56\
4252
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4252
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4253
4253
  \x00\x00\x01\xf2\x00\x00\x00\x00\x00\x01\x00\x00\xb8\xd9\
4254
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4254
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4255
4255
  \x00\x00\x02\x04\x00\x00\x00\x00\x00\x01\x00\x00\xba\xe5\
4256
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4256
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4257
4257
  \x00\x00\x02\x24\x00\x00\x00\x00\x00\x01\x00\x00\xbd\x1a\
4258
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4258
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4259
4259
  \x00\x00\x02\x36\x00\x00\x00\x00\x00\x01\x00\x00\xbe\x78\
4260
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4260
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4261
4261
  \x00\x00\x02\x4e\x00\x00\x00\x00\x00\x01\x00\x00\xbf\x9b\
4262
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4262
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa4\
4263
4263
  \x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\xc0\xad\
4264
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4264
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4265
4265
  \x00\x00\x02\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xc3\x54\
4266
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4266
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4267
4267
  \x00\x00\x02\x90\x00\x00\x00\x00\x00\x01\x00\x00\xc4\x8e\
4268
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4268
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4269
4269
  \x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00\xc6\xc3\
4270
- \x00\x00\x01\x9d\x2c\x32\x27\x27\
4270
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa8\
4271
4271
  \x00\x00\x02\xc0\x00\x00\x00\x00\x00\x01\x00\x00\xd9\x78\
4272
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4272
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4273
4273
  \x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xe9\xb8\
4274
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4274
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4275
4275
  \x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x00\xea\x96\
4276
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4276
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4277
4277
  \x00\x00\x03\x20\x00\x00\x00\x00\x00\x01\x00\x00\xeb\x8b\
4278
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4278
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4279
4279
  \x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\xec\xdd\
4280
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4280
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4281
4281
  \x00\x00\x03\x4c\x00\x00\x00\x00\x00\x01\x00\x00\xf0\xa0\
4282
- \x00\x00\x01\x9d\x2c\x32\x27\x23\
4282
+ \x00\x00\x01\x9e\x44\x7a\x2d\xa5\
4283
4283
  "
4284
4284
 
4285
4285
  qt_version = [int(v) for v in QtCore.qVersion().split('.')]
@@ -8,9 +8,7 @@ from difflib import SequenceMatcher
8
8
  from enum import IntEnum
9
9
  from typing import Dict, List, Optional, Set, Tuple
10
10
 
11
- from libs.formats.yolo_io import YoloReader
12
11
  from libs.formats.pascal_voc_io import PascalVocReader
13
- from libs.formats.create_ml_io import CreateMLReader
14
12
 
15
13
 
16
14
  class IssueType(IntEnum):
@@ -64,6 +64,12 @@ class Canvas(QWidget):
64
64
  selectionChanged = pyqtSignal(bool)
65
65
  shapeMoved = pyqtSignal()
66
66
  drawingPolygon = pyqtSignal(bool)
67
+ # Emitted on polygon vertex insert / remove / drag-move with the
68
+ # pre-mutation points list so MainWindow can push an undo command.
69
+ polygonVerticesEdited = pyqtSignal(object, object) # (shape, old_points)
70
+ # Emitted on keypoint placement / mutation with the pre-mutation
71
+ # keypoints list (may be None) for undo support.
72
+ keypointsEdited = pyqtSignal(object, object) # (shape, old_keypoints)
67
73
 
68
74
  CREATE, EDIT, CREATE_POLYGON, KEYPOINT_MODE = list(range(4))
69
75
 
@@ -128,6 +134,11 @@ class Canvas(QWidget):
128
134
  self._keypoint_template_name = None
129
135
  self._keypoint_template = None
130
136
 
137
+ # Snapshot of points list captured at the start of a polygon
138
+ # vertex drag, so we can emit polygonVerticesEdited on release
139
+ # if the drag actually changed any vertex position.
140
+ self._polygon_drag_old_points = None
141
+
131
142
  def set_drawing_color(self, qcolor):
132
143
  self.drawing_line_color = qcolor
133
144
  self.drawing_rect_color = qcolor
@@ -445,15 +456,27 @@ class Canvas(QWidget):
445
456
  def mousePressEvent(self, ev):
446
457
  pos = self.transform_pos(ev.pos())
447
458
 
459
+ # Snapshot polygon points before any potential vertex drag.
460
+ # Emitted on release if any point actually changed.
461
+ self._polygon_drag_old_points = None
462
+ if (ev.button() == Qt.LeftButton
463
+ and self.selected_shape
464
+ and self.selected_shape.shape_type == ShapeType.POLYGON):
465
+ self._polygon_drag_old_points = list(self.selected_shape.points)
466
+
448
467
  # Keypoint placement
449
468
  if self.mode == self.KEYPOINT_MODE and self._keypoint_shape:
450
469
  kp_count = self._keypoint_count()
451
470
  if ev.button() == Qt.LeftButton and self._keypoint_index < kp_count:
452
471
  if not self.out_of_pixmap(pos):
472
+ old_kps = (list(self._keypoint_shape.keypoints)
473
+ if self._keypoint_shape.keypoints else None)
453
474
  if self._keypoint_shape.keypoints is None:
454
475
  self._keypoint_shape.keypoints = [None] * kp_count
455
476
  self._keypoint_shape.keypoints[self._keypoint_index] = (
456
477
  pos.x(), pos.y(), 2)
478
+ self._emit_keypoints_edit(
479
+ self._keypoint_shape, old_kps)
457
480
  self._keypoint_index = self._next_unplaced_keypoint(
458
481
  self._keypoint_index + 1)
459
482
  if self._keypoint_index >= kp_count:
@@ -463,10 +486,14 @@ class Canvas(QWidget):
463
486
  return
464
487
  elif ev.button() == Qt.RightButton and self._keypoint_index < kp_count:
465
488
  if not self.out_of_pixmap(pos):
489
+ old_kps = (list(self._keypoint_shape.keypoints)
490
+ if self._keypoint_shape.keypoints else None)
466
491
  if self._keypoint_shape.keypoints is None:
467
492
  self._keypoint_shape.keypoints = [None] * kp_count
468
493
  self._keypoint_shape.keypoints[self._keypoint_index] = (
469
494
  pos.x(), pos.y(), 1)
495
+ self._emit_keypoints_edit(
496
+ self._keypoint_shape, old_kps)
470
497
  self._keypoint_index = self._next_unplaced_keypoint(
471
498
  self._keypoint_index + 1)
472
499
  if self._keypoint_index >= kp_count:
@@ -494,7 +521,15 @@ class Canvas(QWidget):
494
521
  mid_idx = self.selected_shape.nearest_midpoint(pos, self.epsilon)
495
522
  if mid_idx is not None:
496
523
  mid = self.selected_shape.midpoint_of_edge(mid_idx)
524
+ old_points = list(self.selected_shape.points)
497
525
  self.selected_shape.insert_point(mid_idx + 1, mid)
526
+ self._emit_polygon_edit(
527
+ self.selected_shape, old_points)
528
+ # Reseat the drag baseline to the post-insert state so a
529
+ # subsequent drag-while-held emits a second edit covering
530
+ # only the move, not the insert (already emitted above).
531
+ self._polygon_drag_old_points = list(
532
+ self.selected_shape.points)
498
533
  self.h_vertex = mid_idx + 1
499
534
  self.h_shape = self.selected_shape
500
535
  self.selected_shape.highlight_vertex(mid_idx + 1, Shape.MOVE_VERTEX)
@@ -526,8 +561,28 @@ class Canvas(QWidget):
526
561
  self.current.add_point(snapped)
527
562
  self.finalise()
528
563
  self._freehand_points = []
564
+ self._polygon_drag_old_points = None
529
565
  return
530
566
 
567
+ # If a polygon was selected on press and any vertex moved during
568
+ # the drag, emit polygonVerticesEdited so MainWindow can push an
569
+ # undo command. Compares pre-press snapshot to current points.
570
+ if (ev.button() == Qt.LeftButton
571
+ and self._polygon_drag_old_points is not None
572
+ and self.selected_shape
573
+ and self.selected_shape.shape_type == ShapeType.POLYGON):
574
+ new_pts = self.selected_shape.points
575
+ old_pts = self._polygon_drag_old_points
576
+ moved = (
577
+ len(new_pts) != len(old_pts)
578
+ or any((a.x(), a.y()) != (b.x(), b.y())
579
+ for a, b in zip(old_pts, new_pts))
580
+ )
581
+ if moved:
582
+ self._emit_polygon_edit(self.selected_shape, old_pts)
583
+ if ev.button() == Qt.LeftButton:
584
+ self._polygon_drag_old_points = None
585
+
531
586
  if ev.button() == Qt.RightButton:
532
587
  # Check if right-clicking a polygon vertex
533
588
  if self.selected_shape and self.selected_shape.shape_type == ShapeType.POLYGON:
@@ -540,7 +595,10 @@ class Canvas(QWidget):
540
595
  delete_action.setEnabled(False)
541
596
  action = vertex_menu.exec_(self.mapToGlobal(ev.pos()))
542
597
  if action == delete_action:
598
+ old_points = list(self.selected_shape.points)
543
599
  self.selected_shape.remove_point(idx)
600
+ self._emit_polygon_edit(
601
+ self.selected_shape, old_points)
544
602
  self.shapeMoved.emit()
545
603
  self.update()
546
604
  return
@@ -812,6 +870,16 @@ class Canvas(QWidget):
812
870
  return True
813
871
  return False
814
872
 
873
+ def _emit_polygon_edit(self, shape, old_points):
874
+ """Emit polygonVerticesEdited with a deep-copy snapshot of old_points."""
875
+ snapshot = [QPointF(p.x(), p.y()) for p in old_points]
876
+ self.polygonVerticesEdited.emit(shape, snapshot)
877
+
878
+ def _emit_keypoints_edit(self, shape, old_keypoints):
879
+ """Emit keypointsEdited with a deep-copy snapshot."""
880
+ snapshot = list(old_keypoints) if old_keypoints else None
881
+ self.keypointsEdited.emit(shape, snapshot)
882
+
815
883
  def de_select_shape(self):
816
884
  if self.selected_shape:
817
885
  self.selected_shape.selected = False
@@ -823,6 +891,10 @@ class Canvas(QWidget):
823
891
  def delete_selected(self):
824
892
  if self.selected_shape:
825
893
  shape = self.selected_shape
894
+ # If this shape is the active keypoint subject, exit keypoint mode
895
+ # before removing — otherwise the next click crashes on a stale ref.
896
+ if self._keypoint_shape is shape:
897
+ self.exit_keypoint_mode()
826
898
  self.un_highlight(shape)
827
899
  self.shapes.remove(self.selected_shape)
828
900
  self.selected_shape = None
@@ -1240,6 +1312,15 @@ class Canvas(QWidget):
1240
1312
  self.un_highlight()
1241
1313
  self.selected_shape_copy = None
1242
1314
 
1315
+ # Clear per-file drawing/keypoint state to avoid stale references.
1316
+ self._keypoint_shape = None
1317
+ self._keypoint_index = 0
1318
+ self._keypoint_template = None
1319
+ self._freehand_active = False
1320
+ self._freehand_points = []
1321
+ self.current = None
1322
+ self.mode = self.EDIT
1323
+
1243
1324
  self.restore_cursor()
1244
1325
  self.pixmap = None
1245
1326
  self.update()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "labelimgplusplus"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  description = "labelImg++ is an enhanced graphical image annotation tool with Gallery Mode for browsing and labeling images"
9
9
  readme = "README.rst"
10
10
  license = {file = "LICENSE"}