From c5c2a34a394746c78363c33ea0d454c0a9d5deaa Mon Sep 17 00:00:00 2001 From: Thibaut Mattio Date: Tue, 28 Feb 2017 11:50:15 +0800 Subject: [PATCH 1/4] Apply PEP recommendation on formatting Running pylint to make the code complient with PEP recommendations on lintage. --- _init_path.py | 1 + labelImg.py | 307 +++++++++++++++++++++++------------------- libs/canvas.py | 59 ++++---- libs/colorDialog.py | 5 +- libs/labelDialog.py | 1 + libs/labelFile.py | 19 +-- libs/lib.py | 9 +- libs/pascal_voc_io.py | 4 +- libs/shape.py | 18 +-- libs/toolBar.py | 3 +- libs/zoomWidget.py | 3 +- tests/test.py | 1 - 12 files changed, 237 insertions(+), 193 deletions(-) diff --git a/_init_path.py b/_init_path.py index 554e26a5..9a0ab467 100644 --- a/_init_path.py +++ b/_init_path.py @@ -1,6 +1,7 @@ """Set up paths""" import sys + def add_path(path): if path not in sys.path: sys.path.insert(0, path) diff --git a/labelImg.py b/labelImg.py index e20fdbca..d9df7717 100755 --- a/labelImg.py +++ b/labelImg.py @@ -17,7 +17,8 @@ try: except ImportError: # needed for py3+qt4 # ref: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html - # ref: http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + # ref: + # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string if sys.version_info.major >= 3: import sip sip.setapi('QVariant', 2) @@ -39,7 +40,8 @@ from pascal_voc_io import XML_EXT __appname__ = 'labelImg' -### Utility functions and classes. +# Utility functions and classes. + def u(x): '''py2/py3 unicode helper''' @@ -52,6 +54,7 @@ def u(x): else: return x # py3 + def have_qstring(): '''p3/qt5 get rid of QString wrapper as py3 has native unicode str type''' return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.')) @@ -62,6 +65,7 @@ def util_qt_strlistclass(): class WindowMixin(object): + def menu(self, title, actions=None): menu = self.menuBar().addMenu(title) if actions: @@ -71,7 +75,7 @@ class WindowMixin(object): def toolbar(self, title, actions=None): toolbar = ToolBar(title) toolbar.setObjectName(u'%sToolBar' % title) - #toolbar.setOrientation(Qt.Vertical) + # toolbar.setOrientation(Qt.Vertical) toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) if actions: addActions(toolbar, actions) @@ -81,8 +85,10 @@ class WindowMixin(object): # PyQt5: TypeError: unhashable type: 'QListWidgetItem' class HashableQListWidgetItem(QListWidgetItem): + def __init__(self, *args): super(HashableQListWidgetItem, self).__init__(*args) + def __hash__(self): return hash(id(self)) @@ -135,17 +141,17 @@ class MainWindow(QMainWindow, WindowMixin): self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.labelListContainer = QWidget() self.labelListContainer.setLayout(listLayout) - listLayout.addWidget(self.editButton)#, 0, Qt.AlignCenter) + listLayout.addWidget(self.editButton) # , 0, Qt.AlignCenter) listLayout.addWidget(self.labelList) - self.dock = QDockWidget(u'Box Labels', self) self.dock.setObjectName(u'Labels') self.dock.setWidget(self.labelListContainer) # Tzutalin 20160906 : Add file list and dock to move faster self.fileListWidget = QListWidget() - self.fileListWidget.itemDoubleClicked.connect(self.fileitemDoubleClicked) + self.fileListWidget.itemDoubleClicked.connect( + self.fileitemDoubleClicked) filelistLayout = QVBoxLayout() filelistLayout.setContentsMargins(0, 0, 0, 0) filelistLayout.addWidget(self.fileListWidget) @@ -167,7 +173,7 @@ class MainWindow(QMainWindow, WindowMixin): self.scrollBars = { Qt.Vertical: scroll.verticalScrollBar(), Qt.Horizontal: scroll.horizontalScrollBar() - } + } self.canvas.scrollRequest.connect(self.scrollRequest) self.canvas.newShape.connect(self.newShape) @@ -180,93 +186,94 @@ class MainWindow(QMainWindow, WindowMixin): # Tzutalin 20160906 : Add file list and dock to move faster self.addDockWidget(Qt.RightDockWidgetArea, self.filedock) self.dockFeatures = QDockWidget.DockWidgetClosable\ - | QDockWidget.DockWidgetFloatable + | QDockWidget.DockWidgetFloatable self.dock.setFeatures(self.dock.features() ^ self.dockFeatures) # Actions action = partial(newAction, self) quit = action('&Quit', self.close, - 'Ctrl+Q', 'quit', u'Quit application') + 'Ctrl+Q', 'quit', u'Quit application') open = action('&Open', self.openFile, - 'Ctrl+O', 'open', u'Open image or label file') + 'Ctrl+O', 'open', u'Open image or label file') opendir = action('&Open Dir', self.openDir, - 'Ctrl+u', 'open', u'Open Dir') + 'Ctrl+u', 'open', u'Open Dir') changeSavedir = action('&Change default saved Annotation dir', self.changeSavedir, - 'Ctrl+r', 'open', u'Change default saved Annotation dir') + 'Ctrl+r', 'open', u'Change default saved Annotation dir') openAnnotation = action('&Open Annotation', self.openAnnotation, - 'Ctrl+q', 'openAnnotation', u'Open Annotation') + 'Ctrl+q', 'openAnnotation', u'Open Annotation') openNextImg = action('&Next Image', self.openNextImg, - 'd', 'next', u'Open Next') + 'd', 'next', u'Open Next') openPrevImg = action('&Prev Image', self.openPrevImg, - 'a', 'prev', u'Open Prev') + 'a', 'prev', u'Open Prev') save = action('&Save', self.saveFile, - 'Ctrl+S', 'save', u'Save labels to file', enabled=False) + 'Ctrl+S', 'save', u'Save labels to file', enabled=False) saveAs = action('&Save As', self.saveFileAs, - 'Ctrl+Shift+S', 'save-as', u'Save labels to a different file', - enabled=False) + 'Ctrl+Shift+S', 'save-as', u'Save labels to a different file', + enabled=False) close = action('&Close', self.closeFile, - 'Ctrl+W', 'close', u'Close current file') + 'Ctrl+W', 'close', u'Close current file') color1 = action('Box &Line Color', self.chooseColor1, - 'Ctrl+L', 'color_line', u'Choose Box line color') + 'Ctrl+L', 'color_line', u'Choose Box line color') color2 = action('Box &Fill Color', self.chooseColor2, - 'Ctrl+Shift+L', 'color', u'Choose Box fill color') + 'Ctrl+Shift+L', 'color', u'Choose Box fill color') createMode = action('Create\nRectBox', self.setCreateMode, - 'Ctrl+N', 'new', u'Start drawing Boxs', enabled=False) + 'Ctrl+N', 'new', u'Start drawing Boxs', enabled=False) editMode = action('&Edit\nRectBox', self.setEditMode, - 'Ctrl+J', 'edit', u'Move and edit Boxs', enabled=False) + 'Ctrl+J', 'edit', u'Move and edit Boxs', enabled=False) create = action('Create\nRectBox', self.createShape, - 'w', 'new', u'Draw a new Box', enabled=False) + 'w', 'new', u'Draw a new Box', enabled=False) delete = action('Delete\nRectBox', self.deleteSelectedShape, - 'Delete', 'delete', u'Delete', enabled=False) + 'Delete', 'delete', u'Delete', enabled=False) copy = action('&Duplicate\nRectBox', self.copySelectedShape, - 'Ctrl+D', 'copy', u'Create a duplicate of the selected Box', - enabled=False) + 'Ctrl+D', 'copy', u'Create a duplicate of the selected Box', + enabled=False) advancedMode = action('&Advanced Mode', self.toggleAdvancedMode, - 'Ctrl+Shift+A', 'expert', u'Switch to advanced mode', - checkable=True) + 'Ctrl+Shift+A', 'expert', u'Switch to advanced mode', + checkable=True) hideAll = action('&Hide\nRectBox', partial(self.togglePolygons, False), - 'Ctrl+H', 'hide', u'Hide all Boxs', - enabled=False) + 'Ctrl+H', 'hide', u'Hide all Boxs', + enabled=False) showAll = action('&Show\nRectBox', partial(self.togglePolygons, True), - 'Ctrl+A', 'hide', u'Show all Boxs', - enabled=False) + 'Ctrl+A', 'hide', u'Show all Boxs', + enabled=False) help = action('&Tutorial', self.tutorial, 'Ctrl+T', 'help', - u'Show demos') + u'Show demos') zoom = QWidgetAction(self) zoom.setDefaultWidget(self.zoomWidget) self.zoomWidget.setWhatsThis( - u"Zoom in or out of the image. Also accessible with"\ - " %s and %s from the canvas." % (fmtShortcut("Ctrl+[-+]"), - fmtShortcut("Ctrl+Wheel"))) + u"Zoom in or out of the image. Also accessible with" + " %s and %s from the canvas." % (fmtShortcut("Ctrl+[-+]"), + fmtShortcut("Ctrl+Wheel"))) self.zoomWidget.setEnabled(False) zoomIn = action('Zoom &In', partial(self.addZoom, 10), - 'Ctrl++', 'zoom-in', u'Increase zoom level', enabled=False) + 'Ctrl++', 'zoom-in', u'Increase zoom level', enabled=False) zoomOut = action('&Zoom Out', partial(self.addZoom, -10), - 'Ctrl+-', 'zoom-out', u'Decrease zoom level', enabled=False) + 'Ctrl+-', 'zoom-out', u'Decrease zoom level', enabled=False) zoomOrg = action('&Original size', partial(self.setZoom, 100), - 'Ctrl+=', 'zoom', u'Zoom to original size', enabled=False) + 'Ctrl+=', 'zoom', u'Zoom to original size', enabled=False) fitWindow = action('&Fit Window', self.setFitWindow, - 'Ctrl+F', 'fit-window', u'Zoom follows window size', - checkable=True, enabled=False) + 'Ctrl+F', 'fit-window', u'Zoom follows window size', + checkable=True, enabled=False) fitWidth = action('Fit &Width', self.setFitWidth, - 'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width', - checkable=True, enabled=False) + 'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width', + checkable=True, enabled=False) # Group zoom controls into a list for easier toggling. - zoomActions = (self.zoomWidget, zoomIn, zoomOut, zoomOrg, fitWindow, fitWidth) + zoomActions = (self.zoomWidget, zoomIn, zoomOut, + zoomOrg, fitWindow, fitWidth) self.zoomMode = self.MANUAL_ZOOM self.scalers = { self.FIT_WINDOW: self.scaleFitWindow, @@ -276,16 +283,16 @@ class MainWindow(QMainWindow, WindowMixin): } edit = action('&Edit Label', self.editLabel, - 'Ctrl+E', 'edit', u'Modify the label of the selected Box', - enabled=False) + 'Ctrl+E', 'edit', u'Modify the label of the selected Box', + enabled=False) self.editButton.setDefaultAction(edit) shapeLineColor = action('Shape &Line Color', self.chshapeLineColor, - icon='color_line', tip=u'Change the line color for this specific shape', - enabled=False) + icon='color_line', tip=u'Change the line color for this specific shape', + enabled=False) shapeFillColor = action('Shape &Fill Color', self.chshapeFillColor, - icon='color', tip=u'Change the fill color for this specific shape', - enabled=False) + icon='color', tip=u'Change the fill color for this specific shape', + enabled=False) labels = self.dock.toggleViewAction() labels.setText('Show/Hide Label Panel') @@ -295,36 +302,40 @@ class MainWindow(QMainWindow, WindowMixin): labelMenu = QMenu() addActions(labelMenu, (edit, delete)) self.labelList.setContextMenuPolicy(Qt.CustomContextMenu) - self.labelList.customContextMenuRequested.connect(self.popLabelListMenu) + self.labelList.customContextMenuRequested.connect( + self.popLabelListMenu) # Store actions for further handling. self.actions = struct(save=save, saveAs=saveAs, open=open, close=close, - lineColor=color1, fillColor=color2, - 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,opendir,save,saveAs,close,quit), - beginner=(), advanced=(), - editMenu=(edit, copy, delete, None, color1, color2), - beginnerContext=(create, edit, copy, delete), - advancedContext=(createMode, editMode, edit, copy, - delete, shapeLineColor, shapeFillColor), - onLoadActive=(close, create, createMode, editMode), - onShapesPresent=(saveAs, hideAll, showAll)) + lineColor=color1, fillColor=color2, + 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, opendir, save, saveAs, close, quit), + beginner=(), advanced=(), + editMenu=(edit, copy, delete, + None, color1, color2), + beginnerContext=(create, edit, copy, delete), + advancedContext=(createMode, editMode, edit, copy, + delete, shapeLineColor, shapeFillColor), + onLoadActive=( + close, create, createMode, editMode), + onShapesPresent=(saveAs, hideAll, showAll)) self.menus = struct( - file=self.menu('&File'), - edit=self.menu('&Edit'), - view=self.menu('&View'), - help=self.menu('&Help'), - recentFiles=QMenu('Open &Recent'), - labelList=labelMenu) + file=self.menu('&File'), + edit=self.menu('&Edit'), + view=self.menu('&View'), + help=self.menu('&Help'), + recentFiles=QMenu('Open &Recent'), + labelList=labelMenu) addActions(self.menus.file, - (open, opendir,changeSavedir, openAnnotation, self.menus.recentFiles, save, saveAs, close, None, quit)) + (open, opendir, changeSavedir, openAnnotation, self.menus.recentFiles, save, saveAs, close, None, quit)) addActions(self.menus.help, (help,)) addActions(self.menus.view, ( labels, advancedMode, None, @@ -406,11 +417,12 @@ class MainWindow(QMainWindow, WindowMixin): self.lastOpenDir = u(settings.get('lastOpenDir', None)) if os.path.exists(saveDir): self.defaultSaveDir = saveDir - self.statusBar().showMessage('%s started. Annotation will be saved to %s' %(__appname__, self.defaultSaveDir)) + self.statusBar().showMessage('%s started. Annotation will be saved to %s' % + (__appname__, self.defaultSaveDir)) self.statusBar().show() # or simply: - #self.restoreGeometry(settings['window/geometry'] + # self.restoreGeometry(settings['window/geometry'] self.restoreState(settings.get('window/state', QByteArray())) self.lineColor = QColor(settings.get('line/color', Shape.line_color)) self.fillColor = QColor(settings.get('fill/color', Shape.fill_color)) @@ -428,7 +440,8 @@ class MainWindow(QMainWindow, WindowMixin): # Populate the File menu dynamically. self.updateFileMenu() - # Since loading the file may take some time, make sure it runs in the background. + # Since loading the file may take some time, make sure it runs in the + # background. self.queueEvent(partial(self.loadFile, self.filePath)) # Callbacks: @@ -436,7 +449,6 @@ class MainWindow(QMainWindow, WindowMixin): self.populateModeActions() - ## Support Functions ## def noShapes(self): @@ -465,7 +477,7 @@ class MainWindow(QMainWindow, WindowMixin): addActions(self.canvas.menus[0], menu) self.menus.edit.clear() actions = (self.actions.create,) if self.beginner()\ - else (self.actions.createMode, self.actions.editMode) + else (self.actions.createMode, self.actions.editMode) addActions(self.menus.edit, actions + self.actions.editMenu) def setBeginner(self): @@ -560,15 +572,17 @@ class MainWindow(QMainWindow, WindowMixin): def updateFileMenu(self): currFilePath = self.filePath + def exists(filename): return os.path.exists(filename) menu = self.menus.recentFiles menu.clear() - files = [f for f in self.recentFiles if f != currFilePath and exists(f)] + files = [f for f in self.recentFiles if f != + currFilePath and exists(f)] for i, f in enumerate(files): icon = newIcon('labels') action = QAction( - icon, '&%d %s' % (i+1, QFileInfo(f).fileName()), self) + icon, '&%d %s' % (i + 1, QFileInfo(f).fileName()), self) action.triggered.connect(partial(self.loadRecent, f)) menu.addAction(action) @@ -587,7 +601,7 @@ class MainWindow(QMainWindow, WindowMixin): # Tzutalin 20160906 : Add file list and dock to move faster def fileitemDoubleClicked(self, item=None): currIndex = self.mImgList.index(u(item.text())) - if currIndex < len(self.mImgList): + if currIndex < len(self.mImgList): filename = self.mImgList[currIndex] if filename: self.loadFile(filename) @@ -642,19 +656,21 @@ class MainWindow(QMainWindow, WindowMixin): def saveLabels(self, annotationFilePath): annotationFilePath = u(annotationFilePath) lf = LabelFile() + def format_shape(s): return dict(label=s.label, - line_color=s.line_color.getRgb()\ - if s.line_color != self.lineColor else None, - fill_color=s.fill_color.getRgb()\ - if s.fill_color != self.fillColor else None, + line_color=s.line_color.getRgb() + if s.line_color != self.lineColor else None, + fill_color=s.fill_color.getRgb() + if s.fill_color != self.fillColor else None, points=[(p.x(), p.y()) for p in s.points]) shapes = [format_shape(shape) for shape in self.canvas.shapes] # Can add differrent annotation formats here try: if self.usingPascalVocFormat is True: - print ('Img: ' + self.filePath + ' -> Its xml: ' + annotationFilePath) + print ('Img: ' + self.filePath + + ' -> Its xml: ' + annotationFilePath) lf.savePascalVocFormat(annotationFilePath, shapes, self.filePath, self.imageData, self.lineColor.getRgb(), self.fillColor.getRgb()) else: @@ -664,12 +680,12 @@ class MainWindow(QMainWindow, WindowMixin): return True except LabelFileError as e: self.errorMessage(u'Error saving label data', - u'%s' % e) + u'%s' % e) return False def copySelectedShape(self): self.addLabel(self.canvas.copySelectedShape()) - #fix copy and delete + # fix copy and delete self.shapeSelectionChanged(True) def labelSelectionChanged(self): @@ -684,34 +700,34 @@ class MainWindow(QMainWindow, WindowMixin): if label != shape.label: shape.label = item.text() self.setDirty() - else: # User probably changed item visibility + else: # User probably changed item visibility self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) - ## Callback functions: + # Callback functions: def newShape(self): """Pop-up and give focus to the label editor. position MUST be in global coordinates. """ if len(self.labelHist) > 0: - self.labelDialog = LabelDialog(parent=self, listItem=self.labelHist) + self.labelDialog = LabelDialog( + parent=self, listItem=self.labelHist) text = self.labelDialog.popUp(text=self.prevLabelText) if text is not None: self.prevLabelText = text self.addLabel(self.canvas.setLastLabel(text)) - if self.beginner(): # Switch to edit mode. + if self.beginner(): # Switch to edit mode. self.canvas.setEditing(True) self.actions.create.setEnabled(True) else: self.actions.editMode.setEnabled(True) self.setDirty() - if text not in self.labelHist: self.labelHist.append(text) else: - #self.canvas.undoLastLine() + # self.canvas.undoLastLine() self.canvas.resetAllLines() def scrollRequest(self, delta, orientation): @@ -771,7 +787,7 @@ class MainWindow(QMainWindow, WindowMixin): except LabelFileError as e: self.errorMessage(u'Error opening file', (u"

%s

" - u"

Make sure %s is a valid label file.") \ + u"

Make sure %s is a valid label file.") % (e, unicodeFilePath)) self.status("Error reading %s" % unicodeFilePath) return False @@ -802,10 +818,11 @@ class MainWindow(QMainWindow, WindowMixin): self.addRecentFile(self.filePath) self.toggleActions(True) - ## Label xml file and show bound box according to its filename + # Label xml file and show bound box according to its filename if self.usingPascalVocFormat is True and \ - self.defaultSaveDir is not None: - basename = os.path.basename(os.path.splitext(self.filePath)[0]) + XML_EXT + self.defaultSaveDir is not None: + basename = os.path.basename( + os.path.splitext(self.filePath)[0]) + XML_EXT xmlPath = os.path.join(self.defaultSaveDir, basename) self.loadPascalXMLByFilename(xmlPath) @@ -831,10 +848,10 @@ class MainWindow(QMainWindow, WindowMixin): def scaleFitWindow(self): """Figure out the size of the pixmap in order to fit the main widget.""" - e = 2.0 # So that no scrollbars are generated. + e = 2.0 # So that no scrollbars are generated. w1 = self.centralWidget().width() - e h1 = self.centralWidget().height() - e - a1 = w1/ h1 + a1 = w1 / h1 # Calculate a new scale value based on the pixmap's aspect ratio. w2 = self.canvas.pixmap.width() - 0.0 h2 = self.canvas.pixmap.height() - 0.0 @@ -880,7 +897,7 @@ class MainWindow(QMainWindow, WindowMixin): self.loadFile(filename) def scanAllImages(self, folderPath): - extensions = ['.jpeg','.jpg', '.png', '.bmp'] + extensions = ['.jpeg', '.jpg', '.png', '.bmp'] images = [] for root, dirs, files in os.walk(folderPath): @@ -899,13 +916,14 @@ class MainWindow(QMainWindow, WindowMixin): path = '.' dirpath = str(QFileDialog.getExistingDirectory(self, - '%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly - | QFileDialog.DontResolveSymlinks)) + '%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly + | QFileDialog.DontResolveSymlinks)) if dirpath is not None and len(dirpath) > 1: self.defaultSaveDir = dirpath - self.statusBar().showMessage('%s . Annotation will be saved to %s' %('Change saved folder', self.defaultSaveDir)) + self.statusBar().showMessage('%s . Annotation will be saved to %s' % + ('Change saved folder', self.defaultSaveDir)) self.statusBar().show() def openAnnotation(self, _value=False): @@ -913,14 +931,14 @@ class MainWindow(QMainWindow, WindowMixin): return path = os.path.dirname(u(self.filePath))\ - if self.filePath else '.' + if self.filePath else '.' if self.usingPascalVocFormat: - formats = ['*.%s' % str(fmt).lower()\ - for fmt in QImageReader.supportedImageFormats()] + formats = ['*.%s' % str(fmt).lower() + for fmt in QImageReader.supportedImageFormats()] filters = "Open Annotation XML file (%s)" % \ - ' '.join(formats + ['*.xml']) + ' '.join(formats + ['*.xml']) filename = str(QFileDialog.getOpenFileName(self, - '%s - Choose a xml file' % __appname__, path, filters)) + '%s - Choose a xml file' % __appname__, path, filters)) self.loadPascalXMLByFilename(filename) def openDir(self, _value=False): @@ -928,14 +946,14 @@ class MainWindow(QMainWindow, WindowMixin): return path = os.path.dirname(self.filePath)\ - if self.filePath else '.' + if self.filePath else '.' if self.lastOpenDir is not None and len(self.lastOpenDir) > 1: path = self.lastOpenDir dirpath = u(QFileDialog.getExistingDirectory(self, - '%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly - | QFileDialog.DontResolveSymlinks)) + '%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly + | QFileDialog.DontResolveSymlinks)) if dirpath is not None and len(dirpath) > 1: self.lastOpenDir = dirpath @@ -960,8 +978,8 @@ class MainWindow(QMainWindow, WindowMixin): return currIndex = self.mImgList.index(self.filePath) - if currIndex -1 >= 0: - filename = self.mImgList[currIndex-1] + if currIndex - 1 >= 0: + filename = self.mImgList[currIndex - 1] if filename: self.loadFile(filename) @@ -983,7 +1001,7 @@ class MainWindow(QMainWindow, WindowMixin): else: currIndex = self.mImgList.index(self.filePath) if currIndex + 1 < len(self.mImgList): - filename = self.mImgList[currIndex+1] + filename = self.mImgList[currIndex + 1] if filename: self.loadFile(filename) @@ -992,13 +1010,13 @@ class MainWindow(QMainWindow, WindowMixin): if not self.mayContinue(): return path = os.path.dirname(str(self.filePath))\ - if self.filePath else '.' - formats = ['*.%s' % str(fmt).lower()\ - for fmt in QImageReader.supportedImageFormats()] + if self.filePath else '.' + formats = ['*.%s' % str(fmt).lower() + for fmt in QImageReader.supportedImageFormats()] filters = "Image & Label files (%s)" % \ - ' '.join(formats + ['*%s' % LabelFile.suffix]) + ' '.join(formats + ['*%s' % LabelFile.suffix]) filename = QFileDialog.getOpenFileName(self, - '%s - Choose Image or Label file' % __appname__, path, filters) + '%s - Choose Image or Label file' % __appname__, path, filters) if filename: self.loadFile(filename) @@ -1008,12 +1026,14 @@ class MainWindow(QMainWindow, WindowMixin): if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): # print('handle the image:' + self.filePath) imgFileName = os.path.basename(self.filePath) - savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix - savedPath = os.path.join(str(self.defaultSaveDir), savedFileName) + savedFileName = os.path.splitext( + imgFileName)[0] + LabelFile.suffix + savedPath = os.path.join( + str(self.defaultSaveDir), savedFileName) self._saveFile(savedPath) else: - self._saveFile(self.filePath if self.labelFile\ - else self.saveFileDialog()) + self._saveFile(self.filePath if self.labelFile + else self.saveFileDialog()) def saveFileAs(self, _value=False): assert not self.image.isNull(), "cannot save empty image" @@ -1024,7 +1044,7 @@ class MainWindow(QMainWindow, WindowMixin): caption = '%s - Choose File' % __appname__ filters = 'File (*%s)' % LabelFile.suffix openDialogPath = self.currentPath() - dlg = QFileDialog(self, caption, openDialogPath, filters) + dlg = QFileDialog(self, caption, openDialogPath, filters) dlg.setDefaultSuffix(LabelFile.suffix[1:]) dlg.setAcceptMode(QFileDialog.AcceptSave) filenameWithoutExtension = os.path.splitext(self.filePath)[0] @@ -1053,7 +1073,7 @@ class MainWindow(QMainWindow, WindowMixin): def hasLabels(self): if not self.itemsToShapes: self.errorMessage(u'No objects labeled', - u'You must label at least one object to save the file.') + u'You must label at least one object to save the file.') return False return True @@ -1063,18 +1083,18 @@ class MainWindow(QMainWindow, WindowMixin): def discardChangesDialog(self): yes, no = QMessageBox.Yes, QMessageBox.No msg = u'You have unsaved changes, proceed anyway?' - return yes == QMessageBox.warning(self, u'Attention', msg, yes|no) + return yes == QMessageBox.warning(self, u'Attention', msg, yes | no) def errorMessage(self, title, message): return QMessageBox.critical(self, title, - '

%s

%s' % (title, message)) + '

%s

%s' % (title, message)) def currentPath(self): return os.path.dirname(self.filePath) if self.filePath else '.' def chooseColor1(self): color = self.colorDialog.getColor(self.lineColor, u'Choose line color', - default=DEFAULT_LINE_COLOR) + default=DEFAULT_LINE_COLOR) if color: self.lineColor = color # Change the color for all shape lines: @@ -1083,9 +1103,9 @@ class MainWindow(QMainWindow, WindowMixin): self.setDirty() def chooseColor2(self): - color = self.colorDialog.getColor(self.fillColor, u'Choose fill color', - default=DEFAULT_FILL_COLOR) - if color: + color = self.colorDialog.getColor(self.fillColor, u'Choose fill color', + default=DEFAULT_FILL_COLOR) + if color: self.fillColor = color Shape.fill_color = self.fillColor self.canvas.update() @@ -1094,7 +1114,7 @@ class MainWindow(QMainWindow, WindowMixin): def deleteSelectedShape(self): yes, no = QMessageBox.Yes, QMessageBox.No msg = u'You are about to permanently delete this Box, proceed anyway?' - if yes == QMessageBox.warning(self, u'Attention', msg, yes|no): + if yes == QMessageBox.warning(self, u'Attention', msg, yes | no): self.remLabel(self.canvas.deleteSelected()) self.setDirty() if self.noShapes(): @@ -1103,7 +1123,7 @@ class MainWindow(QMainWindow, WindowMixin): def chshapeLineColor(self): color = self.colorDialog.getColor(self.lineColor, u'Choose line color', - default=DEFAULT_LINE_COLOR) + default=DEFAULT_LINE_COLOR) if color: self.canvas.selectedShape.line_color = color self.canvas.update() @@ -1111,7 +1131,7 @@ class MainWindow(QMainWindow, WindowMixin): def chshapeFillColor(self): color = self.colorDialog.getColor(self.fillColor, u'Choose fill color', - default=DEFAULT_FILL_COLOR) + default=DEFAULT_FILL_COLOR) if color: self.canvas.selectedShape.fill_color = color self.canvas.update() @@ -1127,7 +1147,8 @@ class MainWindow(QMainWindow, WindowMixin): self.setDirty() def loadPredefinedClasses(self): - predefined_classes_path = os.path.join('data', 'predefined_classes.txt') + predefined_classes_path = os.path.join( + 'data', 'predefined_classes.txt') if os.path.exists(predefined_classes_path) is True: with codecs.open(predefined_classes_path, 'r', 'utf8') as f: for line in f: @@ -1147,8 +1168,10 @@ class MainWindow(QMainWindow, WindowMixin): shapes = tVocParseReader.getShapes() self.loadLabels(shapes) + class Settings(object): """Convenience dict-like wrapper around QSettings.""" + def __init__(self, types=None): self.data = QSettings() self.types = defaultdict(lambda: QVariant, types if types else {}) @@ -1156,7 +1179,7 @@ class Settings(object): def __setitem__(self, key, value): t = self.types[key] self.data.setValue(key, - t(value) if not isinstance(value, t) else value) + t(value) if not isinstance(value, t) else value) def __getitem__(self, key): return self._cast(key, self.data.value(key)) @@ -1172,10 +1195,11 @@ class Settings(object): return str(value) else: try: - method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1)) + method = getattr(QVariant, re.sub( + '^Q', 'to', t.__name__, count=1)) return method(value) except AttributeError as e: - #print(e) + # print(e) return value return value @@ -1183,6 +1207,7 @@ class Settings(object): def inverted(color): return QColor(*[255 - v for v in color.getRgb()]) + def read(filename, default=None): try: with open(filename, 'rb') as f: @@ -1190,6 +1215,7 @@ def read(filename, default=None): except: return default + def get_main_app(argv=[]): """ Standard boilerplate Qt application code. @@ -1202,6 +1228,7 @@ def get_main_app(argv=[]): win.show() return app, win + def main(argv): '''construct main app and run it''' app, _win = get_main_app(argv) diff --git a/libs/canvas.py b/libs/canvas.py index b7e4aafd..592165a7 100644 --- a/libs/canvas.py +++ b/libs/canvas.py @@ -13,12 +13,14 @@ from shape import Shape from lib import distance CURSOR_DEFAULT = Qt.ArrowCursor -CURSOR_POINT = Qt.PointingHandCursor -CURSOR_DRAW = Qt.CrossCursor -CURSOR_MOVE = Qt.ClosedHandCursor -CURSOR_GRAB = Qt.OpenHandCursor +CURSOR_POINT = Qt.PointingHandCursor +CURSOR_DRAW = Qt.CrossCursor +CURSOR_MOVE = Qt.ClosedHandCursor +CURSOR_GRAB = Qt.OpenHandCursor + +# class Canvas(QGLWidget): + -#class Canvas(QGLWidget): class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) @@ -37,8 +39,8 @@ class Canvas(QWidget): self.mode = self.EDIT self.shapes = [] self.current = None - self.selectedShape=None # save the selected shape here - self.selectedShapeCopy=None + self.selectedShape = None # save the selected shape here + self.selectedShapeCopy = None self.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.prevPoint = QPointF() @@ -78,7 +80,7 @@ class Canvas(QWidget): def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE - if not value: # Create + if not value: # Create self.unHighlight() self.deSelectShape() @@ -106,7 +108,8 @@ class Canvas(QWidget): # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]): - # Attract line to starting point and colorise to alert the user: + # Attract line to starting point and colorise to alert the + # user: pos = self.current[0] color = self.current.line_color self.overrideCursor(CURSOR_POINT) @@ -164,12 +167,13 @@ class Canvas(QWidget): if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = None, shape - self.setToolTip("Click & drag to move shape '%s'" % shape.label) + self.setToolTip( + "Click & drag to move shape '%s'" % shape.label) self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() break - else: # Nothing found, clear highlights, reset state. + else: # Nothing found, clear highlights, reset state. if self.hShape: self.hShape.highlightClear() self.update() @@ -270,7 +274,7 @@ class Canvas(QWidget): def selectShapePoint(self, point): """Select the first shape created which contains this point.""" self.deSelectShape() - if self.selectedVertex(): # A vertex is marked for selection. + if self.selectedVertex(): # A vertex is marked for selection. index, shape = self.hVertex, self.hShape shape.highlightVertex(index, shape.MOVE_VERTEX) return @@ -315,14 +319,14 @@ class Canvas(QWidget): def boundedMoveShape(self, shape, pos): if self.outOfPixmap(pos): - return False # No need to move + return False # No need to move o1 = pos + self.offsets[0] if self.outOfPixmap(o1): pos -= QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] if self.outOfPixmap(o2): pos += QPointF(min(0, self.pixmap.width() - o2.x()), - min(0, self.pixmap.height()- o2.y())) + min(0, self.pixmap.height() - o2.y())) # The next line tracks the new position of the cursor # relative to the shape, but also results in making it # a bit "shaky" when nearing the border and allows it to @@ -419,8 +423,8 @@ class Canvas(QWidget): area = super(Canvas, self).size() w, h = self.pixmap.width() * s, self.pixmap.height() * s aw, ah = area.width(), area.height() - x = (aw-w)/(2*s) if aw > w else 0 - y = (ah-h)/(2*s) if ah > h else 0 + x = (aw - w) / (2 * s) if aw > w else 0 + y = (ah - h) / (2 * s) if ah > h else 0 return QPointF(x, y) def outOfPixmap(self, p): @@ -439,7 +443,7 @@ class Canvas(QWidget): def closeEnough(self, p1, p2): #d = distance(p1 - p2) #m = (p1-p2).manhattanLength() - #print "d %.2f, m %d, %.2f" % (d, m, d - m) + # print "d %.2f, m %d, %.2f" % (d, m, d - m) return distance(p1 - p2) < self.epsilon def intersectionPoint(self, p1, p2): @@ -447,7 +451,7 @@ class Canvas(QWidget): # and find the one intersecting the current line segment. # http://paulbourke.net/geometry/lineline2d/ size = self.pixmap.size() - points = [(0,0), + points = [(0, 0), (size.width(), 0), (size.width(), size.height()), (0, size.height())] @@ -455,12 +459,12 @@ class Canvas(QWidget): x2, y2 = p2.x(), p2.y() d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) x3, y3 = points[i] - x4, y4 = points[(i+1)%4] + x4, y4 = points[(i + 1) % 4] if (x, y) == (x1, y1): # Handle cases where previous point is on one of the edges. if x3 == x4: return QPointF(x3, min(max(0, y2), max(y3, y4))) - else: # y3 == y4 + else: # y3 == y4 return QPointF(min(max(0, x2), max(x3, x4)), y3) return QPointF(x, y) @@ -473,10 +477,10 @@ class Canvas(QWidget): x2, y2 = x2y2 for i in range(4): x3, y3 = points[i] - x4, y4 = points[(i+1) % 4] - denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) - nua = (x4-x3) * (y1-y3) - (y4-y3) * (x1-x3) - nub = (x2-x1) * (y1-y3) - (y2-y1) * (x1-x3) + x4, y4 = points[(i + 1) % 4] + denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) + nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) + nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: # This covers two cases: # nua == nub == 0: Coincident @@ -486,7 +490,7 @@ class Canvas(QWidget): if 0 <= ua <= 1 and 0 <= ub <= 1: x = x1 + ua * (x2 - x1) y = y1 + ua * (y2 - y1) - m = QPointF((x3 + x4)/2, (y3 + y4)/2) + m = QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = distance(m - QPointF(x2, y2)) yield d, i, (x, y) @@ -507,8 +511,8 @@ class Canvas(QWidget): self.zoomRequest.emit(ev.delta()) else: self.scrollRequest.emit(ev.delta(), - Qt.Horizontal if (Qt.ShiftModifier == int(mods))\ - else Qt.Vertical) + Qt.Horizontal if (Qt.ShiftModifier == int(mods)) + else Qt.Vertical) else: self.scrollRequest.emit(ev.delta(), Qt.Horizontal) ev.accept() @@ -571,4 +575,3 @@ class Canvas(QWidget): self.restoreCursor() self.pixmap = None self.update() - diff --git a/libs/colorDialog.py b/libs/colorDialog.py index 8c0164a9..d5d94759 100644 --- a/libs/colorDialog.py +++ b/libs/colorDialog.py @@ -8,13 +8,15 @@ except ImportError: BB = QDialogButtonBox + class ColorDialog(QColorDialog): + def __init__(self, parent=None): super(ColorDialog, self).__init__(parent) self.setOption(QColorDialog.ShowAlphaChannel) # The Mac native dialog does not support our restore button. self.setOption(QColorDialog.DontUseNativeDialog) - ## Add a restore defaults button. + # Add a restore defaults button. # The default is set at invocation time, so that it # works across dialogs for different elements. self.default = None @@ -33,4 +35,3 @@ class ColorDialog(QColorDialog): def checkRestore(self, button): if self.bb.buttonRole(button) & BB.ResetRole and self.default: self.setCurrentColor(self.default) - diff --git a/libs/labelDialog.py b/libs/labelDialog.py index c7f70575..636016d6 100644 --- a/libs/labelDialog.py +++ b/libs/labelDialog.py @@ -10,6 +10,7 @@ from lib import newIcon, labelValidator BB = QDialogButtonBox + class LabelDialog(QDialog): def __init__(self, text="Enter object label", parent=None, listItem=None): diff --git a/libs/labelFile.py b/libs/labelFile.py index f4e490ce..6e20891c 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -11,9 +11,11 @@ from pascal_voc_io import PascalVocWriter import os.path import sys + class LabelFileError(Exception): pass + class LabelFile(object): # It might be changed as window creates suffix = '.lif' @@ -26,7 +28,7 @@ class LabelFile(object): self.load(filename) def savePascalVocFormat(self, filename, shapes, imagePath, imageData, - lineColor=None, fillColor=None, databaseSrc=None): + lineColor=None, fillColor=None, databaseSrc=None): imgFolderPath = os.path.dirname(imagePath) imgFolderName = os.path.split(imgFolderPath)[-1] imgFileName = os.path.basename(imagePath) @@ -35,8 +37,9 @@ class LabelFile(object): # Pascal format image = QImage() image.load(imagePath) - imageShape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] - writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt,\ + imageShape = [image.height(), image.width(), + 1 if image.isGrayscale() else 3] + writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt, imageShape, localImgPath=imagePath) bSave = False for shape in shapes: @@ -47,7 +50,7 @@ class LabelFile(object): bSave = True if bSave: - writer.save(targetFile = filename) + writer.save(targetFile=filename) return @staticmethod @@ -64,10 +67,10 @@ class LabelFile(object): for p in points: x = p[0] y = p[1] - xmin = min(x,xmin) - ymin = min(y,ymin) - xmax = max(x,xmax) - ymax = max(y,ymax) + xmin = min(x, xmin) + ymin = min(y, ymin) + xmax = max(x, xmax) + ymax = max(y, ymax) # Martin Kersner, 2015/11/12 # 0-valued coordinates of BB caused an error while diff --git a/libs/lib.py b/libs/lib.py index 854309d7..0600b657 100644 --- a/libs/lib.py +++ b/libs/lib.py @@ -12,6 +12,7 @@ except ImportError: def newIcon(icon): return QIcon(':/' + icon) + def newButton(text, icon=None, slot=None): b = QPushButton(text) if icon is not None: @@ -20,8 +21,9 @@ def newButton(text, icon=None, slot=None): b.clicked.connect(slot) return b + def newAction(parent, text, slot=None, shortcut=None, icon=None, - tip=None, checkable=False, enabled=True): + tip=None, checkable=False, enabled=True): """Create a new action and assign callbacks, shortcuts, etc.""" a = QAction(text, parent) if icon is not None: @@ -51,18 +53,21 @@ def addActions(widget, actions): else: widget.addAction(action) + def labelValidator(): return QRegExpValidator(QRegExp(r'^[^ \t].+'), None) class struct(object): + def __init__(self, **kwargs): self.__dict__.update(kwargs) + def distance(p): return sqrt(p.x() * p.x() + p.y() * p.y()) + def fmtShortcut(text): mod, key = text.split('+', 1) return '%s+%s' % (mod, key) - diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index 940d9ad4..3999661c 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -9,6 +9,7 @@ import codecs XML_EXT = '.xml' + class PascalVocWriter: def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localImgPath=None): @@ -102,7 +103,8 @@ class PascalVocWriter: self.appendObjects(root) out_file = None if targetFile is None: - out_file = codecs.open(self.filename + XML_EXT, 'w', encoding='utf-8') + out_file = codecs.open( + self.filename + XML_EXT, 'w', encoding='utf-8') else: out_file = codecs.open(targetFile, 'w', encoding='utf-8') diff --git a/libs/shape.py b/libs/shape.py index 317fd878..3bf5c1e8 100644 --- a/libs/shape.py +++ b/libs/shape.py @@ -18,13 +18,14 @@ DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155) DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255) DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0) + class Shape(object): P_SQUARE, P_ROUND = range(2) MOVE_VERTEX, NEAR_VERTEX = range(2) - ## The following class variables influence the drawing - ## of _all_ shape objects. + # The following class variables influence the drawing + # of _all_ shape objects. line_color = DEFAULT_LINE_COLOR fill_color = DEFAULT_FILL_COLOR select_line_color = DEFAULT_SELECT_LINE_COLOR @@ -46,7 +47,7 @@ class Shape(object): self._highlightSettings = { self.NEAR_VERTEX: (4, self.P_ROUND), self.MOVE_VERTEX: (1.5, self.P_SQUARE), - } + } self._closed = False @@ -61,7 +62,7 @@ class Shape(object): self._closed = True def reachMaxPoints(self): - if len(self.points) >=4: + if len(self.points) >= 4: return True return False @@ -124,9 +125,9 @@ class Shape(object): else: self.vertex_fill_color = Shape.vertex_fill_color if shape == self.P_SQUARE: - path.addRect(point.x() - d/2, point.y() - d/2, d, d) + path.addRect(point.x() - d / 2, point.y() - d / 2, d, d) elif shape == self.P_ROUND: - path.addEllipse(point, d/2.0, d/2.0) + path.addEllipse(point, d / 2.0, d / 2.0) else: assert False, "unsupported vertex shape" @@ -162,8 +163,8 @@ class Shape(object): self._highlightIndex = None def copy(self): - shape = Shape("Copy of %s" % self.label ) - shape.points= [p for p in self.points] + shape = Shape("Copy of %s" % self.label) + shape.points = [p for p in self.points] shape.fill = self.fill shape.selected = self.selected shape._closed = self._closed @@ -181,4 +182,3 @@ class Shape(object): def __setitem__(self, key, value): self.points[key] = value - diff --git a/libs/toolBar.py b/libs/toolBar.py index 357f8e36..b11887ef 100644 --- a/libs/toolBar.py +++ b/libs/toolBar.py @@ -8,6 +8,7 @@ except ImportError: class ToolBar(QToolBar): + def __init__(self, title): super(ToolBar, self).__init__(title) layout = self.layout() @@ -29,10 +30,10 @@ class ToolBar(QToolBar): class ToolButton(QToolButton): """ToolBar companion class which ensures all buttons have the same size.""" minSize = (60, 60) + def minimumSizeHint(self): ms = super(ToolButton, self).minimumSizeHint() w1, h1 = ms.width(), ms.height() w2, h2 = self.minSize ToolButton.minSize = max(w1, w2), max(h1, h2) return QSize(*ToolButton.minSize) - diff --git a/libs/zoomWidget.py b/libs/zoomWidget.py index 17c5b9f5..d33de0d7 100644 --- a/libs/zoomWidget.py +++ b/libs/zoomWidget.py @@ -6,7 +6,9 @@ except ImportError: from PyQt4.QtGui import * from PyQt4.QtCore import * + class ZoomWidget(QSpinBox): + def __init__(self, value=100): super(ZoomWidget, self).__init__() self.setButtonSymbols(QAbstractSpinBox.NoButtons) @@ -22,4 +24,3 @@ class ZoomWidget(QSpinBox): fm = QFontMetrics(self.font()) width = fm.width(str(self.maximum())) return QSize(width, height) - diff --git a/tests/test.py b/tests/test.py index a9d64d43..869094de 100644 --- a/tests/test.py +++ b/tests/test.py @@ -18,4 +18,3 @@ class TestMainWindow(TestCase): def test_noop(self): pass - From 77d5fae05f286d2b12a2bc4c7c93557b250ac855 Mon Sep 17 00:00:00 2001 From: Thibaut Mattio Date: Tue, 28 Feb 2017 13:18:26 +0800 Subject: [PATCH 2/4] Save an annotation file when there is no ROI --- labelImg.py | 37 +++++++++++++------------------------ libs/labelFile.py | 5 +---- libs/pascal_voc_io.py | 3 +-- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/labelImg.py b/labelImg.py index d9df7717..a00e63a6 100755 --- a/labelImg.py +++ b/labelImg.py @@ -986,7 +986,7 @@ class MainWindow(QMainWindow, WindowMixin): def openNextImg(self, _value=False): # Proceding next image without dialog if having any label if self.autoSaving is True and self.defaultSaveDir is not None: - if self.dirty is True and self.hasLabels(): + if self.dirty is True: self.saveFile() if not self.mayContinue(): @@ -1021,24 +1021,21 @@ class MainWindow(QMainWindow, WindowMixin): self.loadFile(filename) def saveFile(self, _value=False): - assert not self.image.isNull(), "cannot save empty image" - if self.hasLabels(): - if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): - # print('handle the image:' + self.filePath) - imgFileName = os.path.basename(self.filePath) - savedFileName = os.path.splitext( - imgFileName)[0] + LabelFile.suffix - savedPath = os.path.join( - str(self.defaultSaveDir), savedFileName) - self._saveFile(savedPath) - else: - self._saveFile(self.filePath if self.labelFile - else self.saveFileDialog()) + if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): + # print('handle the image:' + self.filePath) + imgFileName = os.path.basename(self.filePath) + savedFileName = os.path.splitext( + imgFileName)[0] + LabelFile.suffix + savedPath = os.path.join( + str(self.defaultSaveDir), savedFileName) + self._saveFile(savedPath) + else: + self._saveFile(self.filePath if self.labelFile + else self.saveFileDialog()) def saveFileAs(self, _value=False): assert not self.image.isNull(), "cannot save empty image" - if self.hasLabels(): - self._saveFile(self.saveFileDialog()) + self._saveFile(self.saveFileDialog()) def saveFileDialog(self): caption = '%s - Choose File' % __appname__ @@ -1069,14 +1066,6 @@ class MainWindow(QMainWindow, WindowMixin): self.canvas.setEnabled(False) self.actions.saveAs.setEnabled(False) - # Message Dialogs. # - def hasLabels(self): - if not self.itemsToShapes: - self.errorMessage(u'No objects labeled', - u'You must label at least one object to save the file.') - return False - return True - def mayContinue(self): return not (self.dirty and not self.discardChangesDialog()) diff --git a/libs/labelFile.py b/libs/labelFile.py index 6e20891c..2847a3b1 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -41,16 +41,13 @@ class LabelFile(object): 1 if image.isGrayscale() else 3] writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt, imageShape, localImgPath=imagePath) - bSave = False for shape in shapes: points = shape['points'] label = shape['label'] bndbox = LabelFile.convertPoints2BndBox(points) writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label) - bSave = True - if bSave: - writer.save(targetFile=filename) + writer.save(targetFile=filename) return @staticmethod diff --git a/libs/pascal_voc_io.py b/libs/pascal_voc_io.py index 3999661c..ec7d02dd 100644 --- a/libs/pascal_voc_io.py +++ b/libs/pascal_voc_io.py @@ -35,8 +35,7 @@ class PascalVocWriter: # Check conditions if self.filename is None or \ self.foldername is None or \ - self.imgSize is None or \ - len(self.boxlist) <= 0: + self.imgSize is None: return None top = Element('annotation') From 1f01cf9074aaf85a383c5039572a1e80834e1d3a Mon Sep 17 00:00:00 2001 From: Thibaut Mattio Date: Tue, 28 Feb 2017 13:29:38 +0800 Subject: [PATCH 3/4] Remove confirmation when deleting a ROI --- labelImg.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/labelImg.py b/labelImg.py index a00e63a6..d7b0243e 100755 --- a/labelImg.py +++ b/labelImg.py @@ -1101,14 +1101,11 @@ class MainWindow(QMainWindow, WindowMixin): self.setDirty() def deleteSelectedShape(self): - yes, no = QMessageBox.Yes, QMessageBox.No - msg = u'You are about to permanently delete this Box, 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) + 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', From 6600f9fe303e24d5135257e02cca2680d211730f Mon Sep 17 00:00:00 2001 From: Thibaut Mattio Date: Tue, 28 Feb 2017 13:29:56 +0800 Subject: [PATCH 4/4] Annotate on release mouse --- libs/canvas.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/libs/canvas.py b/libs/canvas.py index 592165a7..95f6d912 100644 --- a/libs/canvas.py +++ b/libs/canvas.py @@ -225,6 +225,29 @@ class Canvas(QWidget): self.repaint() elif ev.button() == Qt.LeftButton and self.selectedShape: self.overrideCursor(CURSOR_GRAB) + elif ev.button() == Qt.LeftButton: + if self.drawing(): + if self.current and self.current.reachMaxPoints() is False: + initPos = self.current[0] + minX = initPos.x() + minY = initPos.y() + targetPos = self.line[1] + maxX = targetPos.x() + maxY = targetPos.y() + self.current.addPoint(QPointF(maxX, minY)) + self.current.addPoint(targetPos) + self.current.addPoint(QPointF(minX, maxY)) + self.current.addPoint(initPos) + self.line[0] = self.current[-1] + if self.current.isClosed(): + self.finalise() + elif not self.outOfPixmap(pos): + self.current = Shape() + self.current.addPoint(pos) + self.line.points = [pos, pos] + self.setHiding() + self.drawingPolygon.emit(True) + self.update() def endMove(self, copy=False): assert self.selectedShape and self.selectedShapeCopy