Apply PEP recommendation on formatting

Running pylint to make the code complient with PEP recommendations on lintage.
This commit is contained in:
Thibaut Mattio 2017-02-28 11:50:15 +08:00
parent eb87c3d680
commit c5c2a34a39
12 changed files with 237 additions and 193 deletions

View File

@ -1,6 +1,7 @@
"""Set up paths""" """Set up paths"""
import sys import sys
def add_path(path): def add_path(path):
if path not in sys.path: if path not in sys.path:
sys.path.insert(0, path) sys.path.insert(0, path)

View File

@ -17,7 +17,8 @@ try:
except ImportError: except ImportError:
# needed for py3+qt4 # needed for py3+qt4
# ref: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html # 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: if sys.version_info.major >= 3:
import sip import sip
sip.setapi('QVariant', 2) sip.setapi('QVariant', 2)
@ -39,7 +40,8 @@ from pascal_voc_io import XML_EXT
__appname__ = 'labelImg' __appname__ = 'labelImg'
### Utility functions and classes. # Utility functions and classes.
def u(x): def u(x):
'''py2/py3 unicode helper''' '''py2/py3 unicode helper'''
@ -52,6 +54,7 @@ def u(x):
else: else:
return x # py3 return x # py3
def have_qstring(): def have_qstring():
'''p3/qt5 get rid of QString wrapper as py3 has native unicode str type''' '''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.')) return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.'))
@ -62,6 +65,7 @@ def util_qt_strlistclass():
class WindowMixin(object): class WindowMixin(object):
def menu(self, title, actions=None): def menu(self, title, actions=None):
menu = self.menuBar().addMenu(title) menu = self.menuBar().addMenu(title)
if actions: if actions:
@ -71,7 +75,7 @@ class WindowMixin(object):
def toolbar(self, title, actions=None): def toolbar(self, title, actions=None):
toolbar = ToolBar(title) toolbar = ToolBar(title)
toolbar.setObjectName(u'%sToolBar' % title) toolbar.setObjectName(u'%sToolBar' % title)
#toolbar.setOrientation(Qt.Vertical) # toolbar.setOrientation(Qt.Vertical)
toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
if actions: if actions:
addActions(toolbar, actions) addActions(toolbar, actions)
@ -81,8 +85,10 @@ class WindowMixin(object):
# PyQt5: TypeError: unhashable type: 'QListWidgetItem' # PyQt5: TypeError: unhashable type: 'QListWidgetItem'
class HashableQListWidgetItem(QListWidgetItem): class HashableQListWidgetItem(QListWidgetItem):
def __init__(self, *args): def __init__(self, *args):
super(HashableQListWidgetItem, self).__init__(*args) super(HashableQListWidgetItem, self).__init__(*args)
def __hash__(self): def __hash__(self):
return hash(id(self)) return hash(id(self))
@ -135,17 +141,17 @@ class MainWindow(QMainWindow, WindowMixin):
self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.labelListContainer = QWidget() self.labelListContainer = QWidget()
self.labelListContainer.setLayout(listLayout) self.labelListContainer.setLayout(listLayout)
listLayout.addWidget(self.editButton)#, 0, Qt.AlignCenter) listLayout.addWidget(self.editButton) # , 0, Qt.AlignCenter)
listLayout.addWidget(self.labelList) listLayout.addWidget(self.labelList)
self.dock = QDockWidget(u'Box Labels', self) self.dock = QDockWidget(u'Box Labels', self)
self.dock.setObjectName(u'Labels') self.dock.setObjectName(u'Labels')
self.dock.setWidget(self.labelListContainer) self.dock.setWidget(self.labelListContainer)
# Tzutalin 20160906 : Add file list and dock to move faster # Tzutalin 20160906 : Add file list and dock to move faster
self.fileListWidget = QListWidget() self.fileListWidget = QListWidget()
self.fileListWidget.itemDoubleClicked.connect(self.fileitemDoubleClicked) self.fileListWidget.itemDoubleClicked.connect(
self.fileitemDoubleClicked)
filelistLayout = QVBoxLayout() filelistLayout = QVBoxLayout()
filelistLayout.setContentsMargins(0, 0, 0, 0) filelistLayout.setContentsMargins(0, 0, 0, 0)
filelistLayout.addWidget(self.fileListWidget) filelistLayout.addWidget(self.fileListWidget)
@ -167,7 +173,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.scrollBars = { self.scrollBars = {
Qt.Vertical: scroll.verticalScrollBar(), Qt.Vertical: scroll.verticalScrollBar(),
Qt.Horizontal: scroll.horizontalScrollBar() Qt.Horizontal: scroll.horizontalScrollBar()
} }
self.canvas.scrollRequest.connect(self.scrollRequest) self.canvas.scrollRequest.connect(self.scrollRequest)
self.canvas.newShape.connect(self.newShape) self.canvas.newShape.connect(self.newShape)
@ -180,93 +186,94 @@ class MainWindow(QMainWindow, WindowMixin):
# Tzutalin 20160906 : Add file list and dock to move faster # Tzutalin 20160906 : Add file list and dock to move faster
self.addDockWidget(Qt.RightDockWidgetArea, self.filedock) self.addDockWidget(Qt.RightDockWidgetArea, self.filedock)
self.dockFeatures = QDockWidget.DockWidgetClosable\ self.dockFeatures = QDockWidget.DockWidgetClosable\
| QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetFloatable
self.dock.setFeatures(self.dock.features() ^ self.dockFeatures) self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)
# Actions # Actions
action = partial(newAction, self) action = partial(newAction, self)
quit = action('&Quit', self.close, quit = action('&Quit', self.close,
'Ctrl+Q', 'quit', u'Quit application') 'Ctrl+Q', 'quit', u'Quit application')
open = action('&Open', self.openFile, 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, 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, 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, openAnnotation = action('&Open Annotation', self.openAnnotation,
'Ctrl+q', 'openAnnotation', u'Open Annotation') 'Ctrl+q', 'openAnnotation', u'Open Annotation')
openNextImg = action('&Next Image', self.openNextImg, openNextImg = action('&Next Image', self.openNextImg,
'd', 'next', u'Open Next') 'd', 'next', u'Open Next')
openPrevImg = action('&Prev Image', self.openPrevImg, openPrevImg = action('&Prev Image', self.openPrevImg,
'a', 'prev', u'Open Prev') 'a', 'prev', u'Open Prev')
save = action('&Save', self.saveFile, 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, saveAs = action('&Save As', self.saveFileAs,
'Ctrl+Shift+S', 'save-as', u'Save labels to a different file', 'Ctrl+Shift+S', 'save-as', u'Save labels to a different file',
enabled=False) enabled=False)
close = action('&Close', self.closeFile, 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, 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, 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, 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, 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, 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 = action('Delete\nRectBox', self.deleteSelectedShape,
'Delete', 'delete', u'Delete', enabled=False) 'Delete', 'delete', u'Delete', enabled=False)
copy = action('&Duplicate\nRectBox', self.copySelectedShape, copy = action('&Duplicate\nRectBox', self.copySelectedShape,
'Ctrl+D', 'copy', u'Create a duplicate of the selected Box', 'Ctrl+D', 'copy', u'Create a duplicate of the selected Box',
enabled=False) enabled=False)
advancedMode = action('&Advanced Mode', self.toggleAdvancedMode, advancedMode = action('&Advanced Mode', self.toggleAdvancedMode,
'Ctrl+Shift+A', 'expert', u'Switch to advanced mode', 'Ctrl+Shift+A', 'expert', u'Switch to advanced mode',
checkable=True) checkable=True)
hideAll = action('&Hide\nRectBox', partial(self.togglePolygons, False), hideAll = action('&Hide\nRectBox', partial(self.togglePolygons, False),
'Ctrl+H', 'hide', u'Hide all Boxs', 'Ctrl+H', 'hide', u'Hide all Boxs',
enabled=False) enabled=False)
showAll = action('&Show\nRectBox', partial(self.togglePolygons, True), showAll = action('&Show\nRectBox', partial(self.togglePolygons, True),
'Ctrl+A', 'hide', u'Show all Boxs', 'Ctrl+A', 'hide', u'Show all Boxs',
enabled=False) enabled=False)
help = action('&Tutorial', self.tutorial, 'Ctrl+T', 'help', help = action('&Tutorial', self.tutorial, 'Ctrl+T', 'help',
u'Show demos') u'Show demos')
zoom = QWidgetAction(self) zoom = QWidgetAction(self)
zoom.setDefaultWidget(self.zoomWidget) zoom.setDefaultWidget(self.zoomWidget)
self.zoomWidget.setWhatsThis( self.zoomWidget.setWhatsThis(
u"Zoom in or out of the image. Also accessible with"\ u"Zoom in or out of the image. Also accessible with"
" %s and %s from the canvas." % (fmtShortcut("Ctrl+[-+]"), " %s and %s from the canvas." % (fmtShortcut("Ctrl+[-+]"),
fmtShortcut("Ctrl+Wheel"))) fmtShortcut("Ctrl+Wheel")))
self.zoomWidget.setEnabled(False) self.zoomWidget.setEnabled(False)
zoomIn = action('Zoom &In', partial(self.addZoom, 10), 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), 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), 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, fitWindow = action('&Fit Window', self.setFitWindow,
'Ctrl+F', 'fit-window', u'Zoom follows window size', 'Ctrl+F', 'fit-window', u'Zoom follows window size',
checkable=True, enabled=False) checkable=True, enabled=False)
fitWidth = action('Fit &Width', self.setFitWidth, fitWidth = action('Fit &Width', self.setFitWidth,
'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width', 'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width',
checkable=True, enabled=False) checkable=True, enabled=False)
# Group zoom controls into a list for easier toggling. # 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.zoomMode = self.MANUAL_ZOOM
self.scalers = { self.scalers = {
self.FIT_WINDOW: self.scaleFitWindow, self.FIT_WINDOW: self.scaleFitWindow,
@ -276,16 +283,16 @@ class MainWindow(QMainWindow, WindowMixin):
} }
edit = action('&Edit Label', self.editLabel, edit = action('&Edit Label', self.editLabel,
'Ctrl+E', 'edit', u'Modify the label of the selected Box', 'Ctrl+E', 'edit', u'Modify the label of the selected Box',
enabled=False) enabled=False)
self.editButton.setDefaultAction(edit) self.editButton.setDefaultAction(edit)
shapeLineColor = action('Shape &Line Color', self.chshapeLineColor, shapeLineColor = action('Shape &Line Color', self.chshapeLineColor,
icon='color_line', tip=u'Change the line color for this specific shape', icon='color_line', tip=u'Change the line color for this specific shape',
enabled=False) enabled=False)
shapeFillColor = action('Shape &Fill Color', self.chshapeFillColor, shapeFillColor = action('Shape &Fill Color', self.chshapeFillColor,
icon='color', tip=u'Change the fill color for this specific shape', icon='color', tip=u'Change the fill color for this specific shape',
enabled=False) enabled=False)
labels = self.dock.toggleViewAction() labels = self.dock.toggleViewAction()
labels.setText('Show/Hide Label Panel') labels.setText('Show/Hide Label Panel')
@ -295,36 +302,40 @@ class MainWindow(QMainWindow, WindowMixin):
labelMenu = QMenu() labelMenu = QMenu()
addActions(labelMenu, (edit, delete)) addActions(labelMenu, (edit, delete))
self.labelList.setContextMenuPolicy(Qt.CustomContextMenu) self.labelList.setContextMenuPolicy(Qt.CustomContextMenu)
self.labelList.customContextMenuRequested.connect(self.popLabelListMenu) self.labelList.customContextMenuRequested.connect(
self.popLabelListMenu)
# Store actions for further handling. # Store actions for further handling.
self.actions = struct(save=save, saveAs=saveAs, open=open, close=close, self.actions = struct(save=save, saveAs=saveAs, open=open, close=close,
lineColor=color1, fillColor=color2, lineColor=color1, fillColor=color2,
create=create, delete=delete, edit=edit, copy=copy, create=create, delete=delete, edit=edit, copy=copy,
createMode=createMode, editMode=editMode, advancedMode=advancedMode, createMode=createMode, editMode=editMode, advancedMode=advancedMode,
shapeLineColor=shapeLineColor, shapeFillColor=shapeFillColor, shapeLineColor=shapeLineColor, shapeFillColor=shapeFillColor,
zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg, zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg,
fitWindow=fitWindow, fitWidth=fitWidth, fitWindow=fitWindow, fitWidth=fitWidth,
zoomActions=zoomActions, zoomActions=zoomActions,
fileMenuActions=(open,opendir,save,saveAs,close,quit), fileMenuActions=(
beginner=(), advanced=(), open, opendir, save, saveAs, close, quit),
editMenu=(edit, copy, delete, None, color1, color2), beginner=(), advanced=(),
beginnerContext=(create, edit, copy, delete), editMenu=(edit, copy, delete,
advancedContext=(createMode, editMode, edit, copy, None, color1, color2),
delete, shapeLineColor, shapeFillColor), beginnerContext=(create, edit, copy, delete),
onLoadActive=(close, create, createMode, editMode), advancedContext=(createMode, editMode, edit, copy,
onShapesPresent=(saveAs, hideAll, showAll)) delete, shapeLineColor, shapeFillColor),
onLoadActive=(
close, create, createMode, editMode),
onShapesPresent=(saveAs, hideAll, showAll))
self.menus = struct( self.menus = struct(
file=self.menu('&File'), file=self.menu('&File'),
edit=self.menu('&Edit'), edit=self.menu('&Edit'),
view=self.menu('&View'), view=self.menu('&View'),
help=self.menu('&Help'), help=self.menu('&Help'),
recentFiles=QMenu('Open &Recent'), recentFiles=QMenu('Open &Recent'),
labelList=labelMenu) labelList=labelMenu)
addActions(self.menus.file, 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.help, (help,))
addActions(self.menus.view, ( addActions(self.menus.view, (
labels, advancedMode, None, labels, advancedMode, None,
@ -406,11 +417,12 @@ class MainWindow(QMainWindow, WindowMixin):
self.lastOpenDir = u(settings.get('lastOpenDir', None)) self.lastOpenDir = u(settings.get('lastOpenDir', None))
if os.path.exists(saveDir): if os.path.exists(saveDir):
self.defaultSaveDir = 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() self.statusBar().show()
# or simply: # or simply:
#self.restoreGeometry(settings['window/geometry'] # self.restoreGeometry(settings['window/geometry']
self.restoreState(settings.get('window/state', QByteArray())) self.restoreState(settings.get('window/state', QByteArray()))
self.lineColor = QColor(settings.get('line/color', Shape.line_color)) self.lineColor = QColor(settings.get('line/color', Shape.line_color))
self.fillColor = QColor(settings.get('fill/color', Shape.fill_color)) self.fillColor = QColor(settings.get('fill/color', Shape.fill_color))
@ -428,7 +440,8 @@ class MainWindow(QMainWindow, WindowMixin):
# Populate the File menu dynamically. # Populate the File menu dynamically.
self.updateFileMenu() 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)) self.queueEvent(partial(self.loadFile, self.filePath))
# Callbacks: # Callbacks:
@ -436,7 +449,6 @@ class MainWindow(QMainWindow, WindowMixin):
self.populateModeActions() self.populateModeActions()
## Support Functions ## ## Support Functions ##
def noShapes(self): def noShapes(self):
@ -465,7 +477,7 @@ class MainWindow(QMainWindow, WindowMixin):
addActions(self.canvas.menus[0], menu) addActions(self.canvas.menus[0], menu)
self.menus.edit.clear() self.menus.edit.clear()
actions = (self.actions.create,) if self.beginner()\ 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) addActions(self.menus.edit, actions + self.actions.editMenu)
def setBeginner(self): def setBeginner(self):
@ -560,15 +572,17 @@ class MainWindow(QMainWindow, WindowMixin):
def updateFileMenu(self): def updateFileMenu(self):
currFilePath = self.filePath currFilePath = self.filePath
def exists(filename): def exists(filename):
return os.path.exists(filename) return os.path.exists(filename)
menu = self.menus.recentFiles menu = self.menus.recentFiles
menu.clear() 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): for i, f in enumerate(files):
icon = newIcon('labels') icon = newIcon('labels')
action = QAction( 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)) action.triggered.connect(partial(self.loadRecent, f))
menu.addAction(action) menu.addAction(action)
@ -587,7 +601,7 @@ class MainWindow(QMainWindow, WindowMixin):
# Tzutalin 20160906 : Add file list and dock to move faster # Tzutalin 20160906 : Add file list and dock to move faster
def fileitemDoubleClicked(self, item=None): def fileitemDoubleClicked(self, item=None):
currIndex = self.mImgList.index(u(item.text())) currIndex = self.mImgList.index(u(item.text()))
if currIndex < len(self.mImgList): if currIndex < len(self.mImgList):
filename = self.mImgList[currIndex] filename = self.mImgList[currIndex]
if filename: if filename:
self.loadFile(filename) self.loadFile(filename)
@ -642,19 +656,21 @@ class MainWindow(QMainWindow, WindowMixin):
def saveLabels(self, annotationFilePath): def saveLabels(self, annotationFilePath):
annotationFilePath = u(annotationFilePath) annotationFilePath = u(annotationFilePath)
lf = LabelFile() lf = LabelFile()
def format_shape(s): def format_shape(s):
return dict(label=s.label, return dict(label=s.label,
line_color=s.line_color.getRgb()\ line_color=s.line_color.getRgb()
if s.line_color != self.lineColor else None, if s.line_color != self.lineColor else None,
fill_color=s.fill_color.getRgb()\ fill_color=s.fill_color.getRgb()
if s.fill_color != self.fillColor else None, if s.fill_color != self.fillColor else None,
points=[(p.x(), p.y()) for p in s.points]) points=[(p.x(), p.y()) for p in s.points])
shapes = [format_shape(shape) for shape in self.canvas.shapes] shapes = [format_shape(shape) for shape in self.canvas.shapes]
# Can add differrent annotation formats here # Can add differrent annotation formats here
try: try:
if self.usingPascalVocFormat is True: 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, lf.savePascalVocFormat(annotationFilePath, shapes, self.filePath, self.imageData,
self.lineColor.getRgb(), self.fillColor.getRgb()) self.lineColor.getRgb(), self.fillColor.getRgb())
else: else:
@ -664,12 +680,12 @@ class MainWindow(QMainWindow, WindowMixin):
return True return True
except LabelFileError as e: except LabelFileError as e:
self.errorMessage(u'Error saving label data', self.errorMessage(u'Error saving label data',
u'<b>%s</b>' % e) u'<b>%s</b>' % e)
return False return False
def copySelectedShape(self): def copySelectedShape(self):
self.addLabel(self.canvas.copySelectedShape()) self.addLabel(self.canvas.copySelectedShape())
#fix copy and delete # fix copy and delete
self.shapeSelectionChanged(True) self.shapeSelectionChanged(True)
def labelSelectionChanged(self): def labelSelectionChanged(self):
@ -684,34 +700,34 @@ class MainWindow(QMainWindow, WindowMixin):
if label != shape.label: if label != shape.label:
shape.label = item.text() shape.label = item.text()
self.setDirty() self.setDirty()
else: # User probably changed item visibility else: # User probably changed item visibility
self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
## Callback functions: # Callback functions:
def newShape(self): def newShape(self):
"""Pop-up and give focus to the label editor. """Pop-up and give focus to the label editor.
position MUST be in global coordinates. position MUST be in global coordinates.
""" """
if len(self.labelHist) > 0: 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) text = self.labelDialog.popUp(text=self.prevLabelText)
if text is not None: if text is not None:
self.prevLabelText = text self.prevLabelText = text
self.addLabel(self.canvas.setLastLabel(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.canvas.setEditing(True)
self.actions.create.setEnabled(True) self.actions.create.setEnabled(True)
else: else:
self.actions.editMode.setEnabled(True) self.actions.editMode.setEnabled(True)
self.setDirty() self.setDirty()
if text not in self.labelHist: if text not in self.labelHist:
self.labelHist.append(text) self.labelHist.append(text)
else: else:
#self.canvas.undoLastLine() # self.canvas.undoLastLine()
self.canvas.resetAllLines() self.canvas.resetAllLines()
def scrollRequest(self, delta, orientation): def scrollRequest(self, delta, orientation):
@ -771,7 +787,7 @@ class MainWindow(QMainWindow, WindowMixin):
except LabelFileError as e: except LabelFileError as e:
self.errorMessage(u'Error opening file', self.errorMessage(u'Error opening file',
(u"<p><b>%s</b></p>" (u"<p><b>%s</b></p>"
u"<p>Make sure <i>%s</i> is a valid label file.") \ u"<p>Make sure <i>%s</i> is a valid label file.")
% (e, unicodeFilePath)) % (e, unicodeFilePath))
self.status("Error reading %s" % unicodeFilePath) self.status("Error reading %s" % unicodeFilePath)
return False return False
@ -802,10 +818,11 @@ class MainWindow(QMainWindow, WindowMixin):
self.addRecentFile(self.filePath) self.addRecentFile(self.filePath)
self.toggleActions(True) 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 \ if self.usingPascalVocFormat is True and \
self.defaultSaveDir is not None: self.defaultSaveDir is not None:
basename = os.path.basename(os.path.splitext(self.filePath)[0]) + XML_EXT basename = os.path.basename(
os.path.splitext(self.filePath)[0]) + XML_EXT
xmlPath = os.path.join(self.defaultSaveDir, basename) xmlPath = os.path.join(self.defaultSaveDir, basename)
self.loadPascalXMLByFilename(xmlPath) self.loadPascalXMLByFilename(xmlPath)
@ -831,10 +848,10 @@ class MainWindow(QMainWindow, WindowMixin):
def scaleFitWindow(self): def scaleFitWindow(self):
"""Figure out the size of the pixmap in order to fit the main widget.""" """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 w1 = self.centralWidget().width() - e
h1 = self.centralWidget().height() - e h1 = self.centralWidget().height() - e
a1 = w1/ h1 a1 = w1 / h1
# Calculate a new scale value based on the pixmap's aspect ratio. # Calculate a new scale value based on the pixmap's aspect ratio.
w2 = self.canvas.pixmap.width() - 0.0 w2 = self.canvas.pixmap.width() - 0.0
h2 = self.canvas.pixmap.height() - 0.0 h2 = self.canvas.pixmap.height() - 0.0
@ -880,7 +897,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.loadFile(filename) self.loadFile(filename)
def scanAllImages(self, folderPath): def scanAllImages(self, folderPath):
extensions = ['.jpeg','.jpg', '.png', '.bmp'] extensions = ['.jpeg', '.jpg', '.png', '.bmp']
images = [] images = []
for root, dirs, files in os.walk(folderPath): for root, dirs, files in os.walk(folderPath):
@ -899,13 +916,14 @@ class MainWindow(QMainWindow, WindowMixin):
path = '.' path = '.'
dirpath = str(QFileDialog.getExistingDirectory(self, dirpath = str(QFileDialog.getExistingDirectory(self,
'%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly '%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly
| QFileDialog.DontResolveSymlinks)) | QFileDialog.DontResolveSymlinks))
if dirpath is not None and len(dirpath) > 1: if dirpath is not None and len(dirpath) > 1:
self.defaultSaveDir = dirpath 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() self.statusBar().show()
def openAnnotation(self, _value=False): def openAnnotation(self, _value=False):
@ -913,14 +931,14 @@ class MainWindow(QMainWindow, WindowMixin):
return return
path = os.path.dirname(u(self.filePath))\ path = os.path.dirname(u(self.filePath))\
if self.filePath else '.' if self.filePath else '.'
if self.usingPascalVocFormat: if self.usingPascalVocFormat:
formats = ['*.%s' % str(fmt).lower()\ formats = ['*.%s' % str(fmt).lower()
for fmt in QImageReader.supportedImageFormats()] for fmt in QImageReader.supportedImageFormats()]
filters = "Open Annotation XML file (%s)" % \ filters = "Open Annotation XML file (%s)" % \
' '.join(formats + ['*.xml']) ' '.join(formats + ['*.xml'])
filename = str(QFileDialog.getOpenFileName(self, filename = str(QFileDialog.getOpenFileName(self,
'%s - Choose a xml file' % __appname__, path, filters)) '%s - Choose a xml file' % __appname__, path, filters))
self.loadPascalXMLByFilename(filename) self.loadPascalXMLByFilename(filename)
def openDir(self, _value=False): def openDir(self, _value=False):
@ -928,14 +946,14 @@ class MainWindow(QMainWindow, WindowMixin):
return return
path = os.path.dirname(self.filePath)\ 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: if self.lastOpenDir is not None and len(self.lastOpenDir) > 1:
path = self.lastOpenDir path = self.lastOpenDir
dirpath = u(QFileDialog.getExistingDirectory(self, dirpath = u(QFileDialog.getExistingDirectory(self,
'%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly '%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly
| QFileDialog.DontResolveSymlinks)) | QFileDialog.DontResolveSymlinks))
if dirpath is not None and len(dirpath) > 1: if dirpath is not None and len(dirpath) > 1:
self.lastOpenDir = dirpath self.lastOpenDir = dirpath
@ -960,8 +978,8 @@ class MainWindow(QMainWindow, WindowMixin):
return return
currIndex = self.mImgList.index(self.filePath) currIndex = self.mImgList.index(self.filePath)
if currIndex -1 >= 0: if currIndex - 1 >= 0:
filename = self.mImgList[currIndex-1] filename = self.mImgList[currIndex - 1]
if filename: if filename:
self.loadFile(filename) self.loadFile(filename)
@ -983,7 +1001,7 @@ class MainWindow(QMainWindow, WindowMixin):
else: else:
currIndex = self.mImgList.index(self.filePath) currIndex = self.mImgList.index(self.filePath)
if currIndex + 1 < len(self.mImgList): if currIndex + 1 < len(self.mImgList):
filename = self.mImgList[currIndex+1] filename = self.mImgList[currIndex + 1]
if filename: if filename:
self.loadFile(filename) self.loadFile(filename)
@ -992,13 +1010,13 @@ class MainWindow(QMainWindow, WindowMixin):
if not self.mayContinue(): if not self.mayContinue():
return return
path = os.path.dirname(str(self.filePath))\ path = os.path.dirname(str(self.filePath))\
if self.filePath else '.' if self.filePath else '.'
formats = ['*.%s' % str(fmt).lower()\ formats = ['*.%s' % str(fmt).lower()
for fmt in QImageReader.supportedImageFormats()] for fmt in QImageReader.supportedImageFormats()]
filters = "Image & Label files (%s)" % \ filters = "Image & Label files (%s)" % \
' '.join(formats + ['*%s' % LabelFile.suffix]) ' '.join(formats + ['*%s' % LabelFile.suffix])
filename = QFileDialog.getOpenFileName(self, filename = QFileDialog.getOpenFileName(self,
'%s - Choose Image or Label file' % __appname__, path, filters) '%s - Choose Image or Label file' % __appname__, path, filters)
if filename: if filename:
self.loadFile(filename) self.loadFile(filename)
@ -1008,12 +1026,14 @@ class MainWindow(QMainWindow, WindowMixin):
if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)):
# print('handle the image:' + self.filePath) # print('handle the image:' + self.filePath)
imgFileName = os.path.basename(self.filePath) imgFileName = os.path.basename(self.filePath)
savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix savedFileName = os.path.splitext(
savedPath = os.path.join(str(self.defaultSaveDir), savedFileName) imgFileName)[0] + LabelFile.suffix
savedPath = os.path.join(
str(self.defaultSaveDir), savedFileName)
self._saveFile(savedPath) self._saveFile(savedPath)
else: else:
self._saveFile(self.filePath if self.labelFile\ self._saveFile(self.filePath if self.labelFile
else self.saveFileDialog()) else self.saveFileDialog())
def saveFileAs(self, _value=False): def saveFileAs(self, _value=False):
assert not self.image.isNull(), "cannot save empty image" assert not self.image.isNull(), "cannot save empty image"
@ -1024,7 +1044,7 @@ class MainWindow(QMainWindow, WindowMixin):
caption = '%s - Choose File' % __appname__ caption = '%s - Choose File' % __appname__
filters = 'File (*%s)' % LabelFile.suffix filters = 'File (*%s)' % LabelFile.suffix
openDialogPath = self.currentPath() openDialogPath = self.currentPath()
dlg = QFileDialog(self, caption, openDialogPath, filters) dlg = QFileDialog(self, caption, openDialogPath, filters)
dlg.setDefaultSuffix(LabelFile.suffix[1:]) dlg.setDefaultSuffix(LabelFile.suffix[1:])
dlg.setAcceptMode(QFileDialog.AcceptSave) dlg.setAcceptMode(QFileDialog.AcceptSave)
filenameWithoutExtension = os.path.splitext(self.filePath)[0] filenameWithoutExtension = os.path.splitext(self.filePath)[0]
@ -1053,7 +1073,7 @@ class MainWindow(QMainWindow, WindowMixin):
def hasLabels(self): def hasLabels(self):
if not self.itemsToShapes: if not self.itemsToShapes:
self.errorMessage(u'No objects labeled', 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 False
return True return True
@ -1063,18 +1083,18 @@ class MainWindow(QMainWindow, WindowMixin):
def discardChangesDialog(self): def discardChangesDialog(self):
yes, no = QMessageBox.Yes, QMessageBox.No yes, no = QMessageBox.Yes, QMessageBox.No
msg = u'You have unsaved changes, proceed anyway?' 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): def errorMessage(self, title, message):
return QMessageBox.critical(self, title, return QMessageBox.critical(self, title,
'<p><b>%s</b></p>%s' % (title, message)) '<p><b>%s</b></p>%s' % (title, message))
def currentPath(self): def currentPath(self):
return os.path.dirname(self.filePath) if self.filePath else '.' return os.path.dirname(self.filePath) if self.filePath else '.'
def chooseColor1(self): def chooseColor1(self):
color = self.colorDialog.getColor(self.lineColor, u'Choose line color', color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
default=DEFAULT_LINE_COLOR) default=DEFAULT_LINE_COLOR)
if color: if color:
self.lineColor = color self.lineColor = color
# Change the color for all shape lines: # Change the color for all shape lines:
@ -1083,9 +1103,9 @@ class MainWindow(QMainWindow, WindowMixin):
self.setDirty() self.setDirty()
def chooseColor2(self): def chooseColor2(self):
color = self.colorDialog.getColor(self.fillColor, u'Choose fill color', color = self.colorDialog.getColor(self.fillColor, u'Choose fill color',
default=DEFAULT_FILL_COLOR) default=DEFAULT_FILL_COLOR)
if color: if color:
self.fillColor = color self.fillColor = color
Shape.fill_color = self.fillColor Shape.fill_color = self.fillColor
self.canvas.update() self.canvas.update()
@ -1094,7 +1114,7 @@ class MainWindow(QMainWindow, WindowMixin):
def deleteSelectedShape(self): def deleteSelectedShape(self):
yes, no = QMessageBox.Yes, QMessageBox.No yes, no = QMessageBox.Yes, QMessageBox.No
msg = u'You are about to permanently delete this Box, proceed anyway?' 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.remLabel(self.canvas.deleteSelected())
self.setDirty() self.setDirty()
if self.noShapes(): if self.noShapes():
@ -1103,7 +1123,7 @@ class MainWindow(QMainWindow, WindowMixin):
def chshapeLineColor(self): def chshapeLineColor(self):
color = self.colorDialog.getColor(self.lineColor, u'Choose line color', color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
default=DEFAULT_LINE_COLOR) default=DEFAULT_LINE_COLOR)
if color: if color:
self.canvas.selectedShape.line_color = color self.canvas.selectedShape.line_color = color
self.canvas.update() self.canvas.update()
@ -1111,7 +1131,7 @@ class MainWindow(QMainWindow, WindowMixin):
def chshapeFillColor(self): def chshapeFillColor(self):
color = self.colorDialog.getColor(self.fillColor, u'Choose fill color', color = self.colorDialog.getColor(self.fillColor, u'Choose fill color',
default=DEFAULT_FILL_COLOR) default=DEFAULT_FILL_COLOR)
if color: if color:
self.canvas.selectedShape.fill_color = color self.canvas.selectedShape.fill_color = color
self.canvas.update() self.canvas.update()
@ -1127,7 +1147,8 @@ class MainWindow(QMainWindow, WindowMixin):
self.setDirty() self.setDirty()
def loadPredefinedClasses(self): 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: if os.path.exists(predefined_classes_path) is True:
with codecs.open(predefined_classes_path, 'r', 'utf8') as f: with codecs.open(predefined_classes_path, 'r', 'utf8') as f:
for line in f: for line in f:
@ -1147,8 +1168,10 @@ class MainWindow(QMainWindow, WindowMixin):
shapes = tVocParseReader.getShapes() shapes = tVocParseReader.getShapes()
self.loadLabels(shapes) self.loadLabels(shapes)
class Settings(object): class Settings(object):
"""Convenience dict-like wrapper around QSettings.""" """Convenience dict-like wrapper around QSettings."""
def __init__(self, types=None): def __init__(self, types=None):
self.data = QSettings() self.data = QSettings()
self.types = defaultdict(lambda: QVariant, types if types else {}) self.types = defaultdict(lambda: QVariant, types if types else {})
@ -1156,7 +1179,7 @@ class Settings(object):
def __setitem__(self, key, value): def __setitem__(self, key, value):
t = self.types[key] t = self.types[key]
self.data.setValue(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): def __getitem__(self, key):
return self._cast(key, self.data.value(key)) return self._cast(key, self.data.value(key))
@ -1172,10 +1195,11 @@ class Settings(object):
return str(value) return str(value)
else: else:
try: 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) return method(value)
except AttributeError as e: except AttributeError as e:
#print(e) # print(e)
return value return value
return value return value
@ -1183,6 +1207,7 @@ class Settings(object):
def inverted(color): def inverted(color):
return QColor(*[255 - v for v in color.getRgb()]) return QColor(*[255 - v for v in color.getRgb()])
def read(filename, default=None): def read(filename, default=None):
try: try:
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
@ -1190,6 +1215,7 @@ def read(filename, default=None):
except: except:
return default return default
def get_main_app(argv=[]): def get_main_app(argv=[]):
""" """
Standard boilerplate Qt application code. Standard boilerplate Qt application code.
@ -1202,6 +1228,7 @@ def get_main_app(argv=[]):
win.show() win.show()
return app, win return app, win
def main(argv): def main(argv):
'''construct main app and run it''' '''construct main app and run it'''
app, _win = get_main_app(argv) app, _win = get_main_app(argv)

View File

@ -13,12 +13,14 @@ from shape import Shape
from lib import distance from lib import distance
CURSOR_DEFAULT = Qt.ArrowCursor CURSOR_DEFAULT = Qt.ArrowCursor
CURSOR_POINT = Qt.PointingHandCursor CURSOR_POINT = Qt.PointingHandCursor
CURSOR_DRAW = Qt.CrossCursor CURSOR_DRAW = Qt.CrossCursor
CURSOR_MOVE = Qt.ClosedHandCursor CURSOR_MOVE = Qt.ClosedHandCursor
CURSOR_GRAB = Qt.OpenHandCursor CURSOR_GRAB = Qt.OpenHandCursor
# class Canvas(QGLWidget):
#class Canvas(QGLWidget):
class Canvas(QWidget): class Canvas(QWidget):
zoomRequest = pyqtSignal(int) zoomRequest = pyqtSignal(int)
scrollRequest = pyqtSignal(int, int) scrollRequest = pyqtSignal(int, int)
@ -37,8 +39,8 @@ class Canvas(QWidget):
self.mode = self.EDIT self.mode = self.EDIT
self.shapes = [] self.shapes = []
self.current = None self.current = None
self.selectedShape=None # save the selected shape here self.selectedShape = None # save the selected shape here
self.selectedShapeCopy=None self.selectedShapeCopy = None
self.lineColor = QColor(0, 0, 255) self.lineColor = QColor(0, 0, 255)
self.line = Shape(line_color=self.lineColor) self.line = Shape(line_color=self.lineColor)
self.prevPoint = QPointF() self.prevPoint = QPointF()
@ -78,7 +80,7 @@ class Canvas(QWidget):
def setEditing(self, value=True): def setEditing(self, value=True):
self.mode = self.EDIT if value else self.CREATE self.mode = self.EDIT if value else self.CREATE
if not value: # Create if not value: # Create
self.unHighlight() self.unHighlight()
self.deSelectShape() self.deSelectShape()
@ -106,7 +108,8 @@ class Canvas(QWidget):
# Project the point to the pixmap's edges. # Project the point to the pixmap's edges.
pos = self.intersectionPoint(self.current[-1], pos) pos = self.intersectionPoint(self.current[-1], pos)
elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]): 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] pos = self.current[0]
color = self.current.line_color color = self.current.line_color
self.overrideCursor(CURSOR_POINT) self.overrideCursor(CURSOR_POINT)
@ -164,12 +167,13 @@ class Canvas(QWidget):
if self.selectedVertex(): if self.selectedVertex():
self.hShape.highlightClear() self.hShape.highlightClear()
self.hVertex, self.hShape = None, shape 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.setStatusTip(self.toolTip())
self.overrideCursor(CURSOR_GRAB) self.overrideCursor(CURSOR_GRAB)
self.update() self.update()
break break
else: # Nothing found, clear highlights, reset state. else: # Nothing found, clear highlights, reset state.
if self.hShape: if self.hShape:
self.hShape.highlightClear() self.hShape.highlightClear()
self.update() self.update()
@ -270,7 +274,7 @@ class Canvas(QWidget):
def selectShapePoint(self, point): def selectShapePoint(self, point):
"""Select the first shape created which contains this point.""" """Select the first shape created which contains this point."""
self.deSelectShape() 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 index, shape = self.hVertex, self.hShape
shape.highlightVertex(index, shape.MOVE_VERTEX) shape.highlightVertex(index, shape.MOVE_VERTEX)
return return
@ -315,14 +319,14 @@ class Canvas(QWidget):
def boundedMoveShape(self, shape, pos): def boundedMoveShape(self, shape, pos):
if self.outOfPixmap(pos): if self.outOfPixmap(pos):
return False # No need to move return False # No need to move
o1 = pos + self.offsets[0] o1 = pos + self.offsets[0]
if self.outOfPixmap(o1): if self.outOfPixmap(o1):
pos -= QPointF(min(0, o1.x()), min(0, o1.y())) pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
o2 = pos + self.offsets[1] o2 = pos + self.offsets[1]
if self.outOfPixmap(o2): if self.outOfPixmap(o2):
pos += QPointF(min(0, self.pixmap.width() - o2.x()), 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 # The next line tracks the new position of the cursor
# relative to the shape, but also results in making it # relative to the shape, but also results in making it
# a bit "shaky" when nearing the border and allows it to # a bit "shaky" when nearing the border and allows it to
@ -419,8 +423,8 @@ class Canvas(QWidget):
area = super(Canvas, self).size() area = super(Canvas, self).size()
w, h = self.pixmap.width() * s, self.pixmap.height() * s w, h = self.pixmap.width() * s, self.pixmap.height() * s
aw, ah = area.width(), area.height() aw, ah = area.width(), area.height()
x = (aw-w)/(2*s) if aw > w else 0 x = (aw - w) / (2 * s) if aw > w else 0
y = (ah-h)/(2*s) if ah > h else 0 y = (ah - h) / (2 * s) if ah > h else 0
return QPointF(x, y) return QPointF(x, y)
def outOfPixmap(self, p): def outOfPixmap(self, p):
@ -439,7 +443,7 @@ class Canvas(QWidget):
def closeEnough(self, p1, p2): def closeEnough(self, p1, p2):
#d = distance(p1 - p2) #d = distance(p1 - p2)
#m = (p1-p2).manhattanLength() #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 return distance(p1 - p2) < self.epsilon
def intersectionPoint(self, p1, p2): def intersectionPoint(self, p1, p2):
@ -447,7 +451,7 @@ class Canvas(QWidget):
# and find the one intersecting the current line segment. # and find the one intersecting the current line segment.
# http://paulbourke.net/geometry/lineline2d/ # http://paulbourke.net/geometry/lineline2d/
size = self.pixmap.size() size = self.pixmap.size()
points = [(0,0), points = [(0, 0),
(size.width(), 0), (size.width(), 0),
(size.width(), size.height()), (size.width(), size.height()),
(0, size.height())] (0, size.height())]
@ -455,12 +459,12 @@ class Canvas(QWidget):
x2, y2 = p2.x(), p2.y() x2, y2 = p2.x(), p2.y()
d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
x3, y3 = points[i] x3, y3 = points[i]
x4, y4 = points[(i+1)%4] x4, y4 = points[(i + 1) % 4]
if (x, y) == (x1, y1): if (x, y) == (x1, y1):
# Handle cases where previous point is on one of the edges. # Handle cases where previous point is on one of the edges.
if x3 == x4: if x3 == x4:
return QPointF(x3, min(max(0, y2), max(y3, y4))) 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(min(max(0, x2), max(x3, x4)), y3)
return QPointF(x, y) return QPointF(x, y)
@ -473,10 +477,10 @@ class Canvas(QWidget):
x2, y2 = x2y2 x2, y2 = x2y2
for i in range(4): for i in range(4):
x3, y3 = points[i] x3, y3 = points[i]
x4, y4 = points[(i+1) % 4] x4, y4 = points[(i + 1) % 4]
denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
nua = (x4-x3) * (y1-y3) - (y4-y3) * (x1-x3) nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
nub = (x2-x1) * (y1-y3) - (y2-y1) * (x1-x3) nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
if denom == 0: if denom == 0:
# This covers two cases: # This covers two cases:
# nua == nub == 0: Coincident # nua == nub == 0: Coincident
@ -486,7 +490,7 @@ class Canvas(QWidget):
if 0 <= ua <= 1 and 0 <= ub <= 1: if 0 <= ua <= 1 and 0 <= ub <= 1:
x = x1 + ua * (x2 - x1) x = x1 + ua * (x2 - x1)
y = y1 + ua * (y2 - y1) 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)) d = distance(m - QPointF(x2, y2))
yield d, i, (x, y) yield d, i, (x, y)
@ -507,8 +511,8 @@ class Canvas(QWidget):
self.zoomRequest.emit(ev.delta()) self.zoomRequest.emit(ev.delta())
else: else:
self.scrollRequest.emit(ev.delta(), self.scrollRequest.emit(ev.delta(),
Qt.Horizontal if (Qt.ShiftModifier == int(mods))\ Qt.Horizontal if (Qt.ShiftModifier == int(mods))
else Qt.Vertical) else Qt.Vertical)
else: else:
self.scrollRequest.emit(ev.delta(), Qt.Horizontal) self.scrollRequest.emit(ev.delta(), Qt.Horizontal)
ev.accept() ev.accept()
@ -571,4 +575,3 @@ class Canvas(QWidget):
self.restoreCursor() self.restoreCursor()
self.pixmap = None self.pixmap = None
self.update() self.update()

View File

@ -8,13 +8,15 @@ except ImportError:
BB = QDialogButtonBox BB = QDialogButtonBox
class ColorDialog(QColorDialog): class ColorDialog(QColorDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super(ColorDialog, self).__init__(parent) super(ColorDialog, self).__init__(parent)
self.setOption(QColorDialog.ShowAlphaChannel) self.setOption(QColorDialog.ShowAlphaChannel)
# The Mac native dialog does not support our restore button. # The Mac native dialog does not support our restore button.
self.setOption(QColorDialog.DontUseNativeDialog) self.setOption(QColorDialog.DontUseNativeDialog)
## Add a restore defaults button. # Add a restore defaults button.
# The default is set at invocation time, so that it # The default is set at invocation time, so that it
# works across dialogs for different elements. # works across dialogs for different elements.
self.default = None self.default = None
@ -33,4 +35,3 @@ class ColorDialog(QColorDialog):
def checkRestore(self, button): def checkRestore(self, button):
if self.bb.buttonRole(button) & BB.ResetRole and self.default: if self.bb.buttonRole(button) & BB.ResetRole and self.default:
self.setCurrentColor(self.default) self.setCurrentColor(self.default)

View File

@ -10,6 +10,7 @@ from lib import newIcon, labelValidator
BB = QDialogButtonBox BB = QDialogButtonBox
class LabelDialog(QDialog): class LabelDialog(QDialog):
def __init__(self, text="Enter object label", parent=None, listItem=None): def __init__(self, text="Enter object label", parent=None, listItem=None):

View File

@ -11,9 +11,11 @@ from pascal_voc_io import PascalVocWriter
import os.path import os.path
import sys import sys
class LabelFileError(Exception): class LabelFileError(Exception):
pass pass
class LabelFile(object): class LabelFile(object):
# It might be changed as window creates # It might be changed as window creates
suffix = '.lif' suffix = '.lif'
@ -26,7 +28,7 @@ class LabelFile(object):
self.load(filename) self.load(filename)
def savePascalVocFormat(self, filename, shapes, imagePath, imageData, def savePascalVocFormat(self, filename, shapes, imagePath, imageData,
lineColor=None, fillColor=None, databaseSrc=None): lineColor=None, fillColor=None, databaseSrc=None):
imgFolderPath = os.path.dirname(imagePath) imgFolderPath = os.path.dirname(imagePath)
imgFolderName = os.path.split(imgFolderPath)[-1] imgFolderName = os.path.split(imgFolderPath)[-1]
imgFileName = os.path.basename(imagePath) imgFileName = os.path.basename(imagePath)
@ -35,8 +37,9 @@ class LabelFile(object):
# Pascal format # Pascal format
image = QImage() image = QImage()
image.load(imagePath) image.load(imagePath)
imageShape = [image.height(), image.width(), 1 if image.isGrayscale() else 3] imageShape = [image.height(), image.width(),
writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt,\ 1 if image.isGrayscale() else 3]
writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt,
imageShape, localImgPath=imagePath) imageShape, localImgPath=imagePath)
bSave = False bSave = False
for shape in shapes: for shape in shapes:
@ -47,7 +50,7 @@ class LabelFile(object):
bSave = True bSave = True
if bSave: if bSave:
writer.save(targetFile = filename) writer.save(targetFile=filename)
return return
@staticmethod @staticmethod
@ -64,10 +67,10 @@ class LabelFile(object):
for p in points: for p in points:
x = p[0] x = p[0]
y = p[1] y = p[1]
xmin = min(x,xmin) xmin = min(x, xmin)
ymin = min(y,ymin) ymin = min(y, ymin)
xmax = max(x,xmax) xmax = max(x, xmax)
ymax = max(y,ymax) ymax = max(y, ymax)
# Martin Kersner, 2015/11/12 # Martin Kersner, 2015/11/12
# 0-valued coordinates of BB caused an error while # 0-valued coordinates of BB caused an error while

View File

@ -12,6 +12,7 @@ except ImportError:
def newIcon(icon): def newIcon(icon):
return QIcon(':/' + icon) return QIcon(':/' + icon)
def newButton(text, icon=None, slot=None): def newButton(text, icon=None, slot=None):
b = QPushButton(text) b = QPushButton(text)
if icon is not None: if icon is not None:
@ -20,8 +21,9 @@ def newButton(text, icon=None, slot=None):
b.clicked.connect(slot) b.clicked.connect(slot)
return b return b
def newAction(parent, text, slot=None, shortcut=None, icon=None, 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.""" """Create a new action and assign callbacks, shortcuts, etc."""
a = QAction(text, parent) a = QAction(text, parent)
if icon is not None: if icon is not None:
@ -51,18 +53,21 @@ def addActions(widget, actions):
else: else:
widget.addAction(action) widget.addAction(action)
def labelValidator(): def labelValidator():
return QRegExpValidator(QRegExp(r'^[^ \t].+'), None) return QRegExpValidator(QRegExp(r'^[^ \t].+'), None)
class struct(object): class struct(object):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
def distance(p): def distance(p):
return sqrt(p.x() * p.x() + p.y() * p.y()) return sqrt(p.x() * p.x() + p.y() * p.y())
def fmtShortcut(text): def fmtShortcut(text):
mod, key = text.split('+', 1) mod, key = text.split('+', 1)
return '<b>%s</b>+<b>%s</b>' % (mod, key) return '<b>%s</b>+<b>%s</b>' % (mod, key)

View File

@ -9,6 +9,7 @@ import codecs
XML_EXT = '.xml' XML_EXT = '.xml'
class PascalVocWriter: class PascalVocWriter:
def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localImgPath=None): def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localImgPath=None):
@ -102,7 +103,8 @@ class PascalVocWriter:
self.appendObjects(root) self.appendObjects(root)
out_file = None out_file = None
if targetFile is 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: else:
out_file = codecs.open(targetFile, 'w', encoding='utf-8') out_file = codecs.open(targetFile, 'w', encoding='utf-8')

View File

@ -18,13 +18,14 @@ DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155)
DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255) DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255)
DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0) DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0)
class Shape(object): class Shape(object):
P_SQUARE, P_ROUND = range(2) P_SQUARE, P_ROUND = range(2)
MOVE_VERTEX, NEAR_VERTEX = range(2) MOVE_VERTEX, NEAR_VERTEX = range(2)
## The following class variables influence the drawing # The following class variables influence the drawing
## of _all_ shape objects. # of _all_ shape objects.
line_color = DEFAULT_LINE_COLOR line_color = DEFAULT_LINE_COLOR
fill_color = DEFAULT_FILL_COLOR fill_color = DEFAULT_FILL_COLOR
select_line_color = DEFAULT_SELECT_LINE_COLOR select_line_color = DEFAULT_SELECT_LINE_COLOR
@ -46,7 +47,7 @@ class Shape(object):
self._highlightSettings = { self._highlightSettings = {
self.NEAR_VERTEX: (4, self.P_ROUND), self.NEAR_VERTEX: (4, self.P_ROUND),
self.MOVE_VERTEX: (1.5, self.P_SQUARE), self.MOVE_VERTEX: (1.5, self.P_SQUARE),
} }
self._closed = False self._closed = False
@ -61,7 +62,7 @@ class Shape(object):
self._closed = True self._closed = True
def reachMaxPoints(self): def reachMaxPoints(self):
if len(self.points) >=4: if len(self.points) >= 4:
return True return True
return False return False
@ -124,9 +125,9 @@ class Shape(object):
else: else:
self.vertex_fill_color = Shape.vertex_fill_color self.vertex_fill_color = Shape.vertex_fill_color
if shape == self.P_SQUARE: 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: elif shape == self.P_ROUND:
path.addEllipse(point, d/2.0, d/2.0) path.addEllipse(point, d / 2.0, d / 2.0)
else: else:
assert False, "unsupported vertex shape" assert False, "unsupported vertex shape"
@ -162,8 +163,8 @@ class Shape(object):
self._highlightIndex = None self._highlightIndex = None
def copy(self): def copy(self):
shape = Shape("Copy of %s" % self.label ) shape = Shape("Copy of %s" % self.label)
shape.points= [p for p in self.points] shape.points = [p for p in self.points]
shape.fill = self.fill shape.fill = self.fill
shape.selected = self.selected shape.selected = self.selected
shape._closed = self._closed shape._closed = self._closed
@ -181,4 +182,3 @@ class Shape(object):
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.points[key] = value self.points[key] = value

View File

@ -8,6 +8,7 @@ except ImportError:
class ToolBar(QToolBar): class ToolBar(QToolBar):
def __init__(self, title): def __init__(self, title):
super(ToolBar, self).__init__(title) super(ToolBar, self).__init__(title)
layout = self.layout() layout = self.layout()
@ -29,10 +30,10 @@ class ToolBar(QToolBar):
class ToolButton(QToolButton): class ToolButton(QToolButton):
"""ToolBar companion class which ensures all buttons have the same size.""" """ToolBar companion class which ensures all buttons have the same size."""
minSize = (60, 60) minSize = (60, 60)
def minimumSizeHint(self): def minimumSizeHint(self):
ms = super(ToolButton, self).minimumSizeHint() ms = super(ToolButton, self).minimumSizeHint()
w1, h1 = ms.width(), ms.height() w1, h1 = ms.width(), ms.height()
w2, h2 = self.minSize w2, h2 = self.minSize
ToolButton.minSize = max(w1, w2), max(h1, h2) ToolButton.minSize = max(w1, w2), max(h1, h2)
return QSize(*ToolButton.minSize) return QSize(*ToolButton.minSize)

View File

@ -6,7 +6,9 @@ except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
class ZoomWidget(QSpinBox): class ZoomWidget(QSpinBox):
def __init__(self, value=100): def __init__(self, value=100):
super(ZoomWidget, self).__init__() super(ZoomWidget, self).__init__()
self.setButtonSymbols(QAbstractSpinBox.NoButtons) self.setButtonSymbols(QAbstractSpinBox.NoButtons)
@ -22,4 +24,3 @@ class ZoomWidget(QSpinBox):
fm = QFontMetrics(self.font()) fm = QFontMetrics(self.font())
width = fm.width(str(self.maximum())) width = fm.width(str(self.maximum()))
return QSize(width, height) return QSize(width, height)

View File

@ -18,4 +18,3 @@ class TestMainWindow(TestCase):
def test_noop(self): def test_noop(self):
pass pass