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.
- labelimgplusplus-2.0.0a0.dist-info/LICENSE +9 -0
- labelimgplusplus-2.0.0a0.dist-info/METADATA +282 -0
- labelimgplusplus-2.0.0a0.dist-info/RECORD +30 -0
- labelimgplusplus-2.0.0a0.dist-info/WHEEL +5 -0
- labelimgplusplus-2.0.0a0.dist-info/entry_points.txt +2 -0
- labelimgplusplus-2.0.0a0.dist-info/top_level.txt +1 -0
- libs/__init__.py +2 -0
- libs/canvas.py +748 -0
- libs/colorDialog.py +37 -0
- libs/combobox.py +33 -0
- libs/commands.py +328 -0
- libs/constants.py +26 -0
- libs/create_ml_io.py +135 -0
- libs/default_label_combobox.py +27 -0
- libs/galleryWidget.py +568 -0
- libs/hashableQListWidgetItem.py +28 -0
- libs/labelDialog.py +95 -0
- libs/labelFile.py +174 -0
- libs/lightWidget.py +33 -0
- libs/pascal_voc_io.py +171 -0
- libs/resources.py +4212 -0
- libs/settings.py +45 -0
- libs/shape.py +209 -0
- libs/stringBundle.py +78 -0
- libs/styles.py +82 -0
- libs/toolBar.py +275 -0
- libs/ustr.py +17 -0
- libs/utils.py +119 -0
- libs/yolo_io.py +143 -0
- libs/zoomWidget.py +26 -0
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)
|