| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286 | import argparseimport functoolsimport loggingimport os.pathimport reimport sysimport warningsimport webbrowserfrom qtpy import QT_VERSIONfrom qtpy import QtCorefrom qtpy.QtCore import Qtfrom qtpy import QtGuifrom qtpy import QtWidgetsimport yamlQT5 = QT_VERSION[0] == '5'from labelme.canvas import Canvasfrom labelme.colorDialog import ColorDialogfrom labelme.config import default_configfrom labelme.labelDialog import LabelDialogfrom labelme.labelFile import LabelFilefrom labelme.labelFile import LabelFileErrorfrom labelme.lib import addActionsfrom labelme.lib import fmtShortcutfrom labelme.lib import newActionfrom labelme.lib import newIconfrom labelme.lib import structfrom labelme.shape import DEFAULT_FILL_COLORfrom labelme.shape import DEFAULT_LINE_COLORfrom labelme.shape import Shapefrom labelme.toolBar import ToolBarfrom labelme.zoomWidget import ZoomWidget__appname__ = 'labelme'logging.basicConfig(level=logging.INFO)logger = logging.getLogger(__appname__)# FIXME# - [medium] Set max zoom value to something big enough for FitWidth/Window# TODO(unknown):# - [high] Automatically add file suffix when saving.# - [high] Add polygon movement with arrow keys# - [high] Deselect shape when clicking and already selected(?)# - [medium] Zoom should keep the image centered.# - [medium] Add undo button for vertex addition.# - [low,maybe] Open images with drag & drop.# - [low,maybe] Preview images on file dialogs.# - [low,maybe] Sortable label list.# - Zoom is too "steppy".# Utility functions and classes.class WindowMixin(object):    def menu(self, title, actions=None):        menu = self.menuBar().addMenu(title)        if actions:            addActions(menu, actions)        return menu    def toolbar(self, title, actions=None):        toolbar = ToolBar(title)        toolbar.setObjectName('%sToolBar' % title)        # toolbar.setOrientation(Qt.Vertical)        toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)        if actions:            addActions(toolbar, actions)        self.addToolBar(Qt.LeftToolBarArea, toolbar)        return toolbarclass EscapableQListWidget(QtWidgets.QListWidget):    def keyPressEvent(self, event):        if event.key() == Qt.Key_Escape:            self.clearSelection()class LabelQListWidget(QtWidgets.QListWidget):    def __init__(self, *args, **kwargs):        super(LabelQListWidget, self).__init__(*args, **kwargs)        self.canvas = None        self.itemsToShapes = []    def get_shape_from_item(self, item):        for index, (item_, shape) in enumerate(self.itemsToShapes):            if item_ is item:                return shape    def get_item_from_shape(self, shape):        for index, (item, shape_) in enumerate(self.itemsToShapes):            if shape_ is shape:                return item    def clear(self):        super(LabelQListWidget, self).clear()        self.itemsToShapes = []    def setParent(self, parent):        self.parent = parent    def dropEvent(self, event):        shapes = self.shapes        super(LabelQListWidget, self).dropEvent(event)        if self.shapes == shapes:            return        if self.canvas is None:            raise RuntimeError('self.canvas must be set beforehand.')        self.parent.setDirty()        self.canvas.loadShapes(shapes)    @property    def shapes(self):        shapes = []        for i in range(self.count()):            item = self.item(i)            shape = self.get_shape_from_item(item)            shapes.append(shape)        return shapesclass MainWindow(QtWidgets.QMainWindow, WindowMixin):    FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = 0, 1, 2    def __init__(self, filename=None, output=None, store_data=True,                 labels=None, sort_labels=True, auto_save=False,                 validate_label=None):        super(MainWindow, self).__init__()        self.setWindowTitle(__appname__)        # Whether we need to save or not.        self.dirty = False        self._noSelectionSlot = False        # Main widgets and related state.        self.labelDialog = LabelDialog(parent=self, labels=labels,                                       sort_labels=sort_labels)        self.labelList = LabelQListWidget()        self.lastOpenDir = None        self.labelList.itemActivated.connect(self.labelSelectionChanged)        self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)        self.labelList.itemDoubleClicked.connect(self.editLabel)        # Connect to itemChanged to detect checkbox changes.        self.labelList.itemChanged.connect(self.labelItemChanged)        self.labelList.setDragDropMode(            QtWidgets.QAbstractItemView.InternalMove)        self.labelList.setParent(self)        listLayout = QtWidgets.QVBoxLayout()        listLayout.setContentsMargins(0, 0, 0, 0)        self.editButton = QtWidgets.QToolButton()        self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)        listLayout.addWidget(self.editButton)  # 0, Qt.AlignCenter)        listLayout.addWidget(self.labelList)        self.labelListContainer = QtWidgets.QWidget()        self.labelListContainer.setLayout(listLayout)        self.uniqLabelList = EscapableQListWidget()        self.uniqLabelList.setToolTip(            "Select label to start annotating for it. "            "Press 'Esc' to deselect.")        if labels:            self.uniqLabelList.addItems(labels)            self.uniqLabelList.sortItems()        self.labelsdock = QtWidgets.QDockWidget(u'Label List', self)        self.labelsdock.setObjectName(u'Label List')        self.labelsdock.setWidget(self.uniqLabelList)        self.dock = QtWidgets.QDockWidget('Polygon Labels', self)        self.dock.setObjectName('Labels')        self.dock.setWidget(self.labelListContainer)        self.fileListWidget = QtWidgets.QListWidget()        self.fileListWidget.itemSelectionChanged.connect(            self.fileSelectionChanged)        filelistLayout = QtWidgets.QVBoxLayout()        filelistLayout.setContentsMargins(0, 0, 0, 0)        filelistLayout.addWidget(self.fileListWidget)        fileListContainer = QtWidgets.QWidget()        fileListContainer.setLayout(filelistLayout)        self.filedock = QtWidgets.QDockWidget(u'File List', self)        self.filedock.setObjectName(u'Files')        self.filedock.setWidget(fileListContainer)        self.zoomWidget = ZoomWidget()        self.colorDialog = ColorDialog(parent=self)        self.canvas = self.labelList.canvas = Canvas()        self.canvas.zoomRequest.connect(self.zoomRequest)        scrollArea = QtWidgets.QScrollArea()        scrollArea.setWidget(self.canvas)        scrollArea.setWidgetResizable(True)        self.scrollBars = {            Qt.Vertical: scrollArea.verticalScrollBar(),            Qt.Horizontal: scrollArea.horizontalScrollBar(),        }        self.canvas.scrollRequest.connect(self.scrollRequest)        self.canvas.newShape.connect(self.newShape)        self.canvas.shapeMoved.connect(self.setDirty)        self.canvas.selectionChanged.connect(self.shapeSelectionChanged)        self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive)        self.setCentralWidget(scrollArea)        self.addDockWidget(Qt.RightDockWidgetArea, self.labelsdock)        self.addDockWidget(Qt.RightDockWidgetArea, self.dock)        self.addDockWidget(Qt.RightDockWidgetArea, self.filedock)        self.filedock.setFeatures(QtWidgets.QDockWidget.DockWidgetFloatable)        self.dockFeatures = (QtWidgets.QDockWidget.DockWidgetClosable |                             QtWidgets.QDockWidget.DockWidgetFloatable)        self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)        config = self.getConfig()        # Actions        action = functools.partial(newAction, self)        shortcuts = config['shortcuts']        quit = action('&Quit', self.close, shortcuts['quit'], 'quit',                      'Quit application')        open_ = action('&Open', self.openFile, shortcuts['open'], 'open',                       'Open image or label file')        opendir = action('&Open Dir', self.openDirDialog,                         shortcuts['open_dir'], 'open', u'Open Dir')        openNextImg = action('&Next Image', self.openNextImg,                             shortcuts['open_next'], 'next', u'Open Next')        openPrevImg = action('&Prev Image', self.openPrevImg,                             shortcuts['open_prev'], 'prev', u'Open Prev')        save = action('&Save', self.saveFile, shortcuts['save'], 'save',                      'Save labels to file', enabled=False)        saveAs = action('&Save As', self.saveFileAs, shortcuts['save_as'],                        'save-as', 'Save labels to a different file',                        enabled=False)        close = action('&Close', self.closeFile, shortcuts['close'], 'close',                       'Close current file')        color1 = action('Polygon &Line Color', self.chooseColor1,                        shortcuts['edit_line_color'], 'color_line',                        'Choose polygon line color')        color2 = action('Polygon &Fill Color', self.chooseColor2,                        shortcuts['edit_fill_color'], 'color',                        'Choose polygon fill color')        createMode = action('Create\nPolygo&ns', self.setCreateMode,                            shortcuts['create_polygon'], 'objects',                            'Start drawing polygons', enabled=True)        editMode = action('&Edit\nPolygons', self.setEditMode,                          shortcuts['edit_polygon'], 'edit',                          'Move and edit polygons', enabled=False)        delete = action('Delete\nPolygon', self.deleteSelectedShape,                        shortcuts['delete_polygon'], 'cancel',                        'Delete', enabled=False)        copy = action('&Duplicate\nPolygon', self.copySelectedShape,                      shortcuts['duplicate_polygon'], 'copy',                      'Create a duplicate of the selected polygon',                      enabled=False)        undoLastPoint = action('Undo last point', self.canvas.undoLastPoint,                               shortcuts['undo_last_point'], 'undo',                               'Undo last drawn point', enabled=False)        undo = action('Undo', self.undoShapeEdit, shortcuts['undo'], 'undo',                      'Undo last add and edit of shape', enabled=False)        hideAll = action('&Hide\nPolygons',                         functools.partial(self.togglePolygons, False),                         icon='eye', tip='Hide all polygons', enabled=False)        showAll = action('&Show\nPolygons',                         functools.partial(self.togglePolygons, True),                         icon='eye', tip='Show all polygons', enabled=False)        help = action('&Tutorial', self.tutorial, icon='help',                      tip='Show tutorial page')        zoom = QtWidgets.QWidgetAction(self)        zoom.setDefaultWidget(self.zoomWidget)        self.zoomWidget.setWhatsThis(            "Zoom in or out of the image. Also accessible with"            " %s and %s from the canvas." %            (fmtShortcut('%s,%s' % (shortcuts['zoom_in'],                                    shortcuts['zoom_out'])),             fmtShortcut("Ctrl+Wheel")))        self.zoomWidget.setEnabled(False)        zoomIn = action('Zoom &In', functools.partial(self.addZoom, 10),                        shortcuts['zoom_in'], 'zoom-in',                        'Increase zoom level', enabled=False)        zoomOut = action('&Zoom Out', functools.partial(self.addZoom, -10),                         shortcuts['zoom_out'], 'zoom-out',                         'Decrease zoom level', enabled=False)        zoomOrg = action('&Original size',                         functools.partial(self.setZoom, 100),                         shortcuts['zoom_to_original'], 'zoom',                         'Zoom to original size', enabled=False)        fitWindow = action('&Fit Window', self.setFitWindow,                           shortcuts['fit_window'], 'fit-window',                           'Zoom follows window size', checkable=True,                           enabled=False)        fitWidth = action('Fit &Width', self.setFitWidth,                          shortcuts['fit_width'], 'fit-width',                          '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)        self.zoomMode = self.MANUAL_ZOOM        self.scalers = {            self.FIT_WINDOW: self.scaleFitWindow,            self.FIT_WIDTH: self.scaleFitWidth,            # Set to one to scale to 100% when loading files.            self.MANUAL_ZOOM: lambda: 1,        }        edit = action('&Edit Label', self.editLabel, shortcuts['edit_label'],                      'edit', 'Modify the label of the selected polygon',                      enabled=False)        self.editButton.setDefaultAction(edit)        shapeLineColor = action(            'Shape &Line Color', self.chshapeLineColor, icon='color-line',            tip='Change the line color for this specific shape', enabled=False)        shapeFillColor = action(            'Shape &Fill Color', self.chshapeFillColor, icon='color',            tip='Change the fill color for this specific shape', enabled=False)        labels = self.dock.toggleViewAction()        labels.setText('Show/Hide Label Panel')        # Lavel list context menu.        labelMenu = QtWidgets.QMenu()        addActions(labelMenu, (edit, delete))        self.labelList.setContextMenuPolicy(Qt.CustomContextMenu)        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,            delete=delete, edit=edit, copy=copy,            undoLastPoint=undoLastPoint, undo=undo,            createMode=createMode, editMode=editMode,            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),            tool=(),            editMenu=(edit, copy, delete, None, undo, undoLastPoint,                      None, color1, color2),            menu=(                createMode, editMode, edit, copy,                delete, shapeLineColor, shapeFillColor,                undo, undoLastPoint,            ),            onLoadActive=(close, 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=QtWidgets.QMenu('Open &Recent'),            labelList=labelMenu,        )        addActions(self.menus.file, (open_, opendir, self.menus.recentFiles,                                     save, saveAs, close, None, quit))        addActions(self.menus.help, (help,))        addActions(self.menus.view, (            labels, None,            hideAll, showAll, None,            zoomIn, zoomOut, zoomOrg, None,            fitWindow, fitWidth))        self.menus.file.aboutToShow.connect(self.updateFileMenu)        # Custom context menu for the canvas widget:        addActions(self.canvas.menus[0], self.actions.menu)        addActions(self.canvas.menus[1], (            action('&Copy here', self.copyShape),            action('&Move here', self.moveShape)))        self.tools = self.toolbar('Tools')        self.actions.tool = (            open_, opendir, openNextImg, openPrevImg, save,            None, createMode, copy, delete, editMode, undo, None,            zoomIn, zoom, zoomOut, fitWindow, fitWidth)        self.statusBar().showMessage('%s started.' % __appname__)        self.statusBar().show()        # Application state.        self.image = QtGui.QImage()        self.imagePath = None        if auto_save and output is not None:            warnings.warn('If `auto_save` argument is True, `output` argument '                          'is ignored and output filename is automatically '                          'set as IMAGE_BASENAME.json.')        self.labeling_once = output is not None        self.output = output        self._auto_save = auto_save        self._store_data = store_data        if validate_label not in [None, 'exact', 'instance']:            raise ValueError('Unexpected `validate_label`: {}'                             .format(validate_label))        self._validate_label = validate_label        self.recentFiles = []        self.maxRecent = 7        self.lineColor = None        self.fillColor = None        self.otherData = None        self.zoom_level = 100        self.fit_window = False        if filename is not None and os.path.isdir(filename):            self.importDirImages(filename)        else:            self.filename = filename        # XXX: Could be completely declarative.        # Restore application settings.        self.settings = QtCore.QSettings('labelme', 'labelme')        # FIXME: QSettings.value can return None on PyQt4        self.recentFiles = self.settings.value('recentFiles', []) or []        size = self.settings.value('window/size', QtCore.QSize(600, 500))        position = self.settings.value('window/position', QtCore.QPoint(0, 0))        self.resize(size)        self.move(position)        # or simply:        # self.restoreGeometry(settings['window/geometry']        self.restoreState(            self.settings.value('window/state', QtCore.QByteArray()))        self.lineColor = QtGui.QColor(            self.settings.value('line/color', Shape.line_color))        self.fillColor = QtGui.QColor(            self.settings.value('fill/color', Shape.fill_color))        Shape.line_color = self.lineColor        Shape.fill_color = self.fillColor        # Populate the File menu dynamically.        self.updateFileMenu()        # Since loading the file may take some time,        # make sure it runs in the background.        if self.filename is not None:            self.queueEvent(functools.partial(self.loadFile, self.filename))        # Callbacks:        self.zoomWidget.valueChanged.connect(self.paintCanvas)        self.populateModeActions()        # self.firstStart = True        # if self.firstStart:        #    QWhatsThis.enterWhatsThisMode()    # Support Functions    def getConfig(self):        # shortcuts for actions        home = os.path.expanduser('~')        config_file = os.path.join(home, '.labelmerc')        # default config        config = default_config.copy()        def update_dict(target_dict, new_dict):            for key, value in new_dict.items():                if key not in target_dict:                    logger.warn('Skipping unexpected key in config: {}'                                .format(key))                    continue                if isinstance(target_dict[key], dict) and \                        isinstance(value, dict):                    update_dict(target_dict[key], value)                else:                    target_dict[key] = value        if os.path.exists(config_file):            user_config = yaml.load(open(config_file)) or {}            update_dict(config, user_config)        # save config        try:            yaml.safe_dump(config, open(config_file, 'w'),                           default_flow_style=False)        except Exception:            warnings.warn('Failed to save config: {}'.format(config_file))        return config    def noShapes(self):        return not self.labelList.itemsToShapes    def populateModeActions(self):        tool, menu = self.actions.tool, self.actions.menu        self.tools.clear()        addActions(self.tools, tool)        self.canvas.menus[0].clear()        addActions(self.canvas.menus[0], menu)        self.menus.edit.clear()        actions = (self.actions.createMode, self.actions.editMode)        addActions(self.menus.edit, actions + self.actions.editMenu)    def setDirty(self):        if self._auto_save:            label_file = os.path.splitext(self.imagePath)[0] + '.json'            self.saveLabels(label_file)            return        self.dirty = True        self.actions.save.setEnabled(True)        self.actions.undo.setEnabled(self.canvas.isShapeRestorable)        title = __appname__        if self.filename is not None:            title = '{} - {}*'.format(title, self.filename)        self.setWindowTitle(title)    def setClean(self):        self.dirty = False        self.actions.save.setEnabled(False)        self.actions.createMode.setEnabled(True)        title = __appname__        if self.filename is not None:            title = '{} - {}'.format(title, self.filename)        self.setWindowTitle(title)    def toggleActions(self, value=True):        """Enable/Disable widgets which depend on an opened image."""        for z in self.actions.zoomActions:            z.setEnabled(value)        for action in self.actions.onLoadActive:            action.setEnabled(value)    def queueEvent(self, function):        QtCore.QTimer.singleShot(0, function)    def status(self, message, delay=5000):        self.statusBar().showMessage(message, delay)    def resetState(self):        self.labelList.clear()        self.filename = None        self.imagePath = None        self.imageData = None        self.labelFile = None        self.otherData = None        self.canvas.resetState()    def currentItem(self):        items = self.labelList.selectedItems()        if items:            return items[0]        return None    def addRecentFile(self, filename):        if filename in self.recentFiles:            self.recentFiles.remove(filename)        elif len(self.recentFiles) >= self.maxRecent:            self.recentFiles.pop()        self.recentFiles.insert(0, filename)    # Callbacks    def undoShapeEdit(self):        self.canvas.restoreShape()        self.labelList.clear()        self.uniqLabelList.clear()        self.loadShapes(self.canvas.shapes)        self.actions.undo.setEnabled(self.canvas.isShapeRestorable)    def tutorial(self):        url = 'https://github.com/wkentaro/labelme/tree/master/examples/tutorial'  # NOQA        webbrowser.open(url)    def toggleDrawingSensitive(self, drawing=True):        """Toggle drawing sensitive.        In the middle of drawing, toggling between modes should be disabled.        """        self.actions.editMode.setEnabled(not drawing)        self.actions.undoLastPoint.setEnabled(drawing)        self.actions.undo.setEnabled(not drawing)    def toggleDrawMode(self, edit=True):        self.canvas.setEditing(edit)        self.actions.createMode.setEnabled(edit)        self.actions.editMode.setEnabled(not edit)    def setCreateMode(self):        self.toggleDrawMode(False)    def setEditMode(self):        self.toggleDrawMode(True)    def updateFileMenu(self):        current = self.filename        def exists(filename):            return os.path.exists(str(filename))        menu = self.menus.recentFiles        menu.clear()        files = [f for f in self.recentFiles if f != current and exists(f)]        for i, f in enumerate(files):            icon = newIcon('labels')            action = QtWidgets.QAction(                icon, '&%d %s' % (i + 1, QtCore.QFileInfo(f).fileName()), self)            action.triggered.connect(functools.partial(self.loadRecent, f))            menu.addAction(action)    def popLabelListMenu(self, point):        self.menus.labelList.exec_(self.labelList.mapToGlobal(point))    def validateLabel(self, label):        # no validation        if self._validate_label is None:            return True        for i in range(self.uniqLabelList.count()):            l = self.uniqLabelList.item(i).text()            if self._validate_label in ['exact', 'instance']:                if l == label:                    return True            if self._validate_label == 'instance':                m = re.match(r'^{}-[0-9]*$'.format(l), label)                if m:                    return True        return False    def editLabel(self, item=None):        if not self.canvas.editing():            return        item = item if item else self.currentItem()        text = self.labelDialog.popUp(item.text())        if not self.validateLabel(text):            self.errorMessage('Invalid label',                              "Invalid label '{}' with validation type '{}'"                              .format(text, self._validate_label))            text = None        if text is None:            return        item.setText(text)        self.setDirty()        if not self.uniqLabelList.findItems(text, Qt.MatchExactly):            self.uniqLabelList.addItem(text)            self.uniqLabelList.sortItems()    def fileSelectionChanged(self):        items = self.fileListWidget.selectedItems()        if not items:            return        item = items[0]        if not self.mayContinue():            return        currIndex = self.imageList.index(str(item.text()))        if currIndex < len(self.imageList):            filename = self.imageList[currIndex]            if filename:                self.loadFile(filename)    # React to canvas signals.    def shapeSelectionChanged(self, selected=False):        if self._noSelectionSlot:            self._noSelectionSlot = False        else:            shape = self.canvas.selectedShape            if shape:                item = self.labelList.get_item_from_shape(shape)                item.setSelected(True)            else:                self.labelList.clearSelection()        self.actions.delete.setEnabled(selected)        self.actions.copy.setEnabled(selected)        self.actions.edit.setEnabled(selected)        self.actions.shapeLineColor.setEnabled(selected)        self.actions.shapeFillColor.setEnabled(selected)    def addLabel(self, shape):        item = QtWidgets.QListWidgetItem(shape.label)        item.setFlags(item.flags() | Qt.ItemIsUserCheckable)        item.setCheckState(Qt.Checked)        self.labelList.itemsToShapes.append((item, shape))        self.labelList.addItem(item)        if not self.uniqLabelList.findItems(shape.label, Qt.MatchExactly):            self.uniqLabelList.addItem(shape.label)            self.uniqLabelList.sortItems()        self.labelDialog.addLabelHistory(item.text())        for action in self.actions.onShapesPresent:            action.setEnabled(True)    def remLabel(self, shape):        item = self.labelList.get_item_from_shape(shape)        self.labelList.takeItem(self.labelList.row(item))    def loadShapes(self, shapes):        for shape in shapes:            self.addLabel(shape)        self.canvas.loadShapes(shapes)    def loadLabels(self, shapes):        s = []        for label, points, line_color, fill_color in shapes:            shape = Shape(label=label)            for x, y in points:                shape.addPoint(QtCore.QPointF(x, y))            shape.close()            s.append(shape)            if line_color:                shape.line_color = QtGui.QColor(*line_color)            if fill_color:                shape.fill_color = QtGui.QColor(*fill_color)        self.loadShapes(s)    def saveLabels(self, filename):        lf = LabelFile()        def format_shape(s):            return dict(label=str(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,                        points=[(p.x(), p.y()) for p in s.points])        shapes = [format_shape(shape) for shape in self.labelList.shapes]        try:            imagePath = os.path.relpath(                self.imagePath, os.path.dirname(filename))            imageData = self.imageData if self._store_data else None            lf.save(filename, shapes, imagePath, imageData,                    self.lineColor.getRgb(), self.fillColor.getRgb(),                    self.otherData)            self.labelFile = lf            # disable allows next and previous image to proceed            # self.filename = filename            return True        except LabelFileError as e:            self.errorMessage('Error saving label data', '<b>%s</b>' % e)            return False    def copySelectedShape(self):        self.addLabel(self.canvas.copySelectedShape())        # fix copy and delete        self.shapeSelectionChanged(True)    def labelSelectionChanged(self):        item = self.currentItem()        if item and self.canvas.editing():            self._noSelectionSlot = True            shape = self.labelList.get_shape_from_item(item)            self.canvas.selectShape(shape)    def labelItemChanged(self, item):        shape = self.labelList.get_shape_from_item(item)        label = str(item.text())        if label != shape.label:            shape.label = str(item.text())            self.setDirty()        else:  # User probably changed item visibility            self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)    # Callback functions:    def newShape(self):        """Pop-up and give focus to the label editor.        position MUST be in global coordinates.        """        items = self.uniqLabelList.selectedItems()        text = None        if items:            text = items[0].text()        text = self.labelDialog.popUp(text)        if not self.validateLabel(text):            self.errorMessage('Invalid label',                              "Invalid label '{}' with validation type '{}'"                              .format(text, self._validate_label))            text = None        if text is None:            self.canvas.undoLastLine()            self.canvas.shapesBackups.pop()        else:            self.addLabel(self.canvas.setLastLabel(text))            self.actions.editMode.setEnabled(True)            self.actions.undoLastPoint.setEnabled(False)            self.actions.undo.setEnabled(True)            self.setDirty()    def scrollRequest(self, delta, orientation):        units = - delta * 0.1  # natural scroll        bar = self.scrollBars[orientation]        bar.setValue(bar.value() + bar.singleStep() * units)    def setZoom(self, value):        self.actions.fitWidth.setChecked(False)        self.actions.fitWindow.setChecked(False)        self.zoomMode = self.MANUAL_ZOOM        self.zoomWidget.setValue(value)    def addZoom(self, increment=10):        self.setZoom(self.zoomWidget.value() + increment)    def zoomRequest(self, delta, pos):        canvas_width_old = self.canvas.width()        units = delta * 0.1        self.addZoom(units)        canvas_width_new = self.canvas.width()        if canvas_width_old != canvas_width_new:            canvas_scale_factor = canvas_width_new / canvas_width_old            x_shift = round(pos.x() * canvas_scale_factor) - pos.x()            y_shift = round(pos.y() * canvas_scale_factor) - pos.y()            self.scrollBars[Qt.Horizontal].setValue(                self.scrollBars[Qt.Horizontal].value() + x_shift)            self.scrollBars[Qt.Vertical].setValue(                self.scrollBars[Qt.Vertical].value() + y_shift)    def setFitWindow(self, value=True):        if value:            self.actions.fitWidth.setChecked(False)        self.zoomMode = self.FIT_WINDOW if value else self.MANUAL_ZOOM        self.adjustScale()    def setFitWidth(self, value=True):        if value:            self.actions.fitWindow.setChecked(False)        self.zoomMode = self.FIT_WIDTH if value else self.MANUAL_ZOOM        self.adjustScale()    def togglePolygons(self, value):        for item, shape in self.labelList.itemsToShapes:            item.setCheckState(Qt.Checked if value else Qt.Unchecked)    def loadFile(self, filename=None):        """Load the specified file, or the last opened file if None."""        # changing fileListWidget loads file        if (filename in self.imageList and                self.fileListWidget.currentRow() !=                self.imageList.index(filename)):            self.fileListWidget.setCurrentRow(self.imageList.index(filename))            return        self.resetState()        self.canvas.setEnabled(False)        if filename is None:            filename = self.settings.value('filename', '')        filename = str(filename)        if not QtCore.QFile.exists(filename):            self.errorMessage(                'Error opening file', 'No such file: <b>%s</b>' % filename)            return False        # assumes same name, but json extension        self.status("Loading %s..." % os.path.basename(str(filename)))        label_file = os.path.splitext(filename)[0] + '.json'        if QtCore.QFile.exists(label_file) and \                LabelFile.isLabelFile(label_file):            try:                self.labelFile = LabelFile(label_file)                # FIXME: PyQt4 installed via Anaconda fails to load JPEG                # and JSON encoded images.                # https://github.com/ContinuumIO/anaconda-issues/issues/131                if QtGui.QImage.fromData(self.labelFile.imageData).isNull():                    raise LabelFileError(                        'Failed loading image data from label file.\n'                        'Maybe this is a known issue of PyQt4 built on'                        ' Anaconda, and may be fixed by installing PyQt5.')            except LabelFileError as e:                self.errorMessage(                    'Error opening file',                    "<p><b>%s</b></p>"                    "<p>Make sure <i>%s</i> is a valid label file."                    % (e, label_file))                self.status("Error reading %s" % label_file)                return False            self.imageData = self.labelFile.imageData            self.imagePath = os.path.join(os.path.dirname(label_file),                                          self.labelFile.imagePath)            self.lineColor = QtGui.QColor(*self.labelFile.lineColor)            self.fillColor = QtGui.QColor(*self.labelFile.fillColor)            self.otherData = self.labelFile.otherData        else:            # Load image:            # read data first and store for saving into label file.            self.imageData = read(filename, None)            if self.imageData is not None:                # the filename is image not JSON                self.imagePath = filename            self.labelFile = None        image = QtGui.QImage.fromData(self.imageData)        if image.isNull():            formats = ['*.{}'.format(fmt.data().decode())                       for fmt in QtGui.QImageReader.supportedImageFormats()]            self.errorMessage(                'Error opening file',                '<p>Make sure <i>{0}</i> is a valid image file.<br/>'                'Supported image formats: {1}</p>'                .format(filename, ','.join(formats)))            self.status("Error reading %s" % filename)            return False        self.image = image        self.filename = filename        self.canvas.loadPixmap(QtGui.QPixmap.fromImage(image))        if self.labelFile:            self.loadLabels(self.labelFile.shapes)        self.setClean()        self.canvas.setEnabled(True)        self.adjustScale(initial=True)        self.paintCanvas()        self.addRecentFile(self.filename)        self.toggleActions(True)        self.status("Loaded %s" % os.path.basename(str(filename)))        return True    def resizeEvent(self, event):        if self.canvas and not self.image.isNull()\           and self.zoomMode != self.MANUAL_ZOOM:            self.adjustScale()        super(MainWindow, self).resizeEvent(event)    def paintCanvas(self):        assert not self.image.isNull(), "cannot paint null image"        self.canvas.scale = 0.01 * self.zoomWidget.value()        self.canvas.adjustSize()        self.canvas.update()    def adjustScale(self, initial=False):        value = self.scalers[self.FIT_WINDOW if initial else self.zoomMode]()        self.zoomWidget.setValue(int(100 * value))    def scaleFitWindow(self):        """Figure out the size of the pixmap to fit the main widget."""        e = 2.0  # So that no scrollbars are generated.        w1 = self.centralWidget().width() - e        h1 = self.centralWidget().height() - e        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        a2 = w2 / h2        return w1 / w2 if a2 >= a1 else h1 / h2    def scaleFitWidth(self):        # The epsilon does not seem to work too well here.        w = self.centralWidget().width() - 2.0        return w / self.canvas.pixmap.width()    def closeEvent(self, event):        if not self.mayContinue():            event.ignore()        self.settings.setValue(            'filename', self.filename if self.filename else '')        self.settings.setValue('window/size', self.size())        self.settings.setValue('window/position', self.pos())        self.settings.setValue('window/state', self.saveState())        self.settings.setValue('line/color', self.lineColor)        self.settings.setValue('fill/color', self.fillColor)        self.settings.setValue('recentFiles', self.recentFiles)        # ask the use for where to save the labels        # self.settings.setValue('window/geometry', self.saveGeometry())    # User Dialogs #    def loadRecent(self, filename):        if self.mayContinue():            self.loadFile(filename)    def openPrevImg(self, _value=False):        if not self.mayContinue():            return        if len(self.imageList) <= 0:            return        if self.filename is None:            return        currIndex = self.imageList.index(self.filename)        if currIndex - 1 >= 0:            filename = self.imageList[currIndex - 1]            if filename:                self.loadFile(filename)    def openNextImg(self, _value=False, load=True):        if not self.mayContinue():            return        if len(self.imageList) <= 0:            return        filename = None        if self.filename is None:            filename = self.imageList[0]        else:            currIndex = self.imageList.index(self.filename)            if currIndex + 1 < len(self.imageList):                filename = self.imageList[currIndex + 1]        self.filename = filename        if self.filename and load:            self.loadFile(self.filename)    def openFile(self, _value=False):        if not self.mayContinue():            return        path = os.path.dirname(str(self.filename)) if self.filename else '.'        formats = ['*.{}'.format(fmt.data().decode())                   for fmt in QtGui.QImageReader.supportedImageFormats()]        filters = "Image & Label files (%s)" % ' '.join(            formats + ['*%s' % LabelFile.suffix])        filename = QtWidgets.QFileDialog.getOpenFileName(            self, '%s - Choose Image or Label file' % __appname__,            path, filters)        if QT5:            filename, _ = filename        filename = str(filename)        if filename:            self.loadFile(filename)    def saveFile(self, _value=False):        assert not self.image.isNull(), "cannot save empty image"        if self.hasLabels():            if self.labelFile:                # DL20180323 - overwrite when in directory                self._saveFile(self.labelFile.filename)            elif self.output:                self._saveFile(self.output)            else:                self._saveFile(self.saveFileDialog())    def saveFileAs(self, _value=False):        assert not self.image.isNull(), "cannot save empty image"        if self.hasLabels():            self._saveFile(self.saveFileDialog())    def saveFileDialog(self):        caption = '%s - Choose File' % __appname__        filters = 'Label files (*%s)' % LabelFile.suffix        dlg = QtWidgets.QFileDialog(self, caption, self.currentPath(), filters)        dlg.setDefaultSuffix(LabelFile.suffix[1:])        dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)        dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False)        dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False)        basename = os.path.splitext(self.filename)[0]        default_labelfile_name = os.path.join(            self.currentPath(), basename + LabelFile.suffix)        filename = dlg.getSaveFileName(            self, 'Choose File', default_labelfile_name,            'Label files (*%s)' % LabelFile.suffix)        if QT5:            filename, _ = filename        filename = str(filename)        return filename    def _saveFile(self, filename):        if filename and self.saveLabels(filename):            self.addRecentFile(filename)            self.setClean()            if self.labeling_once:                self.close()    def closeFile(self, _value=False):        if not self.mayContinue():            return        self.resetState()        self.setClean()        self.toggleActions(False)        self.canvas.setEnabled(False)        self.actions.saveAs.setEnabled(False)    # Message Dialogs. #    def hasLabels(self):        if not self.labelList.itemsToShapes:            self.errorMessage(                'No objects labeled',                'You must label at least one object to save the file.')            return False        return True    def mayContinue(self):        if not self.dirty:            return True        mb = QtWidgets.QMessageBox        msg = 'Save annotations to "{}" before closing?'.format(self.filename)        answer = mb.question(self,                             'Save annotations?',                             msg,                             mb.Save | mb.Discard | mb.Cancel,                             mb.Save)        if answer == mb.Discard:            return True        elif answer == mb.Save:            self.saveFile()            return True        else:  # answer == mb.Cancel            return False    def errorMessage(self, title, message):        return QtWidgets.QMessageBox.critical(            self, title, '<p><b>%s</b></p>%s' % (title, message))    def currentPath(self):        return os.path.dirname(str(self.filename)) if self.filename else '.'    def chooseColor1(self):        color = self.colorDialog.getColor(            self.lineColor, 'Choose line color', default=DEFAULT_LINE_COLOR)        if color:            self.lineColor = color            # Change the color for all shape lines:            Shape.line_color = self.lineColor            self.canvas.update()            self.setDirty()    def chooseColor2(self):        color = self.colorDialog.getColor(            self.fillColor, 'Choose fill color', default=DEFAULT_FILL_COLOR)        if color:            self.fillColor = color            Shape.fill_color = self.fillColor            self.canvas.update()            self.setDirty()    def deleteSelectedShape(self):        yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No        msg = 'You are about to permanently delete this polygon, ' \              'proceed anyway?'        if yes == QtWidgets.QMessageBox.warning(self, 'Attention', msg,                                                yes | no):            self.remLabel(self.canvas.deleteSelected())            self.setDirty()            if self.noShapes():                for action in self.actions.onShapesPresent:                    action.setEnabled(False)    def chshapeLineColor(self):        color = self.colorDialog.getColor(            self.lineColor, 'Choose line color', default=DEFAULT_LINE_COLOR)        if color:            self.canvas.selectedShape.line_color = color            self.canvas.update()            self.setDirty()    def chshapeFillColor(self):        color = self.colorDialog.getColor(            self.fillColor, 'Choose fill color', default=DEFAULT_FILL_COLOR)        if color:            self.canvas.selectedShape.fill_color = color            self.canvas.update()            self.setDirty()    def copyShape(self):        self.canvas.endMove(copy=True)        self.addLabel(self.canvas.selectedShape)        self.setDirty()    def moveShape(self):        self.canvas.endMove(copy=False)        self.setDirty()    def openDirDialog(self, _value=False, dirpath=None):        if not self.mayContinue():            return        defaultOpenDirPath = dirpath if dirpath else '.'        if self.lastOpenDir and os.path.exists(self.lastOpenDir):            defaultOpenDirPath = self.lastOpenDir        else:            defaultOpenDirPath = os.path.dirname(self.filename) \                if self.filename else '.'        targetDirPath = str(QtWidgets.QFileDialog.getExistingDirectory(            self, '%s - Open Directory' % __appname__, defaultOpenDirPath,            QtWidgets.QFileDialog.ShowDirsOnly |            QtWidgets.QFileDialog.DontResolveSymlinks))        self.importDirImages(targetDirPath)    @property    def imageList(self):        lst = []        for i in range(self.fileListWidget.count()):            item = self.fileListWidget.item(i)            lst.append(item.text())        return lst    def importDirImages(self, dirpath):        if not self.mayContinue() or not dirpath:            return        self.lastOpenDir = dirpath        self.filename = None        self.fileListWidget.clear()        for imgPath in self.scanAllImages(dirpath):            item = QtWidgets.QListWidgetItem(imgPath)            self.fileListWidget.addItem(item)        self.openNextImg(load=False)    def scanAllImages(self, folderPath):        extensions = ['.%s' % fmt.data().decode("ascii").lower()                      for fmt in QtGui.QImageReader.supportedImageFormats()]        images = []        for root, dirs, files in os.walk(folderPath):            for file in files:                if file.lower().endswith(tuple(extensions)):                    relativePath = os.path.join(root, file)                    images.append(relativePath)        images.sort(key=lambda x: x.lower())        return imagesdef inverted(color):    return QtGui.QColor(*[255 - v for v in color.getRgb()])def read(filename, default=None):    try:        with open(filename, 'rb') as f:            return f.read()    except Exception:        return defaultdef main():    """Standard boilerplate Qt application code."""    parser = argparse.ArgumentParser()    parser.add_argument('filename', nargs='?', help='image or label filename')    parser.add_argument('--output', '-O', '-o', help='output label name')    parser.add_argument('--nodata', dest='store_data', action='store_false',                        help='stop storing image data to JSON file')    parser.add_argument('--autosave', action='store_true', help='auto save')    parser.add_argument('--labels',                        help='comma separated list of labels OR file '                        'containing one label per line')    parser.add_argument('--nosortlabels', dest='sort_labels',                        action='store_false', help='stop sorting labels')    parser.add_argument('--validatelabel', choices=['exact', 'instance'],                        help='label validation types')    args = parser.parse_args()    if args.labels is None:        if args.validatelabel is not None:            logger.error('--labels must be specified with --validatelabel')            sys.exit(1)    else:        if os.path.isfile(args.labels):            args.labels = [l.strip() for l in open(args.labels, 'r')                           if l.strip()]        else:            args.labels = [l for l in args.labels.split(',') if l]    app = QtWidgets.QApplication(sys.argv)    app.setApplicationName(__appname__)    app.setWindowIcon(newIcon("icon"))    win = MainWindow(        filename=args.filename,        output=args.output,        store_data=args.store_data,        labels=args.labels,        sort_labels=args.sort_labels,        auto_save=args.autosave,        validate_label=args.validatelabel,    )    win.show()    win.raise_()    sys.exit(app.exec_())
 |