labelImg.py 57.9 KB
Newer Older
TzuTa Lin's avatar
TzuTa Lin committed
1
#!/usr/bin/env python
Jiye Qian's avatar
Jiye Qian committed
2
# -*- coding: utf-8 -*-
3
import codecs
4
import distutils.spawn
TzuTa Lin's avatar
TzuTa Lin committed
5
import os.path
6
import platform
TzuTa Lin's avatar
TzuTa Lin committed
7 8 9 10 11 12
import re
import sys
import subprocess

from functools import partial
from collections import defaultdict
13
from PIL import Image, ImageEnhance
TzuTa Lin's avatar
TzuTa Lin committed
14

15 16 17 18 19 20
try:
    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
except ImportError:
    # needed for py3+qt4
21 22
    # Ref:
    # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
23
    # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
24 25 26 27 28
    if sys.version_info.major >= 3:
        import sip
        sip.setapi('QVariant', 2)
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
TzuTa Lin's avatar
TzuTa Lin committed
29 30

import resources
tzutalin's avatar
tzutalin committed
31
# Add internal libs
32
from libs.constants import *
33
from libs.lib import struct, newAction, newIcon, addActions, fmtShortcut, generateColorByText
tzutalin's avatar
tzutalin committed
34
from libs.settings import Settings
35 36 37 38 39 40 41 42 43
from libs.shape import Shape, DEFAULT_LINE_COLOR, DEFAULT_FILL_COLOR
from libs.canvas import Canvas
from libs.zoomWidget import ZoomWidget
from libs.labelDialog import LabelDialog
from libs.colorDialog import ColorDialog
from libs.labelFile import LabelFile, LabelFileError
from libs.toolBar import ToolBar
from libs.pascal_voc_io import PascalVocReader
from libs.pascal_voc_io import XML_EXT
Wang Yinghao's avatar
Wang Yinghao committed
44
from libs.yolo_io import YoloReader
Wang Yinghao's avatar
Wang Yinghao committed
45
from libs.yolo_io import TXT_EXT
46
from libs.ustr import ustr
tzutalin's avatar
tzutalin committed
47
from libs.version import __version__
TzuTa Lin's avatar
TzuTa Lin committed
48 49 50

__appname__ = 'labelImg'

51 52
# Utility functions and classes.

53 54 55 56 57 58 59 60
def have_qstring():
    '''p3/qt5 get rid of QString wrapper as py3 has native unicode str type'''
    return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.'))

def util_qt_strlistclass():
    return QStringList if have_qstring() else list


TzuTa Lin's avatar
TzuTa Lin committed
61
class WindowMixin(object):
62

TzuTa Lin's avatar
TzuTa Lin committed
63 64 65 66 67 68 69 70 71
    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(u'%sToolBar' % title)
72
        # toolbar.setOrientation(Qt.Vertical)
TzuTa Lin's avatar
TzuTa Lin committed
73 74 75 76 77 78 79
        toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        if actions:
            addActions(toolbar, actions)
        self.addToolBar(Qt.LeftToolBarArea, toolbar)
        return toolbar


80 81
# PyQt5: TypeError: unhashable type: 'QListWidgetItem'
class HashableQListWidgetItem(QListWidgetItem):
82

83 84
    def __init__(self, *args):
        super(HashableQListWidgetItem, self).__init__(*args)
85

86 87 88 89
    def __hash__(self):
        return hash(id(self))


TzuTa Lin's avatar
TzuTa Lin committed
90
class MainWindow(QMainWindow, WindowMixin):
91
    FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = list(range(3))
TzuTa Lin's avatar
TzuTa Lin committed
92

jeffrey's avatar
jeffrey committed
93
    def __init__(self, defaultFilename=None, defaultPrefdefClassFile=None, defaultSaveDir=None):
TzuTa Lin's avatar
TzuTa Lin committed
94 95
        super(MainWindow, self).__init__()
        self.setWindowTitle(__appname__)
96 97 98 99 100 101

        # Load setting in the main thread
        self.settings = Settings()
        self.settings.load()
        settings = self.settings

TzuTa Lin's avatar
Typo  
TzuTa Lin committed
102
        # Save as Pascal voc xml
jeffrey's avatar
jeffrey committed
103
        self.defaultSaveDir = defaultSaveDir
TzuTa Lin's avatar
Typo  
TzuTa Lin committed
104
        self.usingPascalVocFormat = True
Wang Yinghao's avatar
Wang Yinghao committed
105 106
        self.usingYoloFormat = False

TzuTa Lin's avatar
TzuTa Lin committed
107 108 109
        # For loading all image under a directory
        self.mImgList = []
        self.dirname = None
110
        self.labelHist = []
111
        self.lastOpenDir = None
TzuTa Lin's avatar
TzuTa Lin committed
112 113 114 115 116 117

        # Whether we need to save or not.
        self.dirty = False

        self._noSelectionSlot = False
        self._beginner = True
118
        self.screencastViewer = self.getAvailableScreencastViewer()
TzuTa Lin's avatar
TzuTa Lin committed
119
        self.screencast = "https://youtu.be/p0nR2YsCY_U"
TzuTa Lin's avatar
TzuTa Lin committed
120

121 122 123
        # Load predefined classes to the list
        self.loadPredefinedClasses(defaultPrefdefClassFile)

TzuTa Lin's avatar
TzuTa Lin committed
124
        # Main widgets and related state.
125
        self.labelDialog = LabelDialog(parent=self, listItem=self.labelHist)
126

TzuTa Lin's avatar
TzuTa Lin committed
127 128
        self.itemsToShapes = {}
        self.shapesToItems = {}
129
        self.prevLabelText = ''
TzuTa Lin's avatar
TzuTa Lin committed
130 131 132

        listLayout = QVBoxLayout()
        listLayout.setContentsMargins(0, 0, 0, 0)
133

tzutalin's avatar
tzutalin committed
134
        # Create a widget for using default label
135 136
        self.useDefaultLabelCheckbox = QCheckBox(u'Use default label')
        self.useDefaultLabelCheckbox.setChecked(False)
tzutalin's avatar
tzutalin committed
137
        self.defaultLabelTextLine = QLineEdit()
138 139 140 141 142
        useDefaultLabelQHBoxLayout = QHBoxLayout()
        useDefaultLabelQHBoxLayout.addWidget(self.useDefaultLabelCheckbox)
        useDefaultLabelQHBoxLayout.addWidget(self.defaultLabelTextLine)
        useDefaultLabelContainer = QWidget()
        useDefaultLabelContainer.setLayout(useDefaultLabelQHBoxLayout)
tzutalin's avatar
tzutalin committed
143 144 145

        # Create a widget for edit and diffc button
        self.diffcButton = QCheckBox(u'difficult')
146
        self.diffcButton.setChecked(False)
147
        self.diffcButton.stateChanged.connect(self.btnstate)
tzutalin's avatar
tzutalin committed
148 149
        self.editButton = QToolButton()
        self.editButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
150

151
        # Add some of widgets to listLayout
tzutalin's avatar
tzutalin committed
152
        listLayout.addWidget(self.editButton)
153
        listLayout.addWidget(self.diffcButton)
154
        listLayout.addWidget(useDefaultLabelContainer)
155

tzutalin's avatar
tzutalin committed
156 157 158 159 160 161 162 163 164 165
        # Create and add a widget for showing current label items
        self.labelList = QListWidget()
        labelListContainer = QWidget()
        labelListContainer.setLayout(listLayout)
        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)
        listLayout.addWidget(self.labelList)
TzuTa Lin's avatar
TzuTa Lin committed
166 167 168

        self.dock = QDockWidget(u'Box Labels', self)
        self.dock.setObjectName(u'Labels')
tzutalin's avatar
tzutalin committed
169
        self.dock.setWidget(labelListContainer)
TzuTa Lin's avatar
TzuTa Lin committed
170

171 172
        # Tzutalin 20160906 : Add file list and dock to move faster
        self.fileListWidget = QListWidget()
tzutalin's avatar
tzutalin committed
173
        self.fileListWidget.itemDoubleClicked.connect(self.fileitemDoubleClicked)
174 175 176
        filelistLayout = QVBoxLayout()
        filelistLayout.setContentsMargins(0, 0, 0, 0)
        filelistLayout.addWidget(self.fileListWidget)
tzutalin's avatar
tzutalin committed
177 178
        fileListContainer = QWidget()
        fileListContainer.setLayout(filelistLayout)
179 180
        self.filedock = QDockWidget(u'File List', self)
        self.filedock.setObjectName(u'Files')
tzutalin's avatar
tzutalin committed
181
        self.filedock.setWidget(fileListContainer)
182

TzuTa Lin's avatar
TzuTa Lin committed
183 184 185
        self.zoomWidget = ZoomWidget()
        self.colorDialog = ColorDialog(parent=self)

186
        self.canvas = Canvas(parent=self)
TzuTa Lin's avatar
TzuTa Lin committed
187 188 189 190 191 192 193 194
        self.canvas.zoomRequest.connect(self.zoomRequest)

        scroll = QScrollArea()
        scroll.setWidget(self.canvas)
        scroll.setWidgetResizable(True)
        self.scrollBars = {
            Qt.Vertical: scroll.verticalScrollBar(),
            Qt.Horizontal: scroll.horizontalScrollBar()
195
        }
Lars Klein's avatar
Lars Klein committed
196
        self.scrollArea = scroll
TzuTa Lin's avatar
TzuTa Lin committed
197 198
        self.canvas.scrollRequest.connect(self.scrollRequest)

199 200 201 202 203 204
        QScroller.grabGesture(scroll.viewport(), QScroller.MiddleMouseButtonGesture)
        properties = QScroller.scroller(scroll.viewport()).scrollerProperties()
        properties.setScrollMetric(QScrollerProperties.VerticalOvershootPolicy, QScrollerProperties.OvershootAlwaysOff)
        properties.setScrollMetric(QScrollerProperties.OvershootScrollTime, 0.2)
        QScroller.scroller(scroll.viewport()).setScrollerProperties(properties)

TzuTa Lin's avatar
TzuTa Lin committed
205 206 207 208 209 210 211
        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(scroll)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock)
212 213
        # Tzutalin 20160906 : Add file list and dock to move faster
        self.addDockWidget(Qt.RightDockWidgetArea, self.filedock)
tzutalin's avatar
tzutalin committed
214
        self.filedock.setFeatures(QDockWidget.DockWidgetFloatable)
215

tzutalin's avatar
tzutalin committed
216
        self.dockFeatures = QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable
TzuTa Lin's avatar
TzuTa Lin committed
217 218 219 220 221
        self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)

        # Actions
        action = partial(newAction, self)
        quit = action('&Quit', self.close,
222
                      'Ctrl+Q', 'quit', u'Quit application')
223

TzuTa Lin's avatar
TzuTa Lin committed
224
        open = action('&Open', self.openFile,
225
                      'Ctrl+O', 'open', u'Open image or label file')
TzuTa Lin's avatar
TzuTa Lin committed
226

227
        opendir = action('&Open Dir', self.openDirDialog,
228
                         'Ctrl+u', 'open', u'Open Dir')
TzuTa Lin's avatar
TzuTa Lin committed
229

230
        changeSavedir = action('&Change Save Dir', self.changeSavedirDialog,
231
                               'Ctrl+r', 'open', u'Change default saved Annotation dir')
TzuTa Lin's avatar
TzuTa Lin committed
232

233
        openAnnotation = action('&Open Annotation', self.openAnnotationDialog,
234
                                'Ctrl+Shift+O', 'open', u'Open Annotation')
235

TzuTa Lin's avatar
TzuTa Lin committed
236
        openNextImg = action('&Next Image', self.openNextImg,
237
                             'd', 'next', u'Open Next')
TzuTa Lin's avatar
TzuTa Lin committed
238

tzutalin's avatar
tzutalin committed
239
        openPrevImg = action('&Prev Image', self.openPrevImg,
240
                             'a', 'prev', u'Open Prev')
tzutalin's avatar
tzutalin committed
241

Thibaut Mattio's avatar
Thibaut Mattio committed
242 243 244
        verify = action('&Verify Image', self.verifyImg,
                        'space', 'verify', u'Verify Image')

TzuTa Lin's avatar
TzuTa Lin committed
245
        save = action('&Save', self.saveFile,
246
                      'Ctrl+S', 'save', u'Save labels to file', enabled=False)
tzutalin's avatar
tzutalin committed
247

Wang Yinghao's avatar
Wang Yinghao committed
248
        save_format = action('&PascalVOC', self.change_format,
Wang Yinghao's avatar
Wang Yinghao committed
249
                      'Ctrl+', 'format_voc', u'Change save format', enabled=True)
Wang Yinghao's avatar
Wang Yinghao committed
250

TzuTa Lin's avatar
TzuTa Lin committed
251
        saveAs = action('&Save As', self.saveFileAs,
tzutalin's avatar
tzutalin committed
252 253 254
                        'Ctrl+Shift+S', 'save-as', u'Save labels to a different file', enabled=False)

        close = action('&Close', self.closeFile, 'Ctrl+W', 'close', u'Close current file')
zhangjie's avatar
zhangjie committed
255

tzutalin's avatar
tzutalin committed
256 257
        resetAll = action('&ResetAll', self.resetAll, None, 'resetall', u'Reset all')

258
        color1 = action('Box Line Color', self.chooseColor1,
259
                        'Ctrl+L', 'color_line', u'Choose Box line color')
TzuTa Lin's avatar
TzuTa Lin committed
260 261

        createMode = action('Create\nRectBox', self.setCreateMode,
262
                            'w', 'new', u'Start drawing Boxs', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
263
        editMode = action('&Edit\nRectBox', self.setEditMode,
264
                          'Ctrl+J', 'edit', u'Move and edit Boxs', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
265 266

        create = action('Create\nRectBox', self.createShape,
267
                        'w', 'new', u'Draw a new Box', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
268
        delete = action('Delete\nRectBox', self.deleteSelectedShape,
269
                        'Delete', 'delete', u'Delete', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
270
        copy = action('&Duplicate\nRectBox', self.copySelectedShape,
271 272
                      'Ctrl+D', 'copy', u'Create a duplicate of the selected Box',
                      enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
273 274

        advancedMode = action('&Advanced Mode', self.toggleAdvancedMode,
275 276
                              'Ctrl+Shift+A', 'expert', u'Switch to advanced mode',
                              checkable=True)
TzuTa Lin's avatar
TzuTa Lin committed
277 278

        hideAll = action('&Hide\nRectBox', partial(self.togglePolygons, False),
279 280
                         'Ctrl+H', 'hide', u'Hide all Boxs',
                         enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
281
        showAll = action('&Show\nRectBox', partial(self.togglePolygons, True),
282 283
                         'Ctrl+A', 'hide', u'Show all Boxs',
                         enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
284

tzutalin's avatar
tzutalin committed
285 286
        help = action('&Tutorial', self.showTutorialDialog, None, 'help', u'Show demos')
        showInfo = action('&Information', self.showInfoDialog, None, 'help', u'Information')
TzuTa Lin's avatar
TzuTa Lin committed
287 288 289 290

        zoom = QWidgetAction(self)
        zoom.setDefaultWidget(self.zoomWidget)
        self.zoomWidget.setWhatsThis(
291 292 293
            u"Zoom in or out of the image. Also accessible with"
            " %s and %s from the canvas." % (fmtShortcut("Ctrl+[-+]"),
                                             fmtShortcut("Ctrl+Wheel")))
TzuTa Lin's avatar
TzuTa Lin committed
294 295 296
        self.zoomWidget.setEnabled(False)

        zoomIn = action('Zoom &In', partial(self.addZoom, 10),
297
                        'Ctrl++', 'zoom-in', u'Increase zoom level', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
298
        zoomOut = action('&Zoom Out', partial(self.addZoom, -10),
299
                         'Ctrl+-', 'zoom-out', u'Decrease zoom level', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
300
        zoomOrg = action('&Original size', partial(self.setZoom, 100),
301
                         'Ctrl+=', 'zoom', u'Zoom to original size', enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
302
        fitWindow = action('&Fit Window', self.setFitWindow,
303 304
                           'Ctrl+F', 'fit-window', u'Zoom follows window size',
                           checkable=True, enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
305
        fitWidth = action('Fit &Width', self.setFitWidth,
306
                          'Ctrl+Shift+F', 'fit-width', u'Zoom follows window width',
307
                          checkable=True, enabled=False)                      
TzuTa Lin's avatar
TzuTa Lin committed
308
        # Group zoom controls into a list for easier toggling.
309 310
        zoomActions = (self.zoomWidget, zoomIn, zoomOut,
                       zoomOrg, fitWindow, fitWidth)
TzuTa Lin's avatar
TzuTa Lin committed
311 312 313 314 315 316 317 318
        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,
        }

319 320
        enhanceImage = action('Enhance Image', self.enhanceImage, tip='Enhance image for better view')

TzuTa Lin's avatar
TzuTa Lin committed
321
        edit = action('&Edit Label', self.editLabel,
322 323
                      'Ctrl+E', 'edit', u'Modify the label of the selected Box',
                      enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
324 325 326
        self.editButton.setDefaultAction(edit)

        shapeLineColor = action('Shape &Line Color', self.chshapeLineColor,
327 328
                                icon='color_line', tip=u'Change the line color for this specific shape',
                                enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
329
        shapeFillColor = action('Shape &Fill Color', self.chshapeFillColor,
330 331
                                icon='color', tip=u'Change the fill color for this specific shape',
                                enabled=False)
TzuTa Lin's avatar
TzuTa Lin committed
332 333 334 335 336 337 338 339 340

        labels = self.dock.toggleViewAction()
        labels.setText('Show/Hide Label Panel')
        labels.setShortcut('Ctrl+Shift+L')

        # Lavel list context menu.
        labelMenu = QMenu()
        addActions(labelMenu, (edit, delete))
        self.labelList.setContextMenuPolicy(Qt.CustomContextMenu)
341 342
        self.labelList.customContextMenuRequested.connect(
            self.popLabelListMenu)
TzuTa Lin's avatar
TzuTa Lin committed
343 344

        # Store actions for further handling.
Wang Yinghao's avatar
Wang Yinghao committed
345
        self.actions = struct(save=save, save_format=save_format, saveAs=saveAs, open=open, close=close, resetAll = resetAll,
346
                              lineColor=color1, create=create, delete=delete, edit=edit, copy=copy,
347 348 349 350 351 352
                              createMode=createMode, editMode=editMode, advancedMode=advancedMode,
                              shapeLineColor=shapeLineColor, shapeFillColor=shapeFillColor,
                              zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg,
                              fitWindow=fitWindow, fitWidth=fitWidth,
                              zoomActions=zoomActions,
                              fileMenuActions=(
tzutalin's avatar
tzutalin committed
353
                                  open, opendir, save, saveAs, close, resetAll, quit),
354 355
                              beginner=(), advanced=(),
                              editMenu=(edit, copy, delete,
356
                                        None, color1),
357 358 359 360 361 362
                              beginnerContext=(create, edit, copy, delete),
                              advancedContext=(createMode, editMode, edit, copy,
                                               delete, shapeLineColor, shapeFillColor),
                              onLoadActive=(
                                  close, create, createMode, editMode),
                              onShapesPresent=(saveAs, hideAll, showAll))
TzuTa Lin's avatar
TzuTa Lin committed
363 364

        self.menus = struct(
365 366 367 368 369 370
            file=self.menu('&File'),
            edit=self.menu('&Edit'),
            view=self.menu('&View'),
            help=self.menu('&Help'),
            recentFiles=QMenu('Open &Recent'),
            labelList=labelMenu)
TzuTa Lin's avatar
TzuTa Lin committed
371

372
        # Auto saving : Enable auto saving if pressing next
373 374
        self.autoSaving = QAction("Auto Saving", self)
        self.autoSaving.setCheckable(True)
375
        self.autoSaving.setChecked(settings.get(SETTING_AUTO_SAVE, False))
376 377 378 379
        # Sync single class mode from PR#106
        self.singleClassMode = QAction("Single Class Mode", self)
        self.singleClassMode.setShortcut("Ctrl+Shift+S")
        self.singleClassMode.setCheckable(True)
380
        self.singleClassMode.setChecked(settings.get(SETTING_SINGLE_CLASS, False))
381
        self.lastLabel = None
382 383 384 385
        # Add option to enable/disable labels being painted at the top of bounding boxes
        self.paintLabelsOption = QAction("Paint Labels", self)
        self.paintLabelsOption.setShortcut("Ctrl+Shift+P")
        self.paintLabelsOption.setCheckable(True)
tzutalin's avatar
tzutalin committed
386
        self.paintLabelsOption.setChecked(settings.get(SETTING_PAINT_LABEL, False))
387
        self.paintLabelsOption.triggered.connect(self.togglePaintLabelsOption)
388

TzuTa Lin's avatar
TzuTa Lin committed
389
        addActions(self.menus.file,
Wang Yinghao's avatar
Wang Yinghao committed
390
                   (open, opendir, changeSavedir, openAnnotation, self.menus.recentFiles, save, save_format, saveAs, close, resetAll, quit))
tzutalin's avatar
tzutalin committed
391
        addActions(self.menus.help, (help, showInfo))
TzuTa Lin's avatar
TzuTa Lin committed
392
        addActions(self.menus.view, (
393 394
            self.autoSaving,
            self.singleClassMode,
395
            self.paintLabelsOption,
TzuTa Lin's avatar
TzuTa Lin committed
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
            labels, advancedMode, None,
            hideAll, showAll, None,
            zoomIn, zoomOut, zoomOrg, None,
            fitWindow, fitWidth))

        self.menus.file.aboutToShow.connect(self.updateFileMenu)

        # Custom context menu for the canvas widget:
        addActions(self.canvas.menus[0], self.actions.beginnerContext)
        addActions(self.canvas.menus[1], (
            action('&Copy here', self.copyShape),
            action('&Move here', self.moveShape)))

        self.tools = self.toolbar('Tools')
        self.actions.beginner = (
Wang Yinghao's avatar
Wang Yinghao committed
411
            open, opendir, changeSavedir, openNextImg, openPrevImg, verify, save, save_format, None, create, copy, delete, None,
412
            zoomIn, zoom, zoomOut, fitWindow, fitWidth, enhanceImage)
TzuTa Lin's avatar
TzuTa Lin committed
413 414

        self.actions.advanced = (
Wang Yinghao's avatar
Wang Yinghao committed
415
            open, opendir, changeSavedir, openNextImg, openPrevImg, save, save_format, None,
TzuTa Lin's avatar
TzuTa Lin committed
416 417 418 419 420 421 422 423
            createMode, editMode, None,
            hideAll, showAll)

        self.statusBar().showMessage('%s started.' % __appname__)
        self.statusBar().show()

        # Application state.
        self.image = QImage()
424
        self.filePath = ustr(defaultFilename)
TzuTa Lin's avatar
TzuTa Lin committed
425 426 427 428 429 430
        self.recentFiles = []
        self.maxRecent = 7
        self.lineColor = None
        self.fillColor = None
        self.zoom_level = 100
        self.fit_window = False
431
        # Add Chris
432
        self.difficult = False
TzuTa Lin's avatar
TzuTa Lin committed
433

434 435 436 437 438 439 440 441 442 443
        ## Fix the compatible issue for qt4 and qt5. Convert the QStringList to python list
        if settings.get(SETTING_RECENT_FILES):
            if have_qstring():
                recentFileQStringList = settings.get(SETTING_RECENT_FILES)
                self.recentFiles = [ustr(i) for i in recentFileQStringList]
            else:
                self.recentFiles = recentFileQStringList = settings.get(SETTING_RECENT_FILES)

        size = settings.get(SETTING_WIN_SIZE, QSize(600, 500))
        position = settings.get(SETTING_WIN_POSE, QPoint(0, 0))
TzuTa Lin's avatar
TzuTa Lin committed
444 445
        self.resize(size)
        self.move(position)
446 447
        saveDir = ustr(settings.get(SETTING_SAVE_DIR, None))
        self.lastOpenDir = ustr(settings.get(SETTING_LAST_OPEN_DIR, None))
jeffrey's avatar
jeffrey committed
448
        if self.defaultSaveDir is None and saveDir is not None and os.path.exists(saveDir):
449
            self.defaultSaveDir = saveDir
450 451
            self.statusBar().showMessage('%s started. Annotation will be saved to %s' %
                                         (__appname__, self.defaultSaveDir))
452 453
            self.statusBar().show()

454
        self.restoreState(settings.get(SETTING_WIN_STATE, QByteArray()))
455 456 457
        Shape.line_color = self.lineColor = QColor(settings.get(SETTING_LINE_COLOR, DEFAULT_LINE_COLOR))
        Shape.fill_color = self.fillColor = QColor(settings.get(SETTING_FILL_COLOR, DEFAULT_FILL_COLOR))
        self.canvas.setDrawingColor(self.lineColor)
458
        # Add chris
tzutalin's avatar
tzutalin committed
459
        Shape.difficult = self.difficult
TzuTa Lin's avatar
TzuTa Lin committed
460

461 462 463 464 465
        def xbool(x):
            if isinstance(x, QVariant):
                return x.toBool()
            return bool(x)

466
        if xbool(settings.get(SETTING_ADVANCE_MODE, False)):
TzuTa Lin's avatar
TzuTa Lin committed
467 468 469 470 471
            self.actions.advancedMode.setChecked(True)
            self.toggleAdvancedMode()

        # Populate the File menu dynamically.
        self.updateFileMenu()
472 473 474 475 476 477

        # Since loading the file may take some time, make sure it runs in the background.
        if self.filePath and os.path.isdir(self.filePath):
            self.queueEvent(partial(self.importDirImages, self.filePath or ""))
        elif self.filePath:
            self.queueEvent(partial(self.loadFile, self.filePath or ""))
TzuTa Lin's avatar
TzuTa Lin committed
478 479 480 481 482 483

        # Callbacks:
        self.zoomWidget.valueChanged.connect(self.paintCanvas)

        self.populateModeActions()

484 485 486 487
        # Display cursor coordinates at the right of status bar
        self.labelCoordinates = QLabel('')
        self.statusBar().addPermanentWidget(self.labelCoordinates)

488
        # open dir if defaultFilename is a directory
489
        if self.filePath and os.path.isdir(self.filePath):
490
            self.importDirImages(self.filePath)
491

TzuTa Lin's avatar
TzuTa Lin committed
492
    ## Support Functions ##
Wang Yinghao's avatar
Wang Yinghao committed
493
    def set_format(self, save_format):
tzutalin's avatar
tzutalin committed
494 495
        if save_format == FORMAT_PASCALVOC:
            self.actions.save_format.setText(FORMAT_PASCALVOC)
Wang Yinghao's avatar
Wang Yinghao committed
496
            self.actions.save_format.setIcon(newIcon("format_voc"))
Wang Yinghao's avatar
Wang Yinghao committed
497 498
            self.usingPascalVocFormat = True
            self.usingYoloFormat = False
vdalv's avatar
vdalv committed
499
            LabelFile.suffix = XML_EXT
Wang Yinghao's avatar
Wang Yinghao committed
500

tzutalin's avatar
tzutalin committed
501 502
        elif save_format == FORMAT_YOLO:
            self.actions.save_format.setText(FORMAT_YOLO)
Wang Yinghao's avatar
Wang Yinghao committed
503
            self.actions.save_format.setIcon(newIcon("format_yolo"))
Wang Yinghao's avatar
Wang Yinghao committed
504 505
            self.usingPascalVocFormat = False
            self.usingYoloFormat = True
vdalv's avatar
vdalv committed
506
            LabelFile.suffix = TXT_EXT
TzuTa Lin's avatar
TzuTa Lin committed
507

Wang Yinghao's avatar
Wang Yinghao committed
508
    def change_format(self):
tzutalin's avatar
tzutalin committed
509 510
        if self.usingPascalVocFormat: self.set_format(FORMAT_YOLO)
        elif self.usingYoloFormat: self.set_format(FORMAT_PASCALVOC)
Wang Yinghao's avatar
Wang Yinghao committed
511

TzuTa Lin's avatar
TzuTa Lin committed
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
    def noShapes(self):
        return not self.itemsToShapes

    def toggleAdvancedMode(self, value=True):
        self._beginner = not value
        self.canvas.setEditing(True)
        self.populateModeActions()
        self.editButton.setVisible(not value)
        if value:
            self.actions.createMode.setEnabled(True)
            self.actions.editMode.setEnabled(False)
            self.dock.setFeatures(self.dock.features() | self.dockFeatures)
        else:
            self.dock.setFeatures(self.dock.features() ^ self.dockFeatures)

    def populateModeActions(self):
        if self.beginner():
            tool, menu = self.actions.beginner, self.actions.beginnerContext
        else:
            tool, menu = self.actions.advanced, self.actions.advancedContext
        self.tools.clear()
        addActions(self.tools, tool)
        self.canvas.menus[0].clear()
        addActions(self.canvas.menus[0], menu)
        self.menus.edit.clear()
        actions = (self.actions.create,) if self.beginner()\
538
            else (self.actions.createMode, self.actions.editMode)
TzuTa Lin's avatar
TzuTa Lin committed
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
        addActions(self.menus.edit, actions + self.actions.editMenu)

    def setBeginner(self):
        self.tools.clear()
        addActions(self.tools, self.actions.beginner)

    def setAdvanced(self):
        self.tools.clear()
        addActions(self.tools, self.actions.advanced)

    def setDirty(self):
        self.dirty = True
        self.actions.save.setEnabled(True)

    def setClean(self):
        self.dirty = False
        self.actions.save.setEnabled(False)
        self.actions.create.setEnabled(True)

    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):
        QTimer.singleShot(0, function)

    def status(self, message, delay=5000):
        self.statusBar().showMessage(message, delay)

    def resetState(self):
        self.itemsToShapes.clear()
        self.shapesToItems.clear()
        self.labelList.clear()
575
        self.filePath = None
TzuTa Lin's avatar
TzuTa Lin committed
576 577 578
        self.imageData = None
        self.labelFile = None
        self.canvas.resetState()
579
        self.labelCoordinates.clear()
TzuTa Lin's avatar
TzuTa Lin committed
580 581 582 583 584 585 586

    def currentItem(self):
        items = self.labelList.selectedItems()
        if items:
            return items[0]
        return None

587 588 589
    def addRecentFile(self, filePath):
        if filePath in self.recentFiles:
            self.recentFiles.remove(filePath)
TzuTa Lin's avatar
TzuTa Lin committed
590 591
        elif len(self.recentFiles) >= self.maxRecent:
            self.recentFiles.pop()
592
        self.recentFiles.insert(0, filePath)
TzuTa Lin's avatar
TzuTa Lin committed
593 594 595 596 597 598 599

    def beginner(self):
        return self._beginner

    def advanced(self):
        return not self.beginner()

600 601 602 603 604 605 606 607 608 609
    def getAvailableScreencastViewer(self):
        osName = platform.system()

        if osName == 'Windows':
            return ['C:\\Program Files\\Internet Explorer\\iexplore.exe']
        elif osName == 'Linux':
            return ['xdg-open']
        elif osName == 'Darwin':
            return ['open', '-a', 'Safari']

TzuTa Lin's avatar
TzuTa Lin committed
610
    ## Callbacks ##
tzutalin's avatar
tzutalin committed
611
    def showTutorialDialog(self):
612
        subprocess.Popen(self.screencastViewer + [self.screencast])
TzuTa Lin's avatar
TzuTa Lin committed
613

tzutalin's avatar
tzutalin committed
614 615 616 617
    def showInfoDialog(self):
        msg = u'Name:{0} \nApp Version:{1} \n{2} '.format(__appname__, __version__, sys.version_info)
        QMessageBox.information(self, u'Information', msg)

TzuTa Lin's avatar
TzuTa Lin committed
618 619 620 621 622 623 624 625 626 627
    def createShape(self):
        assert self.beginner()
        self.canvas.setEditing(False)
        self.actions.create.setEnabled(False)

    def toggleDrawingSensitive(self, drawing=True):
        """In the middle of drawing, toggling between modes should be disabled."""
        self.actions.editMode.setEnabled(not drawing)
        if not drawing and self.beginner():
            # Cancel creation.
628
            print('Cancel creation.')
TzuTa Lin's avatar
TzuTa Lin committed
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
            self.canvas.setEditing(True)
            self.canvas.restoreCursor()
            self.actions.create.setEnabled(True)

    def toggleDrawMode(self, edit=True):
        self.canvas.setEditing(edit)
        self.actions.createMode.setEnabled(edit)
        self.actions.editMode.setEnabled(not edit)

    def setCreateMode(self):
        assert self.advanced()
        self.toggleDrawMode(False)

    def setEditMode(self):
        assert self.advanced()
        self.toggleDrawMode(True)
645
        self.labelSelectionChanged()
TzuTa Lin's avatar
TzuTa Lin committed
646 647

    def updateFileMenu(self):
648
        currFilePath = self.filePath
649

TzuTa Lin's avatar
TzuTa Lin committed
650
        def exists(filename):
651
            return os.path.exists(filename)
TzuTa Lin's avatar
TzuTa Lin committed
652 653
        menu = self.menus.recentFiles
        menu.clear()
654 655
        files = [f for f in self.recentFiles if f !=
                 currFilePath and exists(f)]
TzuTa Lin's avatar
TzuTa Lin committed
656 657 658
        for i, f in enumerate(files):
            icon = newIcon('labels')
            action = QAction(
659
                icon, '&%d %s' % (i + 1, QFileInfo(f).fileName()), self)
TzuTa Lin's avatar
TzuTa Lin committed
660 661 662 663 664 665
            action.triggered.connect(partial(self.loadRecent, f))
            menu.addAction(action)

    def popLabelListMenu(self, point):
        self.menus.labelList.exec_(self.labelList.mapToGlobal(point))

666
    def editLabel(self):
TzuTa Lin's avatar
TzuTa Lin committed
667 668
        if not self.canvas.editing():
            return
669
        item = self.currentItem()
TzuTa Lin's avatar
TzuTa Lin committed
670 671 672
        text = self.labelDialog.popUp(item.text())
        if text is not None:
            item.setText(text)
zhangjie's avatar
zhangjie committed
673
            item.setBackground(generateColorByText(text))
TzuTa Lin's avatar
TzuTa Lin committed
674
            self.setDirty()
675

676 677
    # Tzutalin 20160906 : Add file list and dock to move faster
    def fileitemDoubleClicked(self, item=None):
678
        currIndex = self.mImgList.index(ustr(item.text()))
679
        if currIndex < len(self.mImgList):
680 681 682
            filename = self.mImgList[currIndex]
            if filename:
                self.loadFile(filename)
683 684

    # Add chris
685
    def btnstate(self, item= None):
686
        """ Function to handle difficult examples
687 688 689
        Update on each object """
        if not self.canvas.editing():
            return
690

691
        item = self.currentItem()
692
        if not item: # If not selected Item, take the first one
693 694 695 696 697 698 699 700
            item = self.labelList.item(self.labelList.count()-1)

        difficult = self.diffcButton.isChecked()

        try:
            shape = self.itemsToShapes[item]
        except:
            pass
701
        # Checked and Update
702 703 704 705 706 707 708 709
        try:
            if difficult != shape.difficult:
                shape.difficult = difficult
                self.setDirty()
            else:  # User probably changed item visibility
                self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
        except:
            pass
TzuTa Lin's avatar
TzuTa Lin committed
710 711 712 713 714 715 716 717

    # React to canvas signals.
    def shapeSelectionChanged(self, selected=False):
        if self._noSelectionSlot:
            self._noSelectionSlot = False
        else:
            shape = self.canvas.selectedShape
            if shape:
718
                self.shapesToItems[shape].setSelected(True)
TzuTa Lin's avatar
TzuTa Lin committed
719 720 721 722 723 724 725 726 727
            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):
728
        shape.paintLabel = self.paintLabelsOption.isChecked()
729
        item = HashableQListWidgetItem(shape.label)
TzuTa Lin's avatar
TzuTa Lin committed
730 731
        item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
        item.setCheckState(Qt.Checked)
732
        item.setBackground(generateColorByText(shape.label))
TzuTa Lin's avatar
TzuTa Lin committed
733 734 735 736 737 738 739
        self.itemsToShapes[item] = shape
        self.shapesToItems[shape] = item
        self.labelList.addItem(item)
        for action in self.actions.onShapesPresent:
            action.setEnabled(True)

    def remLabel(self, shape):
740 741 742
        if shape is None:
            # print('rm empty label')
            return
TzuTa Lin's avatar
TzuTa Lin committed
743 744 745 746 747 748 749
        item = self.shapesToItems[shape]
        self.labelList.takeItem(self.labelList.row(item))
        del self.shapesToItems[shape]
        del self.itemsToShapes[item]

    def loadLabels(self, shapes):
        s = []
750
        for label, points, box, line_color, fill_color, difficult in shapes:
TzuTa Lin's avatar
TzuTa Lin committed
751
            shape = Shape(label=label)
752 753 754 755 756 757 758 759 760
            center = QPointF(*box[0])
            width, height = box[1]
            angle = box[2]
            shape.addPoint(QPointF(center.x() - width / 2, center.y() - height / 2))
            shape.addPoint(QPointF(center.x() + width / 2, center.y() - height / 2))
            shape.addPoint(QPointF(center.x() + width / 2, center.y() + height / 2))
            shape.addPoint(QPointF(center.x() - width / 2, center.y() + height / 2))
            shape.applyRotationAngle(angle, center)
            shape.updateState()
761
            shape.difficult = difficult
TzuTa Lin's avatar
TzuTa Lin committed
762 763
            shape.close()
            s.append(shape)
764 765
            shape.line_color = QColor(*line_color) if line_color else generateColorByText(label)
            shape.fill_color = QColor(*fill_color) if fill_color else generateColorByText(label)
766
            self.addLabel(shape)
767

TzuTa Lin's avatar
TzuTa Lin committed
768 769
        self.canvas.loadShapes(s)

770
    def saveLabels(self, annotationFilePath):
771
        annotationFilePath = ustr(annotationFilePath)
Thibaut Mattio's avatar
Thibaut Mattio committed
772 773 774
        if self.labelFile is None:
            self.labelFile = LabelFile()
            self.labelFile.verified = self.canvas.verified
775

TzuTa Lin's avatar
TzuTa Lin committed
776
        def format_shape(s):
777
            s.updateState()
778
            return dict(label=s.label,
779 780
                        line_color=s.line_color.getRgb(),
                        fill_color=s.fill_color.getRgb(),
781 782
                        points=[(p.x(), p.y()) for p in s.points],
                       # add chris
783 784 785 786 787
                        difficult=s.difficult,
                        center=[s.center.x(), s.center.y()],
                        width=s.width,
                        height=s.height,
                        angle=s.currentAngle)
TzuTa Lin's avatar
TzuTa Lin committed
788 789

        shapes = [format_shape(shape) for shape in self.canvas.shapes]
790
        # Can add differrent annotation formats here
TzuTa Lin's avatar
TzuTa Lin committed
791
        try:
TzuTa Lin's avatar
Typo  
TzuTa Lin committed
792
            if self.usingPascalVocFormat is True:
tzutalin's avatar
tzutalin committed
793
                if ustr(annotationFilePath[-4:]) != ".xml":
794
                    annotationFilePath += XML_EXT
795
                print ('Img: ' + self.filePath + ' -> Its xml: ' + annotationFilePath)
Thibaut Mattio's avatar
Thibaut Mattio committed
796
                self.labelFile.savePascalVocFormat(annotationFilePath, shapes, self.filePath, self.imageData,
797
                                                   self.lineColor.getRgb(), self.fillColor.getRgb(), version=__version__)
Wang Yinghao's avatar
Wang Yinghao committed
798
            elif self.usingYoloFormat is True:
799 800
                if annotationFilePath[-4:] != ".txt":
                    annotationFilePath += TXT_EXT
Wang Yinghao's avatar
Wang Yinghao committed
801 802 803
                print ('Img: ' + self.filePath + ' -> Its txt: ' + annotationFilePath)
                self.labelFile.saveYoloFormat(annotationFilePath, shapes, self.filePath, self.imageData, self.labelHist,
                                                   self.lineColor.getRgb(), self.fillColor.getRgb())
TzuTa Lin's avatar
TzuTa Lin committed
804
            else:
Thibaut Mattio's avatar
Thibaut Mattio committed
805 806
                self.labelFile.save(annotationFilePath, shapes, self.filePath, self.imageData,
                                    self.lineColor.getRgb(), self.fillColor.getRgb())
TzuTa Lin's avatar
TzuTa Lin committed
807
            return True
808
        except LabelFileError as e:
809
            self.errorMessage(u'Error saving label data', u'<b>%s</b>' % e)
TzuTa Lin's avatar
TzuTa Lin committed
810 811 812 813
            return False

    def copySelectedShape(self):
        self.addLabel(self.canvas.copySelectedShape())
814
        # fix copy and delete
TzuTa Lin's avatar
TzuTa Lin committed
815 816 817 818 819 820 821
        self.shapeSelectionChanged(True)

    def labelSelectionChanged(self):
        item = self.currentItem()
        if item and self.canvas.editing():
            self._noSelectionSlot = True
            self.canvas.selectShape(self.itemsToShapes[item])
ChrisDal's avatar
ChrisDal committed
822
            shape = self.itemsToShapes[item]
823 824
            # Add Chris
            self.diffcButton.setChecked(shape.difficult)
TzuTa Lin's avatar
TzuTa Lin committed
825 826 827

    def labelItemChanged(self, item):
        shape = self.itemsToShapes[item]
828
        label = item.text()
TzuTa Lin's avatar
TzuTa Lin committed
829
        if label != shape.label:
830
            shape.label = item.text()
zhangjie's avatar
zhangjie committed
831
            shape.line_color = generateColorByText(shape.label)
TzuTa Lin's avatar
TzuTa Lin committed
832
            self.setDirty()
833
        else:  # User probably changed item visibility
TzuTa Lin's avatar
TzuTa Lin committed
834 835
            self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)

836
    # Callback functions:
TzuTa Lin's avatar
TzuTa Lin committed
837 838 839 840 841
    def newShape(self):
        """Pop-up and give focus to the label editor.

        position MUST be in global coordinates.
        """
842
        if not self.useDefaultLabelCheckbox.isChecked() or not self.defaultLabelTextLine.text():
RegisWANG's avatar
RegisWANG committed
843 844 845 846
            if len(self.labelHist) > 0:
                self.labelDialog = LabelDialog(
                    parent=self, listItem=self.labelHist)

847 848 849 850 851 852
            # Sync single class mode from PR#106
            if self.singleClassMode.isChecked() and self.lastLabel:
                text = self.lastLabel
            else:
                text = self.labelDialog.popUp(text=self.prevLabelText)
                self.lastLabel = text
RegisWANG's avatar
RegisWANG committed
853
        else:
tzutalin's avatar
tzutalin committed
854
            text = self.defaultLabelTextLine.text()
855

856 857
        # Add Chris
        self.diffcButton.setChecked(False)
TzuTa Lin's avatar
TzuTa Lin committed
858
        if text is not None:
859
            self.prevLabelText = text
860 861 862
            generate_color = generateColorByText(text)
            shape = self.canvas.setLastLabel(text, generate_color, generate_color)
            self.addLabel(shape)
863
            if self.beginner():  # Switch to edit mode.
TzuTa Lin's avatar
TzuTa Lin committed
864 865 866 867 868
                self.canvas.setEditing(True)
                self.actions.create.setEnabled(True)
            else:
                self.actions.editMode.setEnabled(True)
            self.setDirty()
869 870 871

            if text not in self.labelHist:
                self.labelHist.append(text)
TzuTa Lin's avatar
TzuTa Lin committed
872
        else:
873
            # self.canvas.undoLastLine()
TzuTa Lin's avatar
TzuTa Lin committed
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
            self.canvas.resetAllLines()

    def scrollRequest(self, delta, orientation):
        units = - delta / (8 * 15)
        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):
Lars Klein's avatar
Lars Klein committed
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
        # get the current scrollbar positions
        # calculate the percentages ~ coordinates
        h_bar = self.scrollBars[Qt.Horizontal]
        v_bar = self.scrollBars[Qt.Vertical]

        # get the current maximum, to know the difference after zooming
        h_bar_max = h_bar.maximum()
        v_bar_max = v_bar.maximum()

        # get the cursor position and canvas size
        # calculate the desired movement from 0 to 1
        # where 0 = move left
        #       1 = move right
        # up and down analogous
        cursor = QCursor()
        pos = cursor.pos()
907
        relative_pos = QWidget.mapFromGlobal(self, pos)
Lars Klein's avatar
Lars Klein committed
908

909 910
        cursor_x = relative_pos.x()
        cursor_y = relative_pos.y()
Lars Klein's avatar
Lars Klein committed
911 912 913 914

        w = self.scrollArea.width()
        h = self.scrollArea.height()

Lars Klein's avatar
Lars Klein committed
915 916
        # the scaling from 0 to 1 has some padding
        # you don't have to hit the very leftmost pixel for a maximum-left movement
Lars Klein's avatar
Lars Klein committed
917 918 919 920
        margin = 0.1
        move_x = (cursor_x - margin * w) / (w - 2 * margin * w)
        move_y = (cursor_y - margin * h) / (h - 2 * margin * h)

921
        # clamp the values from 0 to 1
Lars Klein's avatar
Lars Klein committed
922 923 924
        move_x = min(max(move_x, 0), 1)
        move_y = min(max(move_y, 0), 1)

Lars Klein's avatar
Lars Klein committed
925
        # zoom in
TzuTa Lin's avatar
TzuTa Lin committed
926 927 928 929
        units = delta / (8 * 15)
        scale = 10
        self.addZoom(scale * units)

Lars Klein's avatar
Lars Klein committed
930 931
        # get the difference in scrollbar values
        # this is how far we can move
Lars Klein's avatar
Lars Klein committed
932 933 934 935 936 937 938 939 940 941
        d_h_bar_max = h_bar.maximum() - h_bar_max
        d_v_bar_max = v_bar.maximum() - v_bar_max

        # get the new scrollbar values
        new_h_bar_value = h_bar.value() + move_x * d_h_bar_max
        new_v_bar_value = v_bar.value() + move_y * d_v_bar_max

        h_bar.setValue(new_h_bar_value)
        v_bar.setValue(new_v_bar_value)

TzuTa Lin's avatar
TzuTa Lin committed
942 943 944 945 946 947 948 949 950 951 952 953 954
    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):
955
        for item, shape in self.itemsToShapes.items():
TzuTa Lin's avatar
TzuTa Lin committed
956 957
            item.setCheckState(Qt.Checked if value else Qt.Unchecked)

958 959 960 961 962 963 964 965 966
    def enhanceImage(self, value=True):
        if not self.image.isNull():
            pil_image = Image.fromqimage(self.image)
            enhancer = ImageEnhance.Brightness(pil_image)
            self.image = enhancer.enhance(2).toqimage()
            shapes = self.canvas.shapes
            self.canvas.loadPixmap(QPixmap.fromImage(self.image))
            self.canvas.loadShapes(shapes)

967
    def loadFile(self, filePath=None):
TzuTa Lin's avatar
TzuTa Lin committed
968 969 970
        """Load the specified file, or the last opened file if None."""
        self.resetState()
        self.canvas.setEnabled(False)
971
        if filePath is None:
972
            filePath = self.settings.get(SETTING_FILENAME)
973

Tomas Raila's avatar
Tomas Raila committed
974
        # Make sure that filePath is a regular python string, rather than QString
tzutalin's avatar
tzutalin committed
975
        filePath = ustr(filePath)
Tomas Raila's avatar
Tomas Raila committed
976

977
        unicodeFilePath = ustr(filePath)
978
        # Tzutalin 20160906 : Add file list and dock to move faster
979
        # Highlight the file item
980 981
        if unicodeFilePath and self.fileListWidget.count() > 0:
            index = self.mImgList.index(unicodeFilePath)
982
            fileWidgetItem = self.fileListWidget.item(index)
983
            fileWidgetItem.setSelected(True)
984

985 986
        if unicodeFilePath and os.path.exists(unicodeFilePath):
            if LabelFile.isLabelFile(unicodeFilePath):
TzuTa Lin's avatar
TzuTa Lin committed
987
                try: