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.
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/HISTORY.rst +40 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/PKG-INFO +1 -1
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelImgPlusPlus.py +44 -14
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/PKG-INFO +1 -1
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/__init__.py +2 -2
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/commands.py +76 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/create_ml_io.py +3 -4
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/yolo_io.py +21 -3
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/yolo_seg_io.py +4 -2
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/resources.py +43 -43
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/label_checker.py +0 -2
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/canvas.py +81 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/pyproject.toml +1 -1
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/CONTRIBUTING.rst +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/LICENSE +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/MANIFEST.in +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/README.rst +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/data/predefined_classes.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/SOURCES.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/dependency_links.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/entry_points.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/requires.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/top_level.txt +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/__init__.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/keypoint_config.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/settings.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/shape.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/core/shortcut_config.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/__init__.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/coco_io.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/labelFile.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/formats/pascal_voc_io.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/__init__.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/tools/dataset_splitter.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/__init__.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/constants.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/hashableQListWidgetItem.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/stringBundle.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/styles.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/ustr.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/utils/utils.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/__init__.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/batchVerifyDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/colorDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/combobox.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/default_label_combobox.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/galleryWidget.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/keypointPanel.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/labelCheckerDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/labelDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/lightWidget.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/shortcutsDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/splitDialog.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/statsWidget.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/toolBar.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/libs/widgets/zoomWidget.py +0 -0
- {labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/resources.qrc +0 -0
- {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.
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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', '
|
|
5
|
-
__version__ = '2.
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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=
|
|
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
|
-
|
|
57
|
-
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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.
|
|
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"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{labelimgplusplus-2.3.0 → labelimgplusplus-2.3.2}/labelimgplusplus.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|