-
Notifications
You must be signed in to change notification settings - Fork 2
/
InstanceLabelTool.py
455 lines (380 loc) · 17.7 KB
/
InstanceLabelTool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
"""
Copyright (c) 2018- Guoxia Wang
mingzilaochongtu at gmail com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The Software is provided "as is", without warranty of any kind.
"""
from PyQt4 import QtGui, QtCore
import sys
import os
import json
from lib.waitindicator import WaitOverlay
from lib.annotation import AnnObjectType
from lib.canvas import Canvas
from lib.worker import BatchConvertToBoundariesWorker
class InstanceLabelTool(QtGui.QMainWindow):
def __init__(self):
super(InstanceLabelTool, self).__init__()
# Filenames of all iamges
self.imageList = None
# Image directory
self.imageDir = None
# Current image id
self.idx = 0
# Ground truth extension after labeling occlusion orientation
self.gtExt = '.polygons.json'
# Current image as QImage
self.image = QtGui.QImage()
self.initUI()
def initUI(self):
self.canvas = Canvas(parent=self)
self.canvas.scrollRequest.connect(self.scrollRequest)
scroll = QtGui.QScrollArea()
scroll.setWidget(self.canvas)
scroll.setWidgetResizable(True)
scroll.verticalScrollBar().setSingleStep(1)
scroll.horizontalScrollBar().setSingleStep(1)
self.scrollBars = {
QtCore.Qt.Vertical: scroll.verticalScrollBar(),
QtCore.Qt.Horizontal: scroll.horizontalScrollBar()
}
self.scrollArea = scroll
self.setCentralWidget(scroll)
self.canvas.showMessage.connect(self.statusBarShowMessage)
self.canvas.busyWaiting.connect(self.showWaitOverlay)
# Menu setting
self.menuBar().setNativeMenuBar(False)
# Add File menu
self.fileMenuBar = self.menuBar().addMenu('&File')
openAction = QtGui.QAction('&Open', self)
openAction.triggered.connect(self.loadImageJsonList)
self.fileMenuBar.addAction(openAction)
# Add quit action to File menu
exitAction = QtGui.QAction('&Quit', self)
exitAction.triggered.connect(QtGui.qApp.quit)
self.fileMenuBar.addAction(exitAction)
# Add Tools menu
self.toolsMenuBar = self.menuBar().addMenu('&Tools')
convertToBoundariesAction = QtGui.QAction('&Batch convert to occlusion boundaries', self)
convertToBoundariesAction.triggered.connect(self.batchConvertToOcclusionBoundaries)
self.toolsMenuBar.addAction(convertToBoundariesAction)
# Create a toolbar
self.toolbar = self.addToolBar('Tools')
# Add the tool buttons
iconDir = os.path.join(os.path.dirname(__file__), 'icons')
# Load image and label list, then show the first image and label
loadAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'open.png')), '&Tools', self)
loadAction.setShortcuts(['o'])
self.setTip(loadAction, 'Open json list')
loadAction.triggered.connect(self.loadImageJsonList)
self.toolbar.addAction(loadAction)
# Save the labels to json file
saveChangesAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'save.png')), '&Tools', self)
saveChangesAction.setShortcuts([QtGui.QKeySequence.Save])
self.setTip(saveChangesAction, 'Save changes')
saveChangesAction.triggered.connect(self.saveLabels)
self.toolbar.addAction(saveChangesAction)
saveChangesAction.setEnabled(False)
self.canvas.actChanges.append(saveChangesAction)
self.toolbar.addSeparator()
# Load next image
self.prevAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'prev.png')), '&Tools', self)
self.prevAction.setShortcuts([QtGui.QKeySequence.MoveToPreviousChar])
self.setTip(self.prevAction, 'Previous image')
self.prevAction.triggered.connect(self.prevImage)
self.toolbar.addAction(self.prevAction)
self.prevAction.setEnabled(False)
# Add QLabel to show current image id of all image
self.numLabel = QtGui.QLabel()
self.numLabel.setAlignment(QtCore.Qt.AlignCenter)
self.toolbar.addWidget(self.numLabel)
# Load next image
self.nextAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'next.png')), '&Tools', self)
self.nextAction.setShortcuts([QtGui.QKeySequence.MoveToNextChar])
self.setTip(self.nextAction, 'Next image')
self.nextAction.triggered.connect(self.nextImage)
self.toolbar.addAction(self.nextAction)
self.nextAction.setEnabled(False)
self.toolbar.addSeparator()
# Create new object from drawn polygon
newObjAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'newobject.png')), '&Tools', self)
newObjAction.setShortcuts(['e'])
self.setTip(newObjAction, 'New object')
newObjAction.triggered.connect(self.canvas.newObject)
self.toolbar.addAction(newObjAction)
newObjAction.setEnabled(False)
self.canvas.actClosedPoly.append(newObjAction)
# Delete the selected objects
deleteObjAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'deleteobject.png')), '&Tools', self)
deleteObjAction.setShortcuts(['c'])
self.setTip(deleteObjAction, 'Delete object')
deleteObjAction.triggered.connect(self.canvas.deleteObject)
self.toolbar.addAction(deleteObjAction)
deleteObjAction.setEnabled(False)
self.canvas.actSelObj.append(deleteObjAction)
# Layer up the selected object
layerupObjAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'layerup.png')), '&Tools', self)
layerupObjAction.setShortcuts([QtGui.QKeySequence.MoveToPreviousLine])
self.setTip(layerupObjAction, 'Layer up')
layerupObjAction.triggered.connect(self.canvas.layerUp)
self.toolbar.addAction(layerupObjAction)
# Layer down the selected object
layerdownObjAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'layerdown.png')), '&Tools', self)
layerdownObjAction.setShortcuts([QtGui.QKeySequence.MoveToNextLine])
self.setTip(layerdownObjAction, 'Layer down')
layerdownObjAction.triggered.connect(self.canvas.layerDown)
self.toolbar.addAction(layerdownObjAction)
# Modify the selected object label name
modifyLabelAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'modify.png')), '&Tools', self)
self.setTip(modifyLabelAction, 'Modify label name')
modifyLabelAction.triggered.connect(self.canvas.modifyLabel)
self.toolbar.addAction(modifyLabelAction)
self.toolbar.addSeparator()
# Zoom out
zoomOutAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'zoomout.png')), '&Tools', self)
self.setTip(zoomOutAction, 'Mouse wheel to scroll down')
zoomOutAction.triggered.connect(self.canvas.zoomOut)
self.toolbar.addAction(zoomOutAction)
# Zoom in
zoomInAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'zoomin.png')), '&Tools', self)
self.setTip(zoomInAction, 'Mouse wheel to scroll up')
zoomInAction.triggered.connect(self.canvas.zoomIn)
self.toolbar.addAction(zoomInAction)
# Decrease transparency
minusAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'minus.png')), '&Tools', self)
minusAction.setShortcuts(['-'])
self.setTip(minusAction, 'Decrease transparency')
minusAction.triggered.connect(self.canvas.minus)
self.toolbar.addAction(minusAction)
# Plus transparency
plusAction = QtGui.QAction(QtGui.QIcon(os.path.join(iconDir, 'plus.png')), '&Tools', self)
plusAction.setShortcuts(['+'])
self.setTip(plusAction, 'Increase transparency')
plusAction.triggered.connect(self.canvas.plus)
self.toolbar.addAction(plusAction)
self.toolbar.addSeparator()
# Draw type sets
self.drawTypeSetComboBox = QtGui.QComboBox()
self.loadDrawTypeSet()
self.drawTypeSetComboBox.currentIndexChanged.connect(self.drawTypeChange)
self.toolbar.addWidget(self.drawTypeSetComboBox)
# Label name sets
self.labelSetComboBox = QtGui.QComboBox()
self.loadLabelCategoriesFromFile()
self.labelSetComboBox.currentIndexChanged.connect(self.labelChange)
self.toolbar.addWidget(self.labelSetComboBox)
# Set a wait overlay
self.waitOverlay = WaitOverlay(self)
self.waitOverlay.hide()
# The default text for the status bar
self.defaultStatusbar = 'Ready'
# Create a statusbar and init with default
self.statusBar().showMessage(self.defaultStatusbar)
# Enable mouse move events
self.setMouseTracking(True)
self.toolbar.setMouseTracking(True)
# Open in full screen
screenShape = QtGui.QDesktopWidget().screenGeometry()
self.resize(screenShape.width(), screenShape.height())
# Set the title
self.applicationTitle = 'Instance Label Tool v1.0'
self.setWindowTitle(self.applicationTitle)
# Show the application
self.show()
def setTip(self, action, tip):
shortcuts = "', '".join([str(s.toString()) for s in action.shortcuts()])
if (not shortcuts):
shortcuts = 'none'
tip += " (Hotkeys: '" + shortcuts + "')"
action.setStatusTip(tip)
action.setToolTip(tip)
# show message through statusbar
def statusBarShowMessage(self, message):
self.statusBar().showMessage(message)
def resizeEvent(self, event):
self.waitOverlay.resize(event.size())
event.accept()
@QtCore.pyqtSlot(bool)
def showWaitOverlay(self, show=True):
if (show):
self.waitOverlay.show()
else:
self.waitOverlay.hide()
# Load image json list
def loadImageJsonList(self):
fname = QtGui.QFileDialog.getOpenFileName(self, 'Open image list .json file', '.', 'Json file (*.json)')
fname = str(fname)
if (os.path.isfile(fname)):
self.imageDir = os.path.split(fname)[0]
with open(fname, 'r') as f:
jsonText = f.read()
self.imageList = json.loads(jsonText)
if (not isinstance(self.imageList, list)):
self.statusBarShowMessage("Invalid image list, please check json format")
return
if (self.imageList):
self.idx = 0
self.updatePrevNextToolbarStatus()
self.loadImage()
self.update()
# Load the currently selected image
def loadImage(self):
success = False
message = self.defaultStatusbar
if self.imageList:
filename = self.imageList[self.idx]
filename = os.path.join(self.imageDir, filename)
self.numLabel.setText('{0}/{1}'.format(self.idx+1, len(self.imageList)))
success = self.canvas.loadImage(filename)
if (not success):
message = "failed to read image: {0}".format(filename)
else:
message = filename
self.loadLabels()
self.canvas.update()
else:
self.numLabel.setText('')
self.statusBarShowMessage(message)
# Get the filename where to load/save labels
# Returns empty string if not possible
def getLabelFilename(self):
filename = ""
if (self.imageList):
filename = self.imageList[self.idx]
filename = os.path.join(self.imageDir, filename)
imageExt = os.path.splitext(filename)[1]
filename = filename.replace(imageExt, self.gtExt)
filename = os.path.normpath(filename)
return filename
# Load the labels from json file
def loadLabels(self):
filename = self.getLabelFilename()
if (not filename or not os.path.isfile(filename)):
self.canvas.clearAnnotation()
return
self.canvas.loadLabels(filename)
# Save the labels to json file
def saveLabels(self):
filename = self.getLabelFilename()
if (filename):
self.canvas.saveLabels(filename)
# Scroll canvas
@QtCore.pyqtSlot(int, int)
def scrollRequest(self, offsetX, offsetY):
hBar = self.scrollBars[QtCore.Qt.Horizontal]
hBar.setValue(hBar.value() - offsetX)
vBar = self.scrollBars[QtCore.Qt.Vertical]
vBar.setValue(vBar.value() - offsetY)
# load previous Image
@QtCore.pyqtSlot()
def prevImage(self):
self.saveLabels()
self.idx = max(self.idx - 1, 0)
self.updatePrevNextToolbarStatus()
self.loadImage()
# Load next Image
@QtCore.pyqtSlot()
def nextImage(self):
self.saveLabels()
self.idx = min(self.idx + 1, len(self.imageList)-1)
self.updatePrevNextToolbarStatus()
self.loadImage()
# Initialize prev and next toolbar status
def updatePrevNextToolbarStatus(self):
if (len(self.imageList) > 0 and self.idx < len(self.imageList) - 1):
self.nextAction.setEnabled(True)
else:
self.nextAction.setEnabled(False)
if (self.idx <= 0):
self.prevAction.setEnabled(False)
else:
self.prevAction.setEnabled(True)
# Load catogories label from config file
def loadLabelCategoriesFromFile(self):
filename = os.path.join(os.path.dirname(__file__), 'config.json')
try:
with open(filename, 'r') as f:
jsonText = f.read()
jsonDict = json.loads(jsonText)
categories = [c['name'] for c in jsonDict['categories']]
self.labelSetComboBox.addItems(categories)
self.canvas.setCurrentLabelName(categories[0])
except StandardError as e:
msgBox = QtGui.QMessageBox(self)
msgBox.setWindowTitle("Error")
msgBox.setText("Plase check config.json file!")
msgBox.setIcon(QtGui.QMessageBox.Critical)
msgBox.setStandardButtons(QtGui.QMessageBox.Abort)
msgBox.exec_()
sys.exit()
# label selection changed, we update the current label name
def labelChange(self, index):
labelName = self.labelSetComboBox.currentText()
self.canvas.setCurrentLabelName(str(labelName))
# Load draw type set
def loadDrawTypeSet(self):
# See DrawType for more information
drawTypeName = ['instance', 'boundary']
self.drawTypeSetComboBox.addItems(drawTypeName)
self.canvas.setCurrentDrawType(AnnObjectType.INSTANCE)
# Draw type changed, we update the current draw type
def drawTypeChange(self, index):
self.canvas.setCurrentDrawType(index)
# Batch operation, convert to occlusion boundary
# from instance labels automatically
def batchConvertToOcclusionBoundaries(self):
dlgTitle = "Batch convert to occlusion boundary"
# Check if load image list
if (not self.imageList):
text = "Need load image list firstly."
buttons = QtGui.QMessageBox.Yes
ret = QtGui.QMessageBox.information(self, dlgTitle, text, buttons, QtGui.QMessageBox.Yes)
return
self.progressDialog = QtGui.QProgressDialog("Converting ...", "Cancel", 0, len(self.imageList), self)
self.progressDialog.setWindowTitle(dlgTitle)
self.progressDialog.resize(350, self.progressDialog.height())
self.progressDialog.setWindowModality(QtCore.Qt.WindowModal)
self.progressDialog.canceled.connect(self.batchConvertStop)
self.batchConvertThread = QtCore.QThread()
self.batchConvertWorker = BatchConvertToBoundariesWorker(self.imageList, self.imageDir, self.gtExt)
self.batchConvertWorker.information.connect(self.dealwithBatchConvertUserOperation)
self.batchConvertWorker.updateProgress.connect(self.updateBatchConvertProgressDialog)
self.batchConvertWorker.finished.connect(self.batchConvertStop)
self.batchConvertWorker.moveToThread(self.batchConvertThread)
self.batchConvertThread.started.connect(self.batchConvertWorker.batchConvertToBoundaries)
self.batchConvertThread.start()
self.progressDialog.exec_()
@QtCore.pyqtSlot(int, str)
def updateBatchConvertProgressDialog(self, value, labelText):
self.progressDialog.setValue(value)
self.progressDialog.setLabelText(labelText)
@QtCore.pyqtSlot()
def batchConvertStop(self):
self.batchConvertWorker.stop()
self.batchConvertThread.quit()
self.batchConvertThread.wait()
self.progressDialog.close()
@QtCore.pyqtSlot(str, str)
def dealwithBatchConvertUserOperation(self, infoType, message):
dlgTitle = "Batch convert to occlusion boundary"
buttons = None
defaultButtons = None
if (infoType == "IOError"):
buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
defaultButtons = QtGui.QMessageBox.Yes
elif (infoType == "Overwrite"):
buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.YesToAll | QtGui.QMessageBox.No
defaultButtons = QtGui.QMessageBox.Yes
self.batchConvertWorker.userOperationResult = QtGui.QMessageBox.information(
self, dlgTitle, message, buttons, defaultButtons)
self.batchConvertWorker.waitCondition.wakeAll()
def main():
app = QtGui.QApplication(sys.argv)
tool = InstanceLabelTool()
sys.exit(app.exec_())
if __name__ == '__main__':
main()