labelimgplusplus 2.0.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
libs/colorDialog.py ADDED
@@ -0,0 +1,37 @@
1
+ try:
2
+ from PyQt5.QtGui import *
3
+ from PyQt5.QtCore import *
4
+ from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox
5
+ except ImportError:
6
+ from PyQt4.QtGui import *
7
+ from PyQt4.QtCore import *
8
+
9
+ BB = QDialogButtonBox
10
+
11
+
12
+ class ColorDialog(QColorDialog):
13
+
14
+ def __init__(self, parent=None):
15
+ super(ColorDialog, self).__init__(parent)
16
+ self.setOption(QColorDialog.ShowAlphaChannel)
17
+ # The Mac native dialog does not support our restore button.
18
+ self.setOption(QColorDialog.DontUseNativeDialog)
19
+ # Add a restore defaults button.
20
+ # The default is set at invocation time, so that it
21
+ # works across dialogs for different elements.
22
+ self.default = None
23
+ self.bb = self.layout().itemAt(1).widget()
24
+ self.bb.addButton(BB.RestoreDefaults)
25
+ self.bb.clicked.connect(self.check_restore)
26
+
27
+ def getColor(self, value=None, title=None, default=None):
28
+ self.default = default
29
+ if title:
30
+ self.setWindowTitle(title)
31
+ if value:
32
+ self.setCurrentColor(value)
33
+ return self.currentColor() if self.exec_() else None
34
+
35
+ def check_restore(self, button):
36
+ if self.bb.buttonRole(button) & BB.ResetRole and self.default:
37
+ self.setCurrentColor(self.default)
libs/combobox.py ADDED
@@ -0,0 +1,33 @@
1
+ import sys
2
+ try:
3
+ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox
4
+ except ImportError:
5
+ # needed for py3+qt4
6
+ # Ref:
7
+ # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
8
+ # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
9
+ if sys.version_info.major >= 3:
10
+ import sip
11
+ sip.setapi('QVariant', 2)
12
+ from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox
13
+
14
+
15
+ class ComboBox(QWidget):
16
+ def __init__(self, parent=None, items=[]):
17
+ super(ComboBox, self).__init__(parent)
18
+
19
+ layout = QHBoxLayout()
20
+ self.cb = QComboBox()
21
+ self.items = items
22
+ self.cb.addItems(self.items)
23
+
24
+ self.cb.currentIndexChanged.connect(parent.combo_selection_changed)
25
+
26
+ layout.addWidget(self.cb)
27
+ self.setLayout(layout)
28
+
29
+ def update_items(self, items):
30
+ self.items = items
31
+
32
+ self.cb.clear()
33
+ self.cb.addItems(self.items)
libs/commands.py ADDED
@@ -0,0 +1,328 @@
1
+ # libs/commands.py
2
+ """Command pattern implementation for undo/redo functionality.
3
+
4
+ This module provides undoable command classes for annotation actions.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from copy import deepcopy
9
+
10
+ try:
11
+ from PyQt5.QtCore import QPointF
12
+ except ImportError:
13
+ from PyQt4.QtCore import QPointF
14
+
15
+
16
+ class Command(ABC):
17
+ """Base class for all undoable commands."""
18
+
19
+ @abstractmethod
20
+ def execute(self):
21
+ """Execute the command."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def undo(self):
26
+ """Undo the command."""
27
+ pass
28
+
29
+ @property
30
+ def description(self):
31
+ """Return a description of this command."""
32
+ return "Command"
33
+
34
+
35
+ class CreateShapeCommand(Command):
36
+ """Command for creating a new shape.
37
+
38
+ Undo removes the shape from canvas and label list.
39
+ """
40
+
41
+ def __init__(self, main_window, shape):
42
+ """Initialize with reference to main window and the created shape.
43
+
44
+ Args:
45
+ main_window: The MainWindow instance.
46
+ shape: The Shape object that was created.
47
+ """
48
+ self.main_window = main_window
49
+ self.shape = shape
50
+
51
+ def execute(self):
52
+ """Add the shape to canvas and label list."""
53
+ self.main_window.canvas.shapes.append(self.shape)
54
+ self.main_window.add_label(self.shape)
55
+ self.main_window.canvas.update()
56
+
57
+ def undo(self):
58
+ """Remove the shape from canvas and label list."""
59
+ if self.shape in self.main_window.canvas.shapes:
60
+ self.main_window.canvas.shapes.remove(self.shape)
61
+ self.main_window.remove_label(self.shape)
62
+ if self.main_window.canvas.selected_shape == self.shape:
63
+ self.main_window.canvas.selected_shape = None
64
+ self.main_window.canvas.update()
65
+
66
+ @property
67
+ def description(self):
68
+ return f"Create shape '{self.shape.label}'"
69
+
70
+
71
+ class DeleteShapeCommand(Command):
72
+ """Command for deleting a shape.
73
+
74
+ Undo restores the shape to canvas and label list.
75
+ """
76
+
77
+ def __init__(self, main_window, shape, index=None):
78
+ """Initialize with reference to main window and the deleted shape.
79
+
80
+ Args:
81
+ main_window: The MainWindow instance.
82
+ shape: The Shape object that will be/was deleted.
83
+ index: Optional index where shape was in the shapes list.
84
+ """
85
+ self.main_window = main_window
86
+ self.shape = shape # Keep reference to original shape
87
+ self.index = index
88
+
89
+ def execute(self):
90
+ """Remove the shape from canvas and label list."""
91
+ if self.shape in self.main_window.canvas.shapes:
92
+ self.main_window.canvas.shapes.remove(self.shape)
93
+ self.main_window.remove_label(self.shape)
94
+ if self.main_window.canvas.selected_shape == self.shape:
95
+ self.main_window.canvas.selected_shape = None
96
+ self.main_window.canvas.update()
97
+
98
+ def undo(self):
99
+ """Restore the shape to canvas and label list."""
100
+ if self.index is not None and self.index <= len(self.main_window.canvas.shapes):
101
+ self.main_window.canvas.shapes.insert(self.index, self.shape)
102
+ else:
103
+ self.main_window.canvas.shapes.append(self.shape)
104
+ self.main_window.add_label(self.shape)
105
+ self.main_window.canvas.update()
106
+
107
+ @property
108
+ def description(self):
109
+ return f"Delete shape '{self.shape.label}'"
110
+
111
+
112
+ class MoveShapeCommand(Command):
113
+ """Command for moving a shape.
114
+
115
+ Undo restores the shape to its original position.
116
+ """
117
+
118
+ def __init__(self, main_window, shape, old_points, new_points):
119
+ """Initialize with shape and its positions.
120
+
121
+ Args:
122
+ main_window: The MainWindow instance.
123
+ shape: The Shape object being moved.
124
+ old_points: List of QPointF representing original position.
125
+ new_points: List of QPointF representing new position.
126
+ """
127
+ self.main_window = main_window
128
+ self.shape = shape
129
+ # Store copies of points to avoid reference issues
130
+ self.old_points = [QPointF(p.x(), p.y()) for p in old_points]
131
+ self.new_points = [QPointF(p.x(), p.y()) for p in new_points]
132
+
133
+ def execute(self):
134
+ """Move shape to new position."""
135
+ self.shape.points = [QPointF(p.x(), p.y()) for p in self.new_points]
136
+ self.main_window.canvas.update()
137
+
138
+ def undo(self):
139
+ """Restore shape to original position."""
140
+ self.shape.points = [QPointF(p.x(), p.y()) for p in self.old_points]
141
+ self.main_window.canvas.update()
142
+
143
+ @property
144
+ def description(self):
145
+ return f"Move shape '{self.shape.label}'"
146
+
147
+
148
+ class EditLabelCommand(Command):
149
+ """Command for editing a shape's label.
150
+
151
+ Undo restores the old label text.
152
+ """
153
+
154
+ def __init__(self, main_window, shape, old_label, new_label):
155
+ """Initialize with shape and label values.
156
+
157
+ Args:
158
+ main_window: The MainWindow instance.
159
+ shape: The Shape object being edited.
160
+ old_label: The original label text.
161
+ new_label: The new label text.
162
+ """
163
+ self.main_window = main_window
164
+ self.shape = shape
165
+ self.old_label = old_label
166
+ self.new_label = new_label
167
+
168
+ def execute(self):
169
+ """Apply new label."""
170
+ self.shape.label = self.new_label
171
+ self._update_list_item()
172
+ self.main_window.canvas.update()
173
+
174
+ def undo(self):
175
+ """Restore old label."""
176
+ self.shape.label = self.old_label
177
+ self._update_list_item()
178
+ self.main_window.canvas.update()
179
+
180
+ def _update_list_item(self):
181
+ """Update the label list item to reflect current shape label."""
182
+ if self.shape in self.main_window.shapes_to_items:
183
+ item = self.main_window.shapes_to_items[self.shape]
184
+ item.setText(self.shape.label)
185
+ from libs.hashableQListWidgetItem import generate_color_by_text
186
+ item.setBackground(generate_color_by_text(self.shape.label))
187
+
188
+ @property
189
+ def description(self):
190
+ return f"Edit label '{self.old_label}' -> '{self.new_label}'"
191
+
192
+
193
+ class UndoStack:
194
+ """Manages command history for undo/redo operations.
195
+
196
+ Maintains two stacks: one for undo operations and one for redo.
197
+ When a new command is pushed, the redo stack is cleared.
198
+ """
199
+
200
+ def __init__(self, max_size=50):
201
+ """Initialize the undo stack.
202
+
203
+ Args:
204
+ max_size: Maximum number of commands to store (default 50).
205
+ """
206
+ self._undo_stack = []
207
+ self._redo_stack = []
208
+ self._max_size = max_size
209
+ self._callbacks = []
210
+
211
+ def push(self, command):
212
+ """Add a command to the undo stack.
213
+
214
+ The command should already be executed before pushing.
215
+ Clears the redo stack since we're branching history.
216
+
217
+ Args:
218
+ command: The Command object to push.
219
+ """
220
+ self._undo_stack.append(command)
221
+ self._redo_stack.clear()
222
+
223
+ # Trim stack if it exceeds max size
224
+ while len(self._undo_stack) > self._max_size:
225
+ self._undo_stack.pop(0)
226
+
227
+ self._notify_callbacks()
228
+
229
+ def undo(self):
230
+ """Undo the last command.
231
+
232
+ Returns:
233
+ The undone command, or None if stack was empty.
234
+ """
235
+ if not self.can_undo():
236
+ return None
237
+
238
+ command = self._undo_stack.pop()
239
+ command.undo()
240
+ self._redo_stack.append(command)
241
+ self._notify_callbacks()
242
+ return command
243
+
244
+ def redo(self):
245
+ """Redo the last undone command.
246
+
247
+ Returns:
248
+ The redone command, or None if redo stack was empty.
249
+ """
250
+ if not self.can_redo():
251
+ return None
252
+
253
+ command = self._redo_stack.pop()
254
+ command.execute()
255
+ self._undo_stack.append(command)
256
+ self._notify_callbacks()
257
+ return command
258
+
259
+ def can_undo(self):
260
+ """Check if undo is available.
261
+
262
+ Returns:
263
+ True if there are commands to undo.
264
+ """
265
+ return len(self._undo_stack) > 0
266
+
267
+ def can_redo(self):
268
+ """Check if redo is available.
269
+
270
+ Returns:
271
+ True if there are commands to redo.
272
+ """
273
+ return len(self._redo_stack) > 0
274
+
275
+ def clear(self):
276
+ """Clear both undo and redo stacks.
277
+
278
+ Call this when loading a new file.
279
+ """
280
+ self._undo_stack.clear()
281
+ self._redo_stack.clear()
282
+ self._notify_callbacks()
283
+
284
+ def add_callback(self, callback):
285
+ """Register a callback to be notified when stack changes.
286
+
287
+ Args:
288
+ callback: A callable that takes no arguments.
289
+ """
290
+ self._callbacks.append(callback)
291
+
292
+ def remove_callback(self, callback):
293
+ """Remove a previously registered callback.
294
+
295
+ Args:
296
+ callback: The callback to remove.
297
+ """
298
+ if callback in self._callbacks:
299
+ self._callbacks.remove(callback)
300
+
301
+ def _notify_callbacks(self):
302
+ """Notify all registered callbacks of stack change."""
303
+ for callback in self._callbacks:
304
+ callback()
305
+
306
+ def get_undo_description(self):
307
+ """Get description of the command that would be undone.
308
+
309
+ Returns:
310
+ Description string or None if stack is empty.
311
+ """
312
+ if self.can_undo():
313
+ return self._undo_stack[-1].description
314
+ return None
315
+
316
+ def get_redo_description(self):
317
+ """Get description of the command that would be redone.
318
+
319
+ Returns:
320
+ Description string or None if redo stack is empty.
321
+ """
322
+ if self.can_redo():
323
+ return self._redo_stack[-1].description
324
+ return None
325
+
326
+ def __len__(self):
327
+ """Return number of commands in undo stack."""
328
+ return len(self._undo_stack)
libs/constants.py ADDED
@@ -0,0 +1,26 @@
1
+ SETTING_FILENAME = 'filename'
2
+ SETTING_RECENT_FILES = 'recentFiles'
3
+ SETTING_WIN_SIZE = 'window/size'
4
+ SETTING_WIN_POSE = 'window/position'
5
+ SETTING_WIN_GEOMETRY = 'window/geometry'
6
+ SETTING_LINE_COLOR = 'line/color'
7
+ SETTING_FILL_COLOR = 'fill/color'
8
+ SETTING_ADVANCE_MODE = 'advanced'
9
+ SETTING_WIN_STATE = 'window/state'
10
+ SETTING_SAVE_DIR = 'savedir'
11
+ SETTING_PAINT_LABEL = 'paintlabel'
12
+ SETTING_LAST_OPEN_DIR = 'lastOpenDir'
13
+ SETTING_AUTO_SAVE = 'autosave'
14
+ SETTING_AUTO_SAVE_ENABLED = 'autoSaveEnabled' # Timer-based auto-save toggle
15
+ SETTING_AUTO_SAVE_INTERVAL = 'autoSaveInterval' # Interval in seconds
16
+ SETTING_SINGLE_CLASS = 'singleclass'
17
+ FORMAT_PASCALVOC='PascalVOC'
18
+ FORMAT_YOLO='YOLO'
19
+ FORMAT_CREATEML='CreateML'
20
+ SETTING_DRAW_SQUARE = 'draw/square'
21
+ SETTING_LABEL_FILE_FORMAT= 'labelFileFormat'
22
+ SETTING_FILE_VIEW_MODE = 'fileViewMode'
23
+ SETTING_GALLERY_MODE = 'galleryMode'
24
+ SETTING_ICON_SIZE = 'iconSize'
25
+ SETTING_TOOLBAR_EXPANDED = 'toolbarExpanded'
26
+ DEFAULT_ENCODING = 'utf-8'
libs/create_ml_io.py ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf8 -*-
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from libs.constants import DEFAULT_ENCODING
7
+ import os
8
+
9
+ JSON_EXT = '.json'
10
+ ENCODE_METHOD = DEFAULT_ENCODING
11
+
12
+
13
+ class CreateMLWriter:
14
+ def __init__(self, folder_name, filename, img_size, shapes, output_file, database_src='Unknown', local_img_path=None):
15
+ self.folder_name = folder_name
16
+ self.filename = filename
17
+ self.database_src = database_src
18
+ self.img_size = img_size
19
+ self.box_list = []
20
+ self.local_img_path = local_img_path
21
+ self.verified = False
22
+ self.shapes = shapes
23
+ self.output_file = output_file
24
+
25
+ def write(self):
26
+ if os.path.isfile(self.output_file):
27
+ with open(self.output_file, "r") as file:
28
+ input_data = file.read()
29
+ output_dict = json.loads(input_data)
30
+ else:
31
+ output_dict = []
32
+
33
+ output_image_dict = {
34
+ "image": self.filename,
35
+ "verified": self.verified,
36
+ "annotations": []
37
+ }
38
+
39
+ for shape in self.shapes:
40
+ points = shape["points"]
41
+
42
+ x1 = points[0][0]
43
+ y1 = points[0][1]
44
+ x2 = points[1][0]
45
+ y2 = points[2][1]
46
+
47
+ height, width, x, y = self.calculate_coordinates(x1, x2, y1, y2)
48
+
49
+ shape_dict = {
50
+ "label": shape["label"],
51
+ "coordinates": {
52
+ "x": x,
53
+ "y": y,
54
+ "width": width,
55
+ "height": height
56
+ }
57
+ }
58
+ output_image_dict["annotations"].append(shape_dict)
59
+
60
+ # check if image already in output
61
+ exists = False
62
+ for i in range(0, len(output_dict)):
63
+ if output_dict[i]["image"] == output_image_dict["image"]:
64
+ exists = True
65
+ output_dict[i] = output_image_dict
66
+ break
67
+
68
+ if not exists:
69
+ output_dict.append(output_image_dict)
70
+
71
+ Path(self.output_file).write_text(json.dumps(output_dict), ENCODE_METHOD)
72
+
73
+ def calculate_coordinates(self, x1, x2, y1, y2):
74
+ if x1 < x2:
75
+ x_min = x1
76
+ x_max = x2
77
+ else:
78
+ x_min = x2
79
+ x_max = x1
80
+ if y1 < y2:
81
+ y_min = y1
82
+ y_max = y2
83
+ else:
84
+ y_min = y2
85
+ y_max = y1
86
+ width = x_max - x_min
87
+ if width < 0:
88
+ width = width * -1
89
+ height = y_max - y_min
90
+ # x and y from center of rect
91
+ x = x_min + width / 2
92
+ y = y_min + height / 2
93
+ return height, width, x, y
94
+
95
+
96
+ class CreateMLReader:
97
+ def __init__(self, json_path, file_path):
98
+ self.json_path = json_path
99
+ self.shapes = []
100
+ self.verified = False
101
+ self.filename = os.path.basename(file_path)
102
+ try:
103
+ self.parse_json()
104
+ except ValueError:
105
+ print("JSON decoding failed")
106
+
107
+ def parse_json(self):
108
+ with open(self.json_path, "r") as file:
109
+ input_data = file.read()
110
+
111
+ # Returns a list
112
+ output_list = json.loads(input_data)
113
+
114
+ if output_list:
115
+ self.verified = output_list[0].get("verified", False)
116
+
117
+ if len(self.shapes) > 0:
118
+ self.shapes = []
119
+ for image in output_list:
120
+ if image["image"] == self.filename:
121
+ for shape in image["annotations"]:
122
+ self.add_shape(shape["label"], shape["coordinates"])
123
+
124
+ def add_shape(self, label, bnd_box):
125
+ x_min = bnd_box["x"] - (bnd_box["width"] / 2)
126
+ y_min = bnd_box["y"] - (bnd_box["height"] / 2)
127
+
128
+ x_max = bnd_box["x"] + (bnd_box["width"] / 2)
129
+ y_max = bnd_box["y"] + (bnd_box["height"] / 2)
130
+
131
+ points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
132
+ self.shapes.append((label, points, None, None, True))
133
+
134
+ def get_shapes(self):
135
+ return self.shapes
@@ -0,0 +1,27 @@
1
+ import sys
2
+ try:
3
+ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox
4
+ except ImportError:
5
+ # needed for py3+qt4
6
+ # Ref:
7
+ # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
8
+ # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
9
+ if sys.version_info.major >= 3:
10
+ import sip
11
+ sip.setapi('QVariant', 2)
12
+ from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox
13
+
14
+
15
+ class DefaultLabelComboBox(QWidget):
16
+ def __init__(self, parent=None, items=[]):
17
+ super(DefaultLabelComboBox, self).__init__(parent)
18
+
19
+ layout = QHBoxLayout()
20
+ self.cb = QComboBox()
21
+ self.items = items
22
+ self.cb.addItems(self.items)
23
+
24
+ self.cb.currentIndexChanged.connect(parent.default_label_combo_selection_changed)
25
+
26
+ layout.addWidget(self.cb)
27
+ self.setLayout(layout)