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:
988
                    self.labelFile = LabelFile(unicodeFilePath)
989
                except LabelFileError as e: