|
@@ -32,9 +32,8 @@ __appname__ = 'labelme'
|
|
|
# alternate files. Either keep enabled, or add "Save As" button.
|
|
|
|
|
|
# TODO:
|
|
|
-# - [high] Draw mode is confusing in current implementation.
|
|
|
# - [high] Deselect shape when clicking and already selected(?)
|
|
|
-# - [high] More sensible shortcuts (e.g. Ctrl+C to copy).
|
|
|
+# - [high] Sanitize shortcuts between beginner/advanced mode.
|
|
|
# - [high] Figure out WhatsThis for help.
|
|
|
# - [medium] Zoom should keep the image centered.
|
|
|
# - [medium] Add undo button for vertex addition.
|
|
@@ -42,8 +41,6 @@ __appname__ = 'labelme'
|
|
|
# - [low,maybe] Open images with drag & drop.
|
|
|
# - [low,maybe] Preview images on file dialogs.
|
|
|
# - [low,maybe] Sortable label list.
|
|
|
-# - [extra] Add beginner/advanced mode, where different settings are set for
|
|
|
-# the application, e.g. closable labels, different toolbuttons etc.
|
|
|
# - Zoom is too "steppy".
|
|
|
|
|
|
|
|
@@ -78,6 +75,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.dirty = False
|
|
|
|
|
|
self._noSelectionSlot = False
|
|
|
+ self._beginner = True
|
|
|
|
|
|
# Main widgets and related state.
|
|
|
self.labelDialog = LabelDialog(parent=self)
|
|
@@ -92,9 +90,20 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
# Connect to itemChanged to detect checkbox changes.
|
|
|
self.labelList.itemChanged.connect(self.labelItemChanged)
|
|
|
|
|
|
+ listLayout = QVBoxLayout()
|
|
|
+ listLayout.setContentsMargins(0, 0, 0, 0)
|
|
|
+ listLayout.addWidget(self.labelList)
|
|
|
+ self.editButton = QToolButton()
|
|
|
+ self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
|
+ self.labelListContainer = QWidget()
|
|
|
+ self.labelListContainer.setLayout(listLayout)
|
|
|
+ listLayout.addWidget(self.editButton)#, 0, Qt.AlignCenter)
|
|
|
+ listLayout.addWidget(self.labelList)
|
|
|
+
|
|
|
+
|
|
|
self.dock = QDockWidget(u'Polygon Labels', self)
|
|
|
self.dock.setObjectName(u'Labels')
|
|
|
- self.dock.setWidget(self.labelList)
|
|
|
+ self.dock.setWidget(self.labelListContainer)
|
|
|
|
|
|
self.zoomWidget = ZoomWidget()
|
|
|
self.colorDialog = ColorDialog(parent=self)
|
|
@@ -119,6 +128,9 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
|
|
|
self.setCentralWidget(scroll)
|
|
|
self.addDockWidget(Qt.RightDockWidgetArea, self.dock)
|
|
|
+ self.dockFeatures = QDockWidget.DockWidgetClosable\
|
|
|
+ | QDockWidget.DockWidgetFloatable
|
|
|
+ self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)
|
|
|
|
|
|
# Actions
|
|
|
action = partial(newAction, self)
|
|
@@ -134,18 +146,31 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
'Ctrl+L', 'color', u'Choose polygon line color')
|
|
|
color2 = action('Polygon &Fill Color', self.chooseColor2,
|
|
|
'Ctrl+Shift+F', 'color', u'Choose polygon fill color')
|
|
|
- mode = action('Draw &Polygon', self.toggleDrawing,
|
|
|
- 'Ctrl+N', 'new', u'Start drawing polygons',
|
|
|
- checkable=True, enabled=False)
|
|
|
+
|
|
|
+ createMode = action('Create\n&Polygons', self.setCreateMode,
|
|
|
+ 'Ctrl+N', 'new', u'Start drawing polygons', enabled=False)
|
|
|
+ editMode = action('&Edit\nPolygons', self.setEditMode,
|
|
|
+ 'Ctrl+E', 'edit', u'Move and edit polygons', enabled=False)
|
|
|
+
|
|
|
+ create = action('Create &Polygon', self.createShape,
|
|
|
+ 'Ctrl+N', 'new', u'Draw a new polygon')
|
|
|
+ delete = action('&Delete Polygon', self.deleteSelectedShape,
|
|
|
+ ['Ctrl+D', 'Delete'], 'delete', u'Delete', enabled=False)
|
|
|
copy = action('&Copy Polygon', self.copySelectedShape,
|
|
|
'Ctrl+C', 'copy', u'Create a duplicate of the selected polygon',
|
|
|
enabled=False)
|
|
|
- delete = action('&Delete Polygon', self.deleteSelectedShape,
|
|
|
- ['Ctrl+D', 'Delete'], 'delete', u'Delete', enabled=False)
|
|
|
- hide = action('&Hide Polygons', self.hideLabelsToggle,
|
|
|
- 'Ctrl+H', 'hide', u'Hide all polygons',
|
|
|
+
|
|
|
+ advancedMode = action('&Advanced Mode', self.toggleAdvancedMode,
|
|
|
+ 'Ctrl+Shift+A', 'expert', u'Switch to advanced mode',
|
|
|
checkable=True)
|
|
|
|
|
|
+ hideAll = action('&Hide Polygons', partial(self.togglePolygons, False),
|
|
|
+ 'Ctrl+H', 'hide', u'Hide all polygons',
|
|
|
+ enabled=False)
|
|
|
+ showAll = action('&Show Polygons', partial(self.togglePolygons, True),
|
|
|
+ 'Ctrl+A', 'hide', u'Show all polygons',
|
|
|
+ enabled=False)
|
|
|
+
|
|
|
zoom = QWidgetAction(self)
|
|
|
zoom.setDefaultWidget(self.zoomWidget)
|
|
|
self.zoomWidget.setWhatsThis(
|
|
@@ -179,24 +204,15 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
edit = action('&Edit Label', self.editLabel,
|
|
|
'Ctrl+E', 'edit', u'Modify the label of the selected polygon',
|
|
|
enabled=False)
|
|
|
+ self.editButton.setDefaultAction(edit)
|
|
|
|
|
|
- shapeLineColor = action('Current &Line Color', self.chshapeLineColor,
|
|
|
+ shapeLineColor = action('Shape &Line Color', self.chshapeLineColor,
|
|
|
icon='color', tip=u'Change the line color for this specific shape',
|
|
|
enabled=False)
|
|
|
- shapeFillColor = action('Current &Fill Color', self.chshapeFillColor,
|
|
|
+ shapeFillColor = action('Shape &Fill Color', self.chshapeFillColor,
|
|
|
icon='color', tip=u'Change the fill color for this specific shape',
|
|
|
enabled=False)
|
|
|
|
|
|
- # Custom context menu for the canvas widget:
|
|
|
- addActions(self.canvas.menus[0], (mode, edit, copy, delete))
|
|
|
-
|
|
|
- addActions(self.canvas.menus[0], (
|
|
|
- mode, edit, copy, delete,
|
|
|
- shapeLineColor, shapeFillColor))
|
|
|
- addActions(self.canvas.menus[1], (
|
|
|
- action('&Copy here', self.copyShape),
|
|
|
- action('&Move here', self.moveShape)))
|
|
|
-
|
|
|
labels = self.dock.toggleViewAction()
|
|
|
labels.setText('Show/Hide Label Panel')
|
|
|
labels.setShortcut('Ctrl+Shift+L')
|
|
@@ -212,30 +228,48 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
# Store actions for further handling.
|
|
|
self.actions = struct(save=save, open=open, close=close,
|
|
|
lineColor=color1, fillColor=color2,
|
|
|
- mode=mode, delete=delete, edit=edit, copy=copy,
|
|
|
+ create=create, delete=delete, edit=edit, copy=copy,
|
|
|
+ createMode=createMode, editMode=editMode, advancedMode=advancedMode,
|
|
|
shapeLineColor=shapeLineColor, shapeFillColor=shapeFillColor,
|
|
|
zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg,
|
|
|
fitWindow=fitWindow, fitWidth=fitWidth,
|
|
|
zoomActions=zoomActions,
|
|
|
- fileMenuActions=(open,save,close,quit))
|
|
|
+ fileMenuActions=(open,save,close,quit),
|
|
|
+ beginner=(), advanced=(),
|
|
|
+ beginnerContext=(create, edit, copy, delete),
|
|
|
+ advancedContext=(createMode, editMode, edit, copy,
|
|
|
+ delete, shapeLineColor, shapeFillColor),
|
|
|
+ onLoadActive=(close, create, createMode, editMode),
|
|
|
+ onShapesPresent=(hideAll, showAll))
|
|
|
|
|
|
self.menus = struct(
|
|
|
file=self.menu('&File'),
|
|
|
edit=self.menu('&Polygons'),
|
|
|
view=self.menu('&View'),
|
|
|
labelList=labelMenu)
|
|
|
- addActions(self.menus.edit, (mode, color1, color2))
|
|
|
+ addActions(self.menus.edit, (color1, color2))
|
|
|
addActions(self.menus.view, (
|
|
|
- labels, None,
|
|
|
+ labels, advancedMode, None,
|
|
|
+ hideAll, showAll, None,
|
|
|
zoomIn, zoomOut, zoomOrg, None,
|
|
|
fitWindow, fitWidth))
|
|
|
self.menus.file.aboutToShow.connect(self.updateFileMenu)
|
|
|
|
|
|
+ # Custom context menu for the canvas widget:
|
|
|
+ addActions(self.canvas.menus[0], self.actions.beginnerContext)
|
|
|
+ addActions(self.canvas.menus[1], (
|
|
|
+ action('&Copy here', self.copyShape),
|
|
|
+ action('&Move here', self.moveShape)))
|
|
|
+
|
|
|
self.tools = self.toolbar('Tools')
|
|
|
- addActions(self.tools, (
|
|
|
+ self.actions.beginner = (
|
|
|
+ open, save, None, create, delete, None,
|
|
|
+ zoomIn, zoom, zoomOut, fitWindow, fitWidth)
|
|
|
+
|
|
|
+ self.actions.advanced = (
|
|
|
open, save, None,
|
|
|
- mode, delete, hide, None,
|
|
|
- zoomIn, zoom, zoomOut, fitWindow, fitWidth))
|
|
|
+ createMode, editMode, copy, delete, None,
|
|
|
+ hideAll, showAll)
|
|
|
|
|
|
self.statusBar().showMessage('%s started.' % __appname__)
|
|
|
self.statusBar().show()
|
|
@@ -275,6 +309,10 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
Shape.line_color = self.lineColor
|
|
|
Shape.fill_color = self.fillColor
|
|
|
|
|
|
+ if settings.get('advanced', QVariant()).toBool():
|
|
|
+ self.actions.advancedMode.setChecked(True)
|
|
|
+ self.toggleAdvancedMode()
|
|
|
+
|
|
|
# Populate the File menu dynamically.
|
|
|
self.updateFileMenu()
|
|
|
# Since loading the file may take some time, make sure it runs in the background.
|
|
@@ -283,12 +321,46 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
# Callbacks:
|
|
|
self.zoomWidget.valueChanged.connect(self.paintCanvas)
|
|
|
|
|
|
+ self.populateModeActions()
|
|
|
+
|
|
|
#self.firstStart = True
|
|
|
#if self.firstStart:
|
|
|
# QWhatsThis.enterWhatsThisMode()
|
|
|
|
|
|
## Support Functions ##
|
|
|
|
|
|
+ def noShapes(self):
|
|
|
+ return not self.itemsToShapes
|
|
|
+
|
|
|
+ def toggleAdvancedMode(self, value=True):
|
|
|
+ self._beginner = not value
|
|
|
+ self.populateModeActions()
|
|
|
+ self.editButton.setVisible(not value)
|
|
|
+ if value:
|
|
|
+ self.actions.createMode.setEnabled(True)
|
|
|
+ self.actions.editMode.setEnabled(True)
|
|
|
+ self.dock.setFeatures(self.dock.features() | self.dockFeatures)
|
|
|
+ else:
|
|
|
+ self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)
|
|
|
+
|
|
|
+ def populateModeActions(self):
|
|
|
+ if self.beginner():
|
|
|
+ tool, menu = self.actions.beginner, self.actions.beginnerContext
|
|
|
+ else:
|
|
|
+ tool, menu = self.actions.advanced, self.actions.advancedContext
|
|
|
+ self.tools.clear()
|
|
|
+ addActions(self.tools, tool)
|
|
|
+ self.canvas.menus[0].clear()
|
|
|
+ addActions(self.canvas.menus[0], menu)
|
|
|
+
|
|
|
+ def setBeginner(self):
|
|
|
+ self.tools.clear()
|
|
|
+ addActions(self.tools, self.actions.beginner)
|
|
|
+
|
|
|
+ def setAdvanced(self):
|
|
|
+ self.tools.clear()
|
|
|
+ addActions(self.tools, self.actions.advanced)
|
|
|
+
|
|
|
def setDirty(self):
|
|
|
self.dirty = True
|
|
|
self.actions.save.setEnabled(True)
|
|
@@ -296,14 +368,15 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
def setClean(self):
|
|
|
self.dirty = False
|
|
|
self.actions.save.setEnabled(False)
|
|
|
- self.actions.mode.setEnabled(True)
|
|
|
+ self.actions.create.setEnabled(True)
|
|
|
+ # XXX
|
|
|
|
|
|
def toggleActions(self, value=True):
|
|
|
"""Enable/Disable widgets which depend on an opened image."""
|
|
|
for z in self.actions.zoomActions:
|
|
|
z.setEnabled(value)
|
|
|
- self.actions.close.setEnabled(value)
|
|
|
- self.actions.mode.setEnabled(value)
|
|
|
+ for action in self.actions.onLoadActive:
|
|
|
+ action.setEnabled(value)
|
|
|
|
|
|
def queueEvent(self, function):
|
|
|
QTimer.singleShot(0, function)
|
|
@@ -333,7 +406,34 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.recentFiles.pop()
|
|
|
self.recentFiles.insert(0, filename)
|
|
|
|
|
|
+ def beginner(self):
|
|
|
+ return self._beginner
|
|
|
+
|
|
|
+ def advanced(self):
|
|
|
+ return not self.beginner()
|
|
|
+
|
|
|
## Callbacks ##
|
|
|
+ def createShape(self):
|
|
|
+ assert self.beginner()
|
|
|
+ self.canvas.setEditing(False)
|
|
|
+
|
|
|
+ def toggleDrawingSensitive(self, drawing=True):
|
|
|
+ """In the middle of drawing, toggling between modes should be disabled."""
|
|
|
+ self.actions.createMode.setEnabled(not drawing)
|
|
|
+ self.actions.editMode.setEnabled(not drawing)
|
|
|
+
|
|
|
+ def toggleDrawMode(self, edit=True):
|
|
|
+ self.canvas.setEditing(edit)
|
|
|
+ self.actions.createMode.setEnabled(edit)
|
|
|
+ self.actions.editMode.setEnabled(not edit)
|
|
|
+
|
|
|
+ def setCreateMode(self):
|
|
|
+ assert self.advanced()
|
|
|
+ self.toggleDrawMode(False)
|
|
|
+
|
|
|
+ def setEditMode(self):
|
|
|
+ assert self.advanced()
|
|
|
+ self.toggleDrawMode(True)
|
|
|
|
|
|
def updateFileMenu(self):
|
|
|
current = self.filename
|
|
@@ -356,7 +456,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.menus.labelList.exec_(self.labelList.mapToGlobal(point))
|
|
|
|
|
|
def editLabel(self, item=None):
|
|
|
- if self.canvas.editing():
|
|
|
+ if not self.canvas.editing():
|
|
|
return
|
|
|
item = item if item else self.currentItem()
|
|
|
text = self.simpleLabelDialog.popUp(item.text())
|
|
@@ -387,10 +487,14 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.itemsToShapes[item] = shape
|
|
|
self.shapesToItems[shape] = item
|
|
|
self.labelList.addItem(item)
|
|
|
+ for action in self.actions.onShapesPresent:
|
|
|
+ action.setEnabled(True)
|
|
|
|
|
|
def remLabel(self, shape):
|
|
|
- item = self.shapesToItems.get(shape, None)
|
|
|
+ item = self.shapesToItems[shape]
|
|
|
self.labelList.takeItem(self.labelList.row(item))
|
|
|
+ del self.shapesToItems[shape]
|
|
|
+ del self.itemsToShapes[item]
|
|
|
|
|
|
def loadLabels(self, shapes):
|
|
|
s = []
|
|
@@ -398,6 +502,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
shape = Shape(label=label)
|
|
|
for x, y in points:
|
|
|
shape.addPoint(QPointF(x, y))
|
|
|
+ shape.addPoint(shape[0]) # Close shape
|
|
|
s.append(shape)
|
|
|
self.addLabel(shape)
|
|
|
if line_color:
|
|
@@ -433,7 +538,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
|
|
|
def labelSelectionChanged(self):
|
|
|
item = self.currentItem()
|
|
|
- if item and not self.canvas.editing():
|
|
|
+ if item and self.canvas.editing():
|
|
|
self._noSelectionSlot = True
|
|
|
self.canvas.selectShape(self.itemsToShapes[item])
|
|
|
|
|
@@ -452,10 +557,14 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
|
|
|
position MUST be in global coordinates.
|
|
|
"""
|
|
|
- action = self.labelDialog.popUp(text="Enter object's name", position=position)
|
|
|
+ action = self.labelDialog.popUp(position=position)
|
|
|
if action == self.labelDialog.OK:
|
|
|
self.addLabel(self.canvas.setLastLabel(self.labelDialog.text()))
|
|
|
- self.actions.mode.setEnabled(True)
|
|
|
+ if self.beginner(): # Switch to edit mode.
|
|
|
+ self.canvas.setEditing(True)
|
|
|
+ self.actions.create.setEnabled(True)
|
|
|
+ else:
|
|
|
+ self.actions.editMode.setEnabled(True)
|
|
|
self.setDirty()
|
|
|
elif action == self.labelDialog.UNDO:
|
|
|
self.canvas.undoLastLine()
|
|
@@ -495,10 +604,9 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.zoomMode = self.FIT_WIDTH if value else self.MANUAL_ZOOM
|
|
|
self.adjustScale()
|
|
|
|
|
|
- def hideLabelsToggle(self, value):
|
|
|
- #self.canvas.hideBackroundShapes(value)
|
|
|
+ def togglePolygons(self, value):
|
|
|
for item, shape in self.itemsToShapes.iteritems():
|
|
|
- item.setCheckState(Qt.Unchecked if value else Qt.Checked)
|
|
|
+ item.setCheckState(Qt.Checked if value else Qt.Unchecked)
|
|
|
|
|
|
def loadFile(self, filename=None):
|
|
|
"""Load the specified file, or the last opened file if None."""
|
|
@@ -591,6 +699,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
s['line/color'] = self.lineColor
|
|
|
s['fill/color'] = self.fillColor
|
|
|
s['recentFiles'] = self.recentFiles
|
|
|
+ s['advanced'] = not self._beginner
|
|
|
# ask the use for where to save the labels
|
|
|
#s['window/geometry'] = self.saveGeometry()
|
|
|
|
|
@@ -665,21 +774,15 @@ class MainWindow(QMainWindow, WindowMixin):
|
|
|
self.canvas.update()
|
|
|
self.setDirty()
|
|
|
|
|
|
- def toggleDrawing(self, value=True):
|
|
|
- if value:
|
|
|
- self.canvas.deSelectShape()
|
|
|
- self.canvas.setEditing(value)
|
|
|
-
|
|
|
- def toggleDrawingSensitive(self, drawing=True):
|
|
|
- """In the middle of drawing, toggling between modes should be disabled."""
|
|
|
- self.actions.mode.setEnabled(not drawing)
|
|
|
-
|
|
|
def deleteSelectedShape(self):
|
|
|
yes, no = QMessageBox.Yes, QMessageBox.No
|
|
|
msg = u'You are about to permanently delete this polygon, proceed anyway?'
|
|
|
if yes == QMessageBox.warning(self, u'Attention', msg, yes|no):
|
|
|
self.remLabel(self.canvas.deleteSelected())
|
|
|
self.setDirty()
|
|
|
+ if self.noShapes():
|
|
|
+ for action in self.actions.onShapesPresent:
|
|
|
+ action.setEnabled(False)
|
|
|
|
|
|
def chshapeLineColor(self):
|
|
|
color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
|