├── __init__.py
├── resources
├── icons
│ ├── app.icns
│ ├── app.png
│ ├── copy.png
│ ├── done.png
│ ├── edit.png
│ ├── eye.png
│ ├── file.png
│ ├── fit.png
│ ├── help.png
│ ├── new.png
│ ├── next.png
│ ├── open.png
│ ├── prev.png
│ ├── quit.png
│ ├── save.png
│ ├── undo.png
│ ├── zoom.png
│ ├── cancel.png
│ ├── close.png
│ ├── color.png
│ ├── delete.png
│ ├── expert1.png
│ ├── expert2.png
│ ├── labels.png
│ ├── objects.png
│ ├── save-as.png
│ ├── verify.png
│ ├── zoom-in.png
│ ├── fit-width.png
│ ├── resetall.png
│ ├── zoom-out.png
│ ├── color_line.png
│ ├── feBlend-icon.png
│ ├── fit-window.png
│ ├── format_voc.png
│ ├── format_yolo.png
│ ├── undo-cross.png
│ ├── app.svg
│ ├── open.svg
│ ├── done.svg
│ └── save.svg
└── strings
│ ├── strings-zh-TW.properties
│ ├── strings-zh-CN.properties
│ └── strings.properties
├── tests
├── .gitignore
├── 臉書.jpg
├── test.512.512.bmp
├── test_qt.py
├── test_utils.py
├── test_settings.py
├── test_stringBundle.py
└── test_io.py
├── requirements
└── requirements-linux-python3.txt
├── demo
├── demo.jpg
├── demo3.jpg
├── demo4.png
└── demo5.png
├── libs
├── __init__.py
├── ustr.py
├── constants.py
├── zoomWidget.py
├── hashableQListWidgetItem.py
├── toolBar.py
├── settings.py
├── colorDialog.py
├── stringBundle.py
├── labelDialog.py
├── utils.py
├── yolo_io.py
├── labelFile.py
├── pascal_voc_io.py
├── shape.py
└── canvas.py
├── CONTRIBUTING.rst
├── setup.cfg
├── issue_template.md
├── data
└── predefined_classes.txt
├── MANIFEST.in
├── .gitignore
├── Makefile
├── combobox.py
├── LICENSE
├── .travis.yml
├── HISTORY.rst
├── resources.qrc
├── setup.py
└── README.rst
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/app.icns:
--------------------------------------------------------------------------------
1 | icns
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | test.xml
2 |
--------------------------------------------------------------------------------
/requirements/requirements-linux-python3.txt:
--------------------------------------------------------------------------------
1 | pyqt5==5.10.1
2 | lxml==4.2.4
3 |
--------------------------------------------------------------------------------
/tests/臉書.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/tests/臉書.jpg
--------------------------------------------------------------------------------
/demo/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/demo/demo.jpg
--------------------------------------------------------------------------------
/demo/demo3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/demo/demo3.jpg
--------------------------------------------------------------------------------
/demo/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/demo/demo4.png
--------------------------------------------------------------------------------
/demo/demo5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/demo/demo5.png
--------------------------------------------------------------------------------
/tests/test.512.512.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/tests/test.512.512.bmp
--------------------------------------------------------------------------------
/libs/__init__.py:
--------------------------------------------------------------------------------
1 | __version_info__ = ('1', '8', '2')
2 | __version__ = '.'.join(__version_info__)
3 |
--------------------------------------------------------------------------------
/resources/icons/app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/app.png
--------------------------------------------------------------------------------
/resources/icons/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/copy.png
--------------------------------------------------------------------------------
/resources/icons/done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/done.png
--------------------------------------------------------------------------------
/resources/icons/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/edit.png
--------------------------------------------------------------------------------
/resources/icons/eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/eye.png
--------------------------------------------------------------------------------
/resources/icons/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/file.png
--------------------------------------------------------------------------------
/resources/icons/fit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/fit.png
--------------------------------------------------------------------------------
/resources/icons/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/help.png
--------------------------------------------------------------------------------
/resources/icons/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/new.png
--------------------------------------------------------------------------------
/resources/icons/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/next.png
--------------------------------------------------------------------------------
/resources/icons/open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/open.png
--------------------------------------------------------------------------------
/resources/icons/prev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/prev.png
--------------------------------------------------------------------------------
/resources/icons/quit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/quit.png
--------------------------------------------------------------------------------
/resources/icons/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/save.png
--------------------------------------------------------------------------------
/resources/icons/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/undo.png
--------------------------------------------------------------------------------
/resources/icons/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/zoom.png
--------------------------------------------------------------------------------
/resources/icons/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/cancel.png
--------------------------------------------------------------------------------
/resources/icons/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/close.png
--------------------------------------------------------------------------------
/resources/icons/color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/color.png
--------------------------------------------------------------------------------
/resources/icons/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/delete.png
--------------------------------------------------------------------------------
/resources/icons/expert1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/expert1.png
--------------------------------------------------------------------------------
/resources/icons/expert2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/expert2.png
--------------------------------------------------------------------------------
/resources/icons/labels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/labels.png
--------------------------------------------------------------------------------
/resources/icons/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/objects.png
--------------------------------------------------------------------------------
/resources/icons/save-as.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/save-as.png
--------------------------------------------------------------------------------
/resources/icons/verify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/verify.png
--------------------------------------------------------------------------------
/resources/icons/zoom-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/zoom-in.png
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | TzuTa Lin
2 | [LabelMe](http://labelme2.csail.mit.edu/Release3.0/index.php)
3 | Ryan Flynn
4 |
--------------------------------------------------------------------------------
/resources/icons/fit-width.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/fit-width.png
--------------------------------------------------------------------------------
/resources/icons/resetall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/resetall.png
--------------------------------------------------------------------------------
/resources/icons/zoom-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/zoom-out.png
--------------------------------------------------------------------------------
/resources/icons/color_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/color_line.png
--------------------------------------------------------------------------------
/resources/icons/feBlend-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/feBlend-icon.png
--------------------------------------------------------------------------------
/resources/icons/fit-window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/fit-window.png
--------------------------------------------------------------------------------
/resources/icons/format_voc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/format_voc.png
--------------------------------------------------------------------------------
/resources/icons/format_yolo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/format_yolo.png
--------------------------------------------------------------------------------
/resources/icons/undo-cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranjalAI/labelImg/HEAD/resources/icons/undo-cross.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | commit = True
3 | tag = True
4 |
5 | [bumpversion:file:setup.py]
6 |
7 | [bdist_wheel]
8 | universal = 1
9 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | - **OS:**
7 | - **PyQt version:**
8 |
--------------------------------------------------------------------------------
/data/predefined_classes.txt:
--------------------------------------------------------------------------------
1 | dog
2 | person
3 | cat
4 | tv
5 | car
6 | meatballs
7 | marinara sauce
8 | tomato soup
9 | chicken noodle soup
10 | french onion soup
11 | chicken breast
12 | ribs
13 | pulled pork
14 | hamburger
15 | cavity
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CONTRIBUTING.rst
2 | include HISTORY.rst
3 | include LICENSE
4 | include README.rst
5 |
6 | include resources.qrc
7 |
8 | recursive-include data *
9 | recursive-include icons *
10 | recursive-include libs *
11 |
12 | recursive-exclude build-tools *
13 | recursive-exclude tests *
14 | recursive-exclude * __pycache__
15 | recursive-exclude * *.py[co]
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | resources/icons/.DS_Store
2 |
3 | resources.py
4 |
5 | .idea*
6 | labelImg.egg-info*
7 |
8 | *.pyc
9 | .*.swp
10 |
11 | build/
12 | dist/
13 |
14 | tags
15 | cscope*
16 | .ycm_extra_conf.py
17 | .subvimrc
18 | .vscode
19 | *.pkl
20 |
21 | # MacOS System-Generated
22 | .DS_Store
23 | .DS_Store?
24 | ._*
25 | .Spotlight-V100
26 | .Trashes
27 | ehthumbs.db
28 | Thumbs.db
29 |
--------------------------------------------------------------------------------
/tests/test_qt.py:
--------------------------------------------------------------------------------
1 |
2 | from unittest import TestCase
3 |
4 | from labelImg import get_main_app
5 |
6 |
7 | class TestMainWindow(TestCase):
8 |
9 | app = None
10 | win = None
11 |
12 | def setUp(self):
13 | self.app, self.win = get_main_app()
14 |
15 | def tearDown(self):
16 | self.win.close()
17 | self.app.quit()
18 |
19 | def test_noop(self):
20 | pass
21 |
--------------------------------------------------------------------------------
/libs/ustr.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from libs.constants import DEFAULT_ENCODING
3 |
4 | def ustr(x):
5 | '''py2/py3 unicode helper'''
6 |
7 | if sys.version_info < (3, 0, 0):
8 | from PyQt4.QtCore import QString
9 | if type(x) == str:
10 | return x.decode(DEFAULT_ENCODING)
11 | if type(x) == QString:
12 | #https://blog.csdn.net/friendan/article/details/51088476
13 | #https://blog.csdn.net/xxm524/article/details/74937308
14 | return unicode(x.toUtf8(), DEFAULT_ENCODING, 'ignore')
15 | return x
16 | else:
17 | return x
18 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # ex: set ts=8 noet:
2 |
3 | all: qt5 test
4 |
5 | test: testpy3
6 |
7 | testpy2:
8 | python -m unittest discover tests
9 |
10 | testpy3:
11 | python3 -m unittest discover tests
12 |
13 | qt4: qt4py2
14 |
15 | qt5: qt5py3
16 |
17 | qt4py2:
18 | pyrcc4 -py2 -o libs/resources.py resources.qrc
19 |
20 | qt4py3:
21 | pyrcc4 -py3 -o libs/resources.py resources.qrc
22 |
23 | qt5py3:
24 | pyrcc5 -o libs/resources.py resources.qrc
25 |
26 | clean:
27 | rm -rf ~/.labelImgSettings.pkl *.pyc dist labelImg.egg-info __pycache__ build
28 |
29 | pip_upload:
30 | python3 setup.py upload
31 |
32 | long_description:
33 | restview --long-description
34 |
35 | .PHONY: all
36 |
--------------------------------------------------------------------------------
/libs/constants.py:
--------------------------------------------------------------------------------
1 | SETTING_FILENAME = 'filename'
2 | SETTING_RECENT_FILES = 'recentFiles'
3 | SETTING_WIN_SIZE = 'window/size'
4 | SETTING_WIN_POSE = 'window/position'
5 | SETTING_WIN_GEOMETRY = 'window/geometry'
6 | SETTING_LINE_COLOR = 'line/color'
7 | SETTING_FILL_COLOR = 'fill/color'
8 | SETTING_ADVANCE_MODE = 'advanced'
9 | SETTING_WIN_STATE = 'window/state'
10 | SETTING_SAVE_DIR = 'savedir'
11 | SETTING_PAINT_LABEL = 'paintlabel'
12 | SETTING_LAST_OPEN_DIR = 'lastOpenDir'
13 | SETTING_AUTO_SAVE = 'autosave'
14 | SETTING_SINGLE_CLASS = 'singleclass'
15 | FORMAT_PASCALVOC='PascalVOC'
16 | FORMAT_YOLO='YOLO'
17 | SETTING_DRAW_SQUARE = 'draw/square'
18 | SETTING_LABEL_FILE_FORMAT= 'labelFileFormat'
19 | DEFAULT_ENCODING = 'utf-8'
20 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import unittest
4 | from libs.utils import struct, newAction, newIcon, addActions, fmtShortcut, generateColorByText, natural_sort
5 |
6 | class TestUtils(unittest.TestCase):
7 |
8 | def test_generateColorByGivingUniceText_noError(self):
9 | res = generateColorByText(u'\u958B\u555F\u76EE\u9304')
10 | self.assertTrue(res.green() >= 0)
11 | self.assertTrue(res.red() >= 0)
12 | self.assertTrue(res.blue() >= 0)
13 |
14 | def test_nautalSort_noError(self):
15 | l1 = ['f1', 'f11', 'f3' ]
16 | exptected_l1 = ['f1', 'f3', 'f11']
17 | natural_sort(l1)
18 | for idx, val in enumerate(l1):
19 | self.assertTrue(val == exptected_l1[idx])
20 |
21 | if __name__ == '__main__':
22 | unittest.main()
23 |
--------------------------------------------------------------------------------
/libs/zoomWidget.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 |
10 | class ZoomWidget(QSpinBox):
11 |
12 | def __init__(self, value=100):
13 | super(ZoomWidget, self).__init__()
14 | self.setButtonSymbols(QAbstractSpinBox.NoButtons)
15 | self.setRange(1, 500)
16 | self.setSuffix(' %')
17 | self.setValue(value)
18 | self.setToolTip(u'Zoom Level')
19 | self.setStatusTip(self.toolTip())
20 | self.setAlignment(Qt.AlignCenter)
21 |
22 | def minimumSizeHint(self):
23 | height = super(ZoomWidget, self).minimumSizeHint().height()
24 | fm = QFontMetrics(self.font())
25 | width = fm.width(str(self.maximum()))
26 | return QSize(width, height)
27 |
--------------------------------------------------------------------------------
/libs/hashableQListWidgetItem.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import sys
4 | try:
5 | from PyQt5.QtGui import *
6 | from PyQt5.QtCore import *
7 | from PyQt5.QtWidgets import *
8 | except ImportError:
9 | # needed for py3+qt4
10 | # Ref:
11 | # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
12 | # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
13 | if sys.version_info.major >= 3:
14 | import sip
15 | sip.setapi('QVariant', 2)
16 | from PyQt4.QtGui import *
17 | from PyQt4.QtCore import *
18 |
19 | # PyQt5: TypeError: unhashable type: 'QListWidgetItem'
20 |
21 |
22 | class HashableQListWidgetItem(QListWidgetItem):
23 |
24 | def __init__(self, *args):
25 | super(HashableQListWidgetItem, self).__init__(*args)
26 |
27 | def __hash__(self):
28 | return hash(id(self))
29 |
--------------------------------------------------------------------------------
/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 | import time
5 | import unittest
6 |
7 | __author__ = 'TzuTaLin'
8 |
9 | dir_name = os.path.abspath(os.path.dirname(__file__))
10 | libs_path = os.path.join(dir_name, '..', 'libs')
11 | sys.path.insert(0, libs_path)
12 | from settings import Settings
13 |
14 | class TestSettings(unittest.TestCase):
15 |
16 | def test_basic(self):
17 | settings = Settings()
18 | settings['test0'] = 'hello'
19 | settings['test1'] = 10
20 | settings['test2'] = [0, 2, 3]
21 | self.assertEqual(settings.get('test3', 3), 3)
22 | self.assertEqual(settings.save(), True)
23 |
24 | settings.load()
25 | self.assertEqual(settings.get('test0'), 'hello')
26 | self.assertEqual(settings.get('test1'), 10)
27 |
28 | settings.reset()
29 |
30 |
31 |
32 | if __name__ == '__main__':
33 | unittest.main()
34 |
--------------------------------------------------------------------------------
/combobox.py:
--------------------------------------------------------------------------------
1 | import sys
2 | try:
3 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox
4 | except ImportError:
5 | # needed for py3+qt4
6 | # Ref:
7 | # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
8 | # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
9 | if sys.version_info.major >= 3:
10 | import sip
11 | sip.setapi('QVariant', 2)
12 | from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox
13 |
14 |
15 | class ComboBox(QWidget):
16 | def __init__(self, parent=None, items=[]):
17 | super(ComboBox, self).__init__(parent)
18 |
19 | layout = QHBoxLayout()
20 | self.cb = QComboBox()
21 | self.items = items
22 | self.cb.addItems(self.items)
23 |
24 | self.cb.currentIndexChanged.connect(parent.comboSelectionChanged)
25 |
26 | layout.addWidget(self.cb)
27 | self.setLayout(layout)
28 |
29 | def update_items(self, items):
30 | self.items = items
31 |
32 | self.cb.clear()
33 | self.cb.addItems(self.items)
34 |
--------------------------------------------------------------------------------
/tests/test_stringBundle.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import unittest
4 | import resources
5 | from stringBundle import StringBundle
6 |
7 | class TestStringBundle(unittest.TestCase):
8 |
9 | def test_loadDefaultBundle_withoutError(self):
10 | strBundle = StringBundle.getBundle('en')
11 | self.assertEqual(strBundle.getString("openDir"), 'Open Dir', 'Fail to load the default bundle')
12 |
13 | def test_fallback_withoutError(self):
14 | strBundle = StringBundle.getBundle('zh-TW')
15 | self.assertEqual(strBundle.getString("openDir"), u'\u958B\u555F\u76EE\u9304', 'Fail to load the zh-TW bundle')
16 |
17 | def test_setInvaleLocaleToEnv_printErrorMsg(self):
18 | prev_lc = os.environ['LC_ALL']
19 | prev_lang = os.environ['LANG']
20 | os.environ['LC_ALL'] = 'UTF-8'
21 | os.environ['LANG'] = 'UTF-8'
22 | strBundle = StringBundle.getBundle()
23 | self.assertEqual(strBundle.getString("openDir"), 'Open Dir', 'Fail to load the default bundle')
24 | os.environ['LC_ALL'] = prev_lc
25 | os.environ['LANG'] = prev_lang
26 |
27 |
28 | if __name__ == '__main__':
29 | unittest.main()
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) <2015-Present> Tzutalin
2 |
3 | Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba, William T. Freeman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/tests/test_io.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import unittest
4 |
5 | class TestPascalVocRW(unittest.TestCase):
6 |
7 | def test_upper(self):
8 | dir_name = os.path.abspath(os.path.dirname(__file__))
9 | libs_path = os.path.join(dir_name, '..', 'libs')
10 | sys.path.insert(0, libs_path)
11 | from pascal_voc_io import PascalVocWriter
12 | from pascal_voc_io import PascalVocReader
13 |
14 | # Test Write/Read
15 | writer = PascalVocWriter('tests', 'test', (512, 512, 1), localImgPath='tests/test.512.512.bmp')
16 | difficult = 1
17 | writer.addBndBox(60, 40, 430, 504, 'person', difficult)
18 | writer.addBndBox(113, 40, 450, 403, 'face', difficult)
19 | writer.save('tests/test.xml')
20 |
21 | reader = PascalVocReader('tests/test.xml')
22 | shapes = reader.getShapes()
23 |
24 | personBndBox = shapes[0]
25 | face = shapes[1]
26 | self.assertEqual(personBndBox[0], 'person')
27 | self.assertEqual(personBndBox[1], [(60, 40), (430, 40), (430, 504), (60, 504)])
28 | self.assertEqual(face[0], 'face')
29 | self.assertEqual(face[1], [(113, 40), (450, 40), (450, 403), (113, 403)])
30 |
31 | if __name__ == '__main__':
32 | unittest.main()
33 |
--------------------------------------------------------------------------------
/libs/toolBar.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 |
10 | class ToolBar(QToolBar):
11 |
12 | def __init__(self, title):
13 | super(ToolBar, self).__init__(title)
14 | layout = self.layout()
15 | m = (0, 0, 0, 0)
16 | layout.setSpacing(0)
17 | layout.setContentsMargins(*m)
18 | self.setContentsMargins(*m)
19 | self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
20 |
21 | def addAction(self, action):
22 | if isinstance(action, QWidgetAction):
23 | return super(ToolBar, self).addAction(action)
24 | btn = ToolButton()
25 | btn.setDefaultAction(action)
26 | btn.setToolButtonStyle(self.toolButtonStyle())
27 | self.addWidget(btn)
28 |
29 |
30 | class ToolButton(QToolButton):
31 | """ToolBar companion class which ensures all buttons have the same size."""
32 | minSize = (60, 60)
33 |
34 | def minimumSizeHint(self):
35 | ms = super(ToolButton, self).minimumSizeHint()
36 | w1, h1 = ms.width(), ms.height()
37 | w2, h2 = self.minSize
38 | ToolButton.minSize = max(w1, w2), max(h1, h2)
39 | return QSize(*ToolButton.minSize)
40 |
--------------------------------------------------------------------------------
/libs/settings.py:
--------------------------------------------------------------------------------
1 | import pickle
2 | import os
3 | import sys
4 |
5 |
6 | class Settings(object):
7 | def __init__(self):
8 | # Be default, the home will be in the same folder as labelImg
9 | home = os.path.expanduser("~")
10 | self.data = {}
11 | self.path = os.path.join(home, '.labelImgSettings.pkl')
12 |
13 | def __setitem__(self, key, value):
14 | self.data[key] = value
15 |
16 | def __getitem__(self, key):
17 | return self.data[key]
18 |
19 | def get(self, key, default=None):
20 | if key in self.data:
21 | return self.data[key]
22 | return default
23 |
24 | def save(self):
25 | if self.path:
26 | with open(self.path, 'wb') as f:
27 | pickle.dump(self.data, f, pickle.HIGHEST_PROTOCOL)
28 | return True
29 | return False
30 |
31 | def load(self):
32 | try:
33 | if os.path.exists(self.path):
34 | with open(self.path, 'rb') as f:
35 | self.data = pickle.load(f)
36 | return True
37 | except:
38 | print('Loading setting failed')
39 | return False
40 |
41 | def reset(self):
42 | if os.path.exists(self.path):
43 | os.remove(self.path)
44 | print('Remove setting pkl file ${0}'.format(self.path))
45 | self.data = {}
46 | self.path = None
47 |
--------------------------------------------------------------------------------
/libs/colorDialog.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 | BB = QDialogButtonBox
10 |
11 |
12 | class ColorDialog(QColorDialog):
13 |
14 | def __init__(self, parent=None):
15 | super(ColorDialog, self).__init__(parent)
16 | self.setOption(QColorDialog.ShowAlphaChannel)
17 | # The Mac native dialog does not support our restore button.
18 | self.setOption(QColorDialog.DontUseNativeDialog)
19 | # Add a restore defaults button.
20 | # The default is set at invocation time, so that it
21 | # works across dialogs for different elements.
22 | self.default = None
23 | self.bb = self.layout().itemAt(1).widget()
24 | self.bb.addButton(BB.RestoreDefaults)
25 | self.bb.clicked.connect(self.checkRestore)
26 |
27 | def getColor(self, value=None, title=None, default=None):
28 | self.default = default
29 | if title:
30 | self.setWindowTitle(title)
31 | if value:
32 | self.setCurrentColor(value)
33 | return self.currentColor() if self.exec_() else None
34 |
35 | def checkRestore(self, button):
36 | if self.bb.buttonRole(button) & BB.ResetRole and self.default:
37 | self.setCurrentColor(self.default)
38 |
--------------------------------------------------------------------------------
/resources/strings/strings-zh-TW.properties:
--------------------------------------------------------------------------------
1 | saveAsDetail=將標籤保存到其他文件
2 | changeSaveDir=改變存放目錄
3 | openFile=開啟檔案
4 | shapeLineColorDetail=更改線條顏色
5 | resetAll=重置
6 | crtBox=創建區塊
7 | crtBoxDetail=畫一個區塊
8 | dupBoxDetail=複製區塊
9 | verifyImg=驗證圖像
10 | zoominDetail=放大
11 | verifyImgDetail=驗證圖像
12 | saveDetail=將標籤存到
13 | openFileDetail=打開圖像
14 | fitWidthDetail=調整到窗口寬度
15 | tutorial=YouTube教學
16 | editLabel=編輯標籤
17 | openAnnotationDetail=打開標籤文件
18 | quit=結束
19 | shapeFillColorDetail=更改填充顏色
20 | closeCurDetail=關閉目前檔案
21 | closeCur=關閉
22 | deleteImg=刪除圖像
23 | deleteImgDetail=刪除目前圖像
24 | fitWin=調整到跟窗口一樣大小
25 | delBox=刪除選取區塊
26 | boxLineColorDetail=選擇框線顏色
27 | originalsize=原始大小
28 | resetAllDetail=重設所有設定
29 | zoomoutDetail=畫面放大
30 | save=儲存
31 | saveAs=另存為
32 | fitWinDetail=縮放到窗口一樣
33 | openDir=開啟目錄
34 | showHide=顯示/隱藏標籤
35 | changeSaveFormat=更改儲存格式
36 | shapeFillColor=填充顏色
37 | quitApp=離開本程式
38 | dupBox=複製區塊
39 | delBoxDetail=刪除區塊
40 | zoomin=放大畫面
41 | info=資訊
42 | openAnnotation=開啟標籤
43 | prevImgDetail=上一個圖像
44 | fitWidth=縮放到跟畫面一樣寬
45 | zoomout=縮小畫面
46 | changeSavedAnnotationDir=更改預設標籤存的目錄
47 | nextImgDetail=下一個圖像
48 | originalsizeDetail=放大到原始大小
49 | prevImg=上一個圖像
50 | tutorialDetail=顯示示範內容
51 | shapeLineColor=形狀線條顏色
52 | boxLineColor=日期分隔線顏色
53 | editLabelDetail=修改所選區塊的標籤
54 | nextImg=下一張圖片
55 | useDefaultLabel=使用預設標籤
56 | useDifficult=有難度的
57 | boxLabelText=區塊的標籤
58 | labels=標籤
59 | autoSaveMode=自動儲存模式
60 | singleClsMode=單一類別模式
61 | displayLabel=顯示類別
62 | fileList=檔案清單
63 | files=檔案
64 | advancedMode=進階模式
65 | advancedModeDetail=切到進階模式
66 | showAllBoxDetail=顯示所有區塊
67 | hideAllBoxDetail=隱藏所有區塊
68 |
--------------------------------------------------------------------------------
/resources/strings/strings-zh-CN.properties:
--------------------------------------------------------------------------------
1 | saveAsDetail=將标签保存到其他文件
2 | changeSaveDir=改变存放目录
3 | openFile=打开文件
4 | shapeLineColorDetail=更改线条颜色
5 | resetAll=全部重置
6 | crtBox=创建区块
7 | crtBoxDetail=创建一个新的区块
8 | dupBoxDetail=复制区块
9 | verifyImg=验证图像
10 | zoominDetail=放大
11 | verifyImgDetail=验证图像
12 | saveDetail=保存标签文件
13 | openFileDetail=打开图像文件
14 | fitWidthDetail=调整宽度适应到窗口宽度
15 | tutorial=YouTube教学
16 | editLabel=编辑标签
17 | openAnnotationDetail=打开标签文件
18 | quit=退出
19 | shapeFillColorDetail=更改填充颜色
20 | closeCurDetail=关闭当前文件
21 | closeCur=关闭文件
22 | deleteImg=删除图像
23 | deleteImgDetail=删除当前图像
24 | fitWin=调整到窗口大小
25 | delBox=删除选择的区块
26 | boxLineColorDetail=选择线框颜色
27 | originalsize=原始大小
28 | resetAllDetail=重置所有设定
29 | zoomoutDetail=放大画面
30 | save=保存
31 | saveAs=另存为
32 | fitWinDetail=缩放到当前窗口大小
33 | openDir=打开目录
34 | showHide=显示/隐藏标签
35 | changeSaveFormat=更改存储格式
36 | shapeFillColor=填充颜色
37 | quitApp=退出程序
38 | dupBox=复制区块
39 | delBoxDetail=删除区块
40 | zoomin=放大画面
41 | info=信息
42 | openAnnotation=开启标签
43 | prevImgDetail=上一个图像
44 | fitWidth=缩放到跟当前画面一样宽
45 | zoomout=缩小画面
46 | changeSavedAnnotationDir=更改保存标签文件的预设目录
47 | nextImgDetail=下一个图像
48 | originalsizeDetail=放大到原始大小
49 | prevImg=上一个图像
50 | tutorialDetail=显示示范内容
51 | shapeLineColor=形状线条颜色
52 | boxLineColor=区块线条颜色
53 | editLabelDetail=修改当前所选的区块颜色
54 | nextImg=下一个图片
55 | useDefaultLabel=使用预设标签
56 | useDifficult=有难度的
57 | boxLabelText=区块的标签
58 | labels=标签
59 | autoSaveMode=自动保存模式
60 | singleClsMode=单一类别模式
61 | displayLabel=显示类别
62 | fileList=文件列表
63 | files=文件
64 | advancedMode=专家模式
65 | advancedModeDetail=切换到专家模式
66 | showAllBoxDetail=显示所有区块
67 | hideAllBoxDetail=隐藏所有区块
68 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # vim: set ts=2 et:
2 |
3 | # run xvfb with 32-bit color
4 | # xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here
5 |
6 | matrix:
7 | include:
8 |
9 | # Python 3 + QT5
10 | - os: linux
11 | dist: trusty
12 | sudo: required
13 | language: generic
14 | python: "3.5"
15 | env:
16 | - QT=5
17 | - CONDA=4.2.0
18 | addons:
19 | apt:
20 | packages:
21 | - cmake
22 | - xvfb
23 | before_install:
24 | # ref: https://repo.anaconda.com/archive/
25 | - curl -O https://repo.anaconda.com/archive/Anaconda3-2020.02-Linux-x86_64.sh
26 | # ref: http://conda.pydata.org/docs/help/silent.html
27 | - /bin/bash Anaconda3-2020.02-Linux-x86_64.sh -b -p $HOME/anaconda3
28 | - export PATH="$HOME/anaconda3/bin:$PATH"
29 | # ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda
30 | - conda create -y -n labelImg-py3qt5 python=3.5
31 | - source activate labelImg-py3qt5
32 | - conda install -y pyqt=5
33 | - conda install -y lxml
34 | - make qt5py3
35 | - xvfb-run make testpy3
36 |
37 | # Pipenv Python 3 + QT5 - Build .app
38 | - os: osx
39 | language: generic
40 | python: "3.7"
41 | env:
42 | - PIPENV_VENV_IN_PROJECT=1
43 | - PIPENV_IGNORE_VIRTUALENVS=1
44 | install:
45 | - pip3 install pipenv
46 | - pipenv install pyqt5 lxml
47 | - pipenv run pip install pyqt5==5.13.2 lxml
48 | - pipenv run make qt5py3
49 | - rm -rf build dist
50 | - pipenv run python setup.py py2app
51 | - open dist/labelImg.app
52 |
53 | script:
54 | - exit 0
55 |
--------------------------------------------------------------------------------
/resources/icons/app.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
31 |
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | History
2 | =======
3 |
4 | 1.8.2 (2018-12-02)
5 | ------------------
6 |
7 | * Fix pip depolyment issue
8 |
9 |
10 | 1.8.1 (2018-12-02)
11 | ------------------
12 |
13 | * Fix issues
14 | * Support zh-Tw strings
15 |
16 |
17 | 1.8.0 (2018-10-21)
18 | ------------------
19 |
20 | * Support drawing sqaure rect
21 | * Add item single click slot
22 | * Fix issues
23 |
24 | 1.7.0 (2018-05-18)
25 | ------------------
26 |
27 | * Support YOLO
28 | * Fix minor issues
29 |
30 |
31 | 1.6.1 (2018-04-17)
32 | ------------------
33 |
34 | * Fix issue
35 |
36 | 1.6.0 (2018-01-29)
37 | ------------------
38 |
39 | * Add more pre-defined labels
40 | * Show cursor pose in status bar
41 | * Fix minor issues
42 |
43 | 1.5.2 (2017-10-24)
44 | ------------------
45 |
46 | * Assign different colors to different lablels
47 |
48 | 1.5.1 (2017-9-27)
49 | ------------------
50 |
51 | * Show a autosaving dialog
52 |
53 | 1.5.0 (2017-9-14)
54 | ------------------
55 |
56 | * Fix the issues
57 | * Add feature: Draw a box easier
58 |
59 |
60 | 1.4.3 (2017-08-09)
61 | ------------------
62 |
63 | * Refactor setting
64 | * Fix the issues
65 |
66 |
67 | 1.4.0 (2017-07-07)
68 | ------------------
69 |
70 | * Add feature: auto saving
71 | * Add feature: single class mode
72 | * Fix the issues
73 |
74 | 1.3.4 (2017-07-07)
75 | ------------------
76 |
77 | * Fix issues and improve zoom-in
78 |
79 | 1.3.3 (2017-05-31)
80 | ------------------
81 |
82 | * Fix issues
83 |
84 | 1.3.2 (2017-05-18)
85 | ------------------
86 |
87 | * Fix issues
88 |
89 |
90 | 1.3.1 (2017-05-11)
91 | ------------------
92 |
93 | * Fix issues
94 |
95 | 1.3.0 (2017-04-22)
96 | ------------------
97 |
98 | * Fix issues
99 | * Add difficult tag
100 | * Create new files for pypi
101 |
102 | 1.2.3 (2017-04-22)
103 | ------------------
104 |
105 | * Fix issues
106 |
107 | 1.2.2 (2017-01-09)
108 | ------------------
109 |
110 | * Fix issues
111 |
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | resources/icons/help.png
5 | resources/icons/app.png
6 | resources/icons/expert2.png
7 | resources/icons/done.png
8 | resources/icons/file.png
9 | resources/icons/labels.png
10 | resources/icons/objects.png
11 | resources/icons/close.png
12 | resources/icons/fit-width.png
13 | resources/icons/fit-window.png
14 | resources/icons/undo.png
15 | resources/icons/eye.png
16 | resources/icons/quit.png
17 | resources/icons/copy.png
18 | resources/icons/edit.png
19 | resources/icons/open.png
20 | resources/icons/save.png
21 | resources/icons/format_voc.png
22 | resources/icons/format_yolo.png
23 | resources/icons/save-as.png
24 | resources/icons/color.png
25 | resources/icons/color_line.png
26 | resources/icons/zoom.png
27 | resources/icons/zoom-in.png
28 | resources/icons/zoom-out.png
29 | resources/icons/cancel.png
30 | resources/icons/next.png
31 | resources/icons/prev.png
32 | resources/icons/resetall.png
33 | resources/icons/verify.png
34 | resources/strings/strings.properties
35 | resources/strings/strings-zh-TW.properties
36 | resources/strings/strings-zh-CN.properties
37 |
38 |
39 |
--------------------------------------------------------------------------------
/resources/strings/strings.properties:
--------------------------------------------------------------------------------
1 | openFile=Open
2 | openFileDetail=Open image or label file
3 | quit=Quit
4 | quitApp=Quit application
5 | openDir=Open Dir
6 | changeSavedAnnotationDir=Change default saved Annotation dir
7 | openAnnotation=Open Annotation
8 | openAnnotationDetail=Open an annotation file
9 | changeSaveDir=Change Save Dir
10 | nextImg=Next Image
11 | nextImgDetail=Open the next Image
12 | prevImg=Prev Image
13 | prevImgDetail=Open the previous Image
14 | verifyImg=Verify Image
15 | verifyImgDetail=Verify Image
16 | save=Save
17 | saveDetail=Save the labels to a file
18 | changeSaveFormat=Change save format
19 | saveAs=Save As
20 | saveAsDetail=Save the labels to a different file
21 | closeCur=Close
22 | closeCurDetail=Close the current file
23 | deleteImg=Delete current image
24 | deleteImgDetail=Delete the current image
25 | resetAll=Reset All
26 | resetAllDetail=Reset All
27 | boxLineColor=Box Line Color
28 | boxLineColorDetail=Choose Box line color
29 | crtBox=Create RectBox
30 | crtBoxDetail=Draw a new box
31 | delBox=Delete RectBox
32 | delBoxDetail=Remove the box
33 | dupBox=Duplicate RectBox
34 | dupBoxDetail=Create a duplicate of the selected box
35 | tutorial=Tutorial
36 | tutorialDetail=Show demo
37 | info=Information
38 | zoomin=Zoom In
39 | zoominDetail=Increase zoom level
40 | zoomout=Zoom Out
41 | zoomoutDetail=Decrease zoom level
42 | originalsize=Original size
43 | originalsizeDetail=Zoom to original size
44 | fitWin=Fit Window
45 | fitWinDetail=Zoom follows window size
46 | fitWidth=Fit Width
47 | fitWidthDetail=Zoom follows window width
48 | editLabel=Edit Label
49 | editLabelDetail=Modify the label of the selected Box
50 | shapeLineColor=Shape Line Color
51 | shapeLineColorDetail=Change the line color for this specific shape
52 | shapeFillColor=Shape Fill Color
53 | shapeFillColorDetail=Change the fill color for this specific shape
54 | showHide=Show/Hide Label Panel
55 | useDefaultLabel=Use default label
56 | useDifficult=difficult
57 | boxLabelText=Box Labels
58 | labels=Labels
59 | autoSaveMode=Auto Save mode
60 | singleClsMode=Single Class Mode
61 | displayLabel=Display Labels
62 | fileList=File List
63 | files=Files
64 | advancedMode=Advanced Mode
65 | advancedModeDetail=Swtich to advanced mode
66 | showAllBoxDetail=Show all bounding boxes
67 | hideAllBoxDetail=Hide all bounding boxes
68 |
--------------------------------------------------------------------------------
/libs/stringBundle.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import re
4 | import os
5 | import sys
6 | import locale
7 | from libs.ustr import ustr
8 |
9 | try:
10 | from PyQt5.QtCore import *
11 | except ImportError:
12 | if sys.version_info.major >= 3:
13 | import sip
14 | sip.setapi('QVariant', 2)
15 | from PyQt4.QtCore import *
16 |
17 |
18 | class StringBundle:
19 |
20 | __create_key = object()
21 |
22 | def __init__(self, create_key, localeStr):
23 | assert(create_key == StringBundle.__create_key), "StringBundle must be created using StringBundle.getBundle"
24 | self.idToMessage = {}
25 | paths = self.__createLookupFallbackList(localeStr)
26 | for path in paths:
27 | self.__loadBundle(path)
28 |
29 | @classmethod
30 | def getBundle(cls, localeStr=None):
31 | if localeStr is None:
32 | try:
33 | localeStr = locale.getlocale()[0] if locale.getlocale() and len(
34 | locale.getlocale()) > 0 else os.getenv('LANG')
35 | except:
36 | print('Invalid locale')
37 | localeStr = 'en'
38 |
39 | return StringBundle(cls.__create_key, localeStr)
40 |
41 | def getString(self, stringId):
42 | assert(stringId in self.idToMessage), "Missing string id : " + stringId
43 | return self.idToMessage[stringId]
44 |
45 | def __createLookupFallbackList(self, localeStr):
46 | resultPaths = []
47 | basePath = ":/strings"
48 | resultPaths.append(basePath)
49 | if localeStr is not None:
50 | # Don't follow standard BCP47. Simple fallback
51 | tags = re.split('[^a-zA-Z]', localeStr)
52 | for tag in tags:
53 | lastPath = resultPaths[-1]
54 | resultPaths.append(lastPath + '-' + tag)
55 |
56 | return resultPaths
57 |
58 | def __loadBundle(self, path):
59 | PROP_SEPERATOR = '='
60 | f = QFile(path)
61 | if f.exists():
62 | if f.open(QIODevice.ReadOnly | QFile.Text):
63 | text = QTextStream(f)
64 | text.setCodec("UTF-8")
65 |
66 | while not text.atEnd():
67 | line = ustr(text.readLine())
68 | key_value = line.split(PROP_SEPERATOR)
69 | key = key_value[0].strip()
70 | value = PROP_SEPERATOR.join(key_value[1:]).strip().strip('"')
71 | self.idToMessage[key] = value
72 |
73 | f.close()
74 |
--------------------------------------------------------------------------------
/libs/labelDialog.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 | from libs.utils import newIcon, labelValidator
10 |
11 | BB = QDialogButtonBox
12 |
13 |
14 | class LabelDialog(QDialog):
15 |
16 | def __init__(self, text="Enter object label", parent=None, listItem=None):
17 | super(LabelDialog, self).__init__(parent)
18 |
19 | self.edit = QLineEdit()
20 | self.edit.setText(text)
21 | self.edit.setValidator(labelValidator())
22 | self.edit.editingFinished.connect(self.postProcess)
23 |
24 | model = QStringListModel()
25 | model.setStringList(listItem)
26 | completer = QCompleter()
27 | completer.setModel(model)
28 | self.edit.setCompleter(completer)
29 |
30 | layout = QVBoxLayout()
31 | layout.addWidget(self.edit)
32 | self.buttonBox = bb = BB(BB.Ok | BB.Cancel, Qt.Horizontal, self)
33 | bb.button(BB.Ok).setIcon(newIcon('done'))
34 | bb.button(BB.Cancel).setIcon(newIcon('undo'))
35 | bb.accepted.connect(self.validate)
36 | bb.rejected.connect(self.reject)
37 | layout.addWidget(bb)
38 |
39 | if listItem is not None and len(listItem) > 0:
40 | self.listWidget = QListWidget(self)
41 | for item in listItem:
42 | self.listWidget.addItem(item)
43 | self.listWidget.itemClicked.connect(self.listItemClick)
44 | self.listWidget.itemDoubleClicked.connect(self.listItemDoubleClick)
45 | layout.addWidget(self.listWidget)
46 |
47 | self.setLayout(layout)
48 |
49 | def validate(self):
50 | try:
51 | if self.edit.text().trimmed():
52 | self.accept()
53 | except AttributeError:
54 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
55 | if self.edit.text().strip():
56 | self.accept()
57 |
58 | def postProcess(self):
59 | try:
60 | self.edit.setText(self.edit.text().trimmed())
61 | except AttributeError:
62 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
63 | self.edit.setText(self.edit.text())
64 |
65 | def popUp(self, text='', move=True):
66 | self.edit.setText(text)
67 | self.edit.setSelection(0, len(text))
68 | self.edit.setFocus(Qt.PopupFocusReason)
69 | if move:
70 | self.move(QCursor.pos())
71 | return self.edit.text() if self.exec_() else None
72 |
73 | def listItemClick(self, tQListWidgetItem):
74 | try:
75 | text = tQListWidgetItem.text().trimmed()
76 | except AttributeError:
77 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
78 | text = tQListWidgetItem.text().strip()
79 | self.edit.setText(text)
80 |
81 | def listItemDoubleClick(self, tQListWidgetItem):
82 | self.listItemClick(tQListWidgetItem)
83 | self.validate()
84 |
--------------------------------------------------------------------------------
/libs/utils.py:
--------------------------------------------------------------------------------
1 | from math import sqrt
2 | from libs.ustr import ustr
3 | import hashlib
4 | import re
5 | import sys
6 |
7 | try:
8 | from PyQt5.QtGui import *
9 | from PyQt5.QtCore import *
10 | from PyQt5.QtWidgets import *
11 | except ImportError:
12 | from PyQt4.QtGui import *
13 | from PyQt4.QtCore import *
14 |
15 |
16 | def newIcon(icon):
17 | return QIcon(':/' + icon)
18 |
19 |
20 | def newButton(text, icon=None, slot=None):
21 | b = QPushButton(text)
22 | if icon is not None:
23 | b.setIcon(newIcon(icon))
24 | if slot is not None:
25 | b.clicked.connect(slot)
26 | return b
27 |
28 |
29 | def newAction(parent, text, slot=None, shortcut=None, icon=None,
30 | tip=None, checkable=False, enabled=True):
31 | """Create a new action and assign callbacks, shortcuts, etc."""
32 | a = QAction(text, parent)
33 | if icon is not None:
34 | a.setIcon(newIcon(icon))
35 | if shortcut is not None:
36 | if isinstance(shortcut, (list, tuple)):
37 | a.setShortcuts(shortcut)
38 | else:
39 | a.setShortcut(shortcut)
40 | if tip is not None:
41 | a.setToolTip(tip)
42 | a.setStatusTip(tip)
43 | if slot is not None:
44 | a.triggered.connect(slot)
45 | if checkable:
46 | a.setCheckable(True)
47 | a.setEnabled(enabled)
48 | return a
49 |
50 |
51 | def addActions(widget, actions):
52 | for action in actions:
53 | if action is None:
54 | widget.addSeparator()
55 | elif isinstance(action, QMenu):
56 | widget.addMenu(action)
57 | else:
58 | widget.addAction(action)
59 |
60 |
61 | def labelValidator():
62 | return QRegExpValidator(QRegExp(r'^[^ \t].+'), None)
63 |
64 |
65 | class struct(object):
66 |
67 | def __init__(self, **kwargs):
68 | self.__dict__.update(kwargs)
69 |
70 |
71 | def distance(p):
72 | return sqrt(p.x() * p.x() + p.y() * p.y())
73 |
74 |
75 | def fmtShortcut(text):
76 | mod, key = text.split('+', 1)
77 | return '%s+%s' % (mod, key)
78 |
79 |
80 | def generateColorByText(text):
81 | s = ustr(text)
82 | hashCode = int(hashlib.sha256(s.encode('utf-8')).hexdigest(), 16)
83 | r = int((hashCode / 255) % 255)
84 | g = int((hashCode / 65025) % 255)
85 | b = int((hashCode / 16581375) % 255)
86 | return QColor(r, g, b, 100)
87 |
88 | def have_qstring():
89 | '''p3/qt5 get rid of QString wrapper as py3 has native unicode str type'''
90 | return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.'))
91 |
92 | def util_qt_strlistclass():
93 | return QStringList if have_qstring() else list
94 |
95 | def natural_sort(list, key=lambda s:s):
96 | """
97 | Sort the list into natural alphanumeric order.
98 | """
99 | def get_alphanum_key_func(key):
100 | convert = lambda text: int(text) if text.isdigit() else text
101 | return lambda s: [convert(c) for c in re.split('([0-9]+)', key(s))]
102 | sort_key = get_alphanum_key_func(key)
103 | list.sort(key=sort_key)
104 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from setuptools import setup, find_packages, Command
5 | from sys import platform as _platform
6 | from shutil import rmtree
7 | import sys
8 | import os
9 |
10 | here = os.path.abspath(os.path.dirname(__file__))
11 | NAME = 'labelImg'
12 | REQUIRES_PYTHON = '>=3.0.0'
13 | REQUIRED_DEP = ['pyqt5', 'lxml']
14 | about = {}
15 |
16 | with open(os.path.join(here, 'libs', '__init__.py')) as f:
17 | exec(f.read(), about)
18 |
19 | with open('README.rst') as readme_file:
20 | readme = readme_file.read()
21 |
22 | with open('HISTORY.rst') as history_file:
23 | history = history_file.read()
24 |
25 |
26 | # OS specific settings
27 | SET_REQUIRES = []
28 | if _platform == "linux" or _platform == "linux2":
29 | # linux
30 | print('linux')
31 | elif _platform == "darwin":
32 | # MAC OS X
33 | SET_REQUIRES.append('py2app')
34 |
35 | required_packages = find_packages()
36 | required_packages.append('labelImg')
37 |
38 | APP = [NAME + '.py']
39 | OPTIONS = {
40 | 'argv_emulation': True,
41 | 'iconfile': 'resources/icons/app.icns'
42 | }
43 |
44 | class UploadCommand(Command):
45 | """Support setup.py upload."""
46 |
47 | description=readme + '\n\n' + history,
48 |
49 | user_options = []
50 |
51 | @staticmethod
52 | def status(s):
53 | """Prints things in bold."""
54 | print('\033[1m{0}\033[0m'.format(s))
55 |
56 | def initialize_options(self):
57 | pass
58 |
59 | def finalize_options(self):
60 | pass
61 |
62 | def run(self):
63 | try:
64 | self.status('Removing previous builds…')
65 | rmtree(os.path.join(here, 'dist'))
66 | except OSError:
67 | self.status('Fail to remove previous builds..')
68 | pass
69 |
70 | self.status('Building Source and Wheel (universal) distribution…')
71 | os.system(
72 | '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
73 |
74 | self.status('Uploading the package to PyPI via Twine…')
75 | os.system('twine upload dist/*')
76 |
77 | self.status('Pushing git tags…')
78 | os.system('git tag -d v{0}'.format(about['__version__']))
79 | os.system('git tag v{0}'.format(about['__version__']))
80 | # os.system('git push --tags')
81 |
82 | sys.exit()
83 |
84 |
85 | setup(
86 | app=APP,
87 | name=NAME,
88 | version=about['__version__'],
89 | description="LabelImg is a graphical image annotation tool and label object bounding boxes in images",
90 | long_description=readme + '\n\n' + history,
91 | author="TzuTa Lin",
92 | author_email='tzu.ta.lin@gmail.com',
93 | url='https://github.com/tzutalin/labelImg',
94 | python_requires=REQUIRES_PYTHON,
95 | package_dir={'labelImg': '.'},
96 | packages=required_packages,
97 | entry_points={
98 | 'console_scripts': [
99 | 'labelImg=labelImg.labelImg:main'
100 | ]
101 | },
102 | include_package_data=True,
103 | install_requires=REQUIRED_DEP,
104 | license="MIT license",
105 | zip_safe=False,
106 | keywords='labelImg labelTool development annotation deeplearning',
107 | classifiers=[
108 | 'Development Status :: 5 - Production/Stable',
109 | 'Intended Audience :: Developers',
110 | 'License :: OSI Approved :: MIT License',
111 | 'Natural Language :: English',
112 | 'Programming Language :: Python :: 3',
113 | 'Programming Language :: Python :: 3.3',
114 | 'Programming Language :: Python :: 3.4',
115 | 'Programming Language :: Python :: 3.5',
116 | 'Programming Language :: Python :: 3.6',
117 | 'Programming Language :: Python :: 3.7',
118 | ],
119 | package_data={'data/predefined_classes.txt': ['data/predefined_classes.txt']},
120 | options={'py2app': OPTIONS},
121 | setup_requires=SET_REQUIRES,
122 | # $ setup.py publish support.
123 | cmdclass={
124 | 'upload': UploadCommand,
125 | }
126 | )
127 |
--------------------------------------------------------------------------------
/libs/yolo_io.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf8 -*-
3 | import sys
4 | import os
5 | from xml.etree import ElementTree
6 | from xml.etree.ElementTree import Element, SubElement
7 | from lxml import etree
8 | import codecs
9 | from libs.constants import DEFAULT_ENCODING
10 |
11 | TXT_EXT = '.txt'
12 | ENCODE_METHOD = DEFAULT_ENCODING
13 |
14 | class YOLOWriter:
15 |
16 | def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localImgPath=None):
17 | self.foldername = foldername
18 | self.filename = filename
19 | self.databaseSrc = databaseSrc
20 | self.imgSize = imgSize
21 | self.boxlist = []
22 | self.localImgPath = localImgPath
23 | self.verified = False
24 |
25 | def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult):
26 | bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax}
27 | bndbox['name'] = name
28 | bndbox['difficult'] = difficult
29 | self.boxlist.append(bndbox)
30 |
31 | def BndBox2YoloLine(self, box, classList=[]):
32 | xmin = box['xmin']
33 | xmax = box['xmax']
34 | ymin = box['ymin']
35 | ymax = box['ymax']
36 |
37 | xcen = float((xmin + xmax)) / 2 / self.imgSize[1]
38 | ycen = float((ymin + ymax)) / 2 / self.imgSize[0]
39 |
40 | w = float((xmax - xmin)) / self.imgSize[1]
41 | h = float((ymax - ymin)) / self.imgSize[0]
42 |
43 | # PR387
44 | boxName = box['name']
45 | if boxName not in classList:
46 | classList.append(boxName)
47 |
48 | classIndex = classList.index(boxName)
49 |
50 | return classIndex, xcen, ycen, w, h
51 |
52 | def save(self, classList=[], targetFile=None):
53 |
54 | out_file = None #Update yolo .txt
55 | out_class_file = None #Update class list .txt
56 |
57 | if targetFile is None:
58 | out_file = open(
59 | self.filename + TXT_EXT, 'w', encoding=ENCODE_METHOD)
60 | classesFile = os.path.join(os.path.dirname(os.path.abspath(self.filename)), "classes.txt")
61 | out_class_file = open(classesFile, 'w')
62 |
63 | else:
64 | out_file = codecs.open(targetFile, 'w', encoding=ENCODE_METHOD)
65 | classesFile = os.path.join(os.path.dirname(os.path.abspath(targetFile)), "classes.txt")
66 | out_class_file = open(classesFile, 'w')
67 |
68 |
69 | for box in self.boxlist:
70 | classIndex, xcen, ycen, w, h = self.BndBox2YoloLine(box, classList)
71 | # print (classIndex, xcen, ycen, w, h)
72 | out_file.write("%d %.6f %.6f %.6f %.6f\n" % (classIndex, xcen, ycen, w, h))
73 |
74 | # print (classList)
75 | # print (out_class_file)
76 | for c in classList:
77 | out_class_file.write(c+'\n')
78 |
79 | out_class_file.close()
80 | out_file.close()
81 |
82 |
83 |
84 | class YoloReader:
85 |
86 | def __init__(self, filepath, image, classListPath=None):
87 | # shapes type:
88 | # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult]
89 | self.shapes = []
90 | self.filepath = filepath
91 |
92 | if classListPath is None:
93 | dir_path = os.path.dirname(os.path.realpath(self.filepath))
94 | self.classListPath = os.path.join(dir_path, "classes.txt")
95 | else:
96 | self.classListPath = classListPath
97 |
98 | # print (filepath, self.classListPath)
99 |
100 | classesFile = open(self.classListPath, 'r')
101 | self.classes = classesFile.read().strip('\n').split('\n')
102 |
103 | # print (self.classes)
104 |
105 | imgSize = [image.height(), image.width(),
106 | 1 if image.isGrayscale() else 3]
107 |
108 | self.imgSize = imgSize
109 |
110 | self.verified = False
111 | # try:
112 | self.parseYoloFormat()
113 | # except:
114 | # pass
115 |
116 | def getShapes(self):
117 | return self.shapes
118 |
119 | def addShape(self, label, xmin, ymin, xmax, ymax, difficult):
120 |
121 | points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
122 | self.shapes.append((label, points, None, None, difficult))
123 |
124 | def yoloLine2Shape(self, classIndex, xcen, ycen, w, h):
125 | label = self.classes[int(classIndex)]
126 |
127 | xmin = max(float(xcen) - float(w) / 2, 0)
128 | xmax = min(float(xcen) + float(w) / 2, 1)
129 | ymin = max(float(ycen) - float(h) / 2, 0)
130 | ymax = min(float(ycen) + float(h) / 2, 1)
131 |
132 | xmin = int(self.imgSize[1] * xmin)
133 | xmax = int(self.imgSize[1] * xmax)
134 | ymin = int(self.imgSize[0] * ymin)
135 | ymax = int(self.imgSize[0] * ymax)
136 |
137 | return label, xmin, ymin, xmax, ymax
138 |
139 | def parseYoloFormat(self):
140 | bndBoxFile = open(self.filepath, 'r')
141 | for bndBox in bndBoxFile:
142 | classIndex, xcen, ycen, w, h = bndBox.strip().split(' ')
143 | label, xmin, ymin, xmax, ymax = self.yoloLine2Shape(classIndex, xcen, ycen, w, h)
144 |
145 | # Caveat: difficult flag is discarded when saved as yolo format.
146 | self.addShape(label, xmin, ymin, xmax, ymax, False)
147 |
--------------------------------------------------------------------------------
/libs/labelFile.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Tzutalin
2 | # Create by TzuTaLin
3 |
4 | try:
5 | from PyQt5.QtGui import QImage
6 | except ImportError:
7 | from PyQt4.QtGui import QImage
8 |
9 | from base64 import b64encode, b64decode
10 | from libs.pascal_voc_io import PascalVocWriter
11 | from libs.yolo_io import YOLOWriter
12 | from libs.pascal_voc_io import XML_EXT
13 | from enum import Enum
14 | import os.path
15 | import sys
16 |
17 |
18 | class LabelFileFormat(Enum):
19 | PASCAL_VOC= 1
20 | YOLO = 2
21 |
22 |
23 | class LabelFileError(Exception):
24 | pass
25 |
26 |
27 | class LabelFile(object):
28 | # It might be changed as window creates. By default, using XML ext
29 | # suffix = '.lif'
30 | suffix = XML_EXT
31 |
32 | def __init__(self, filename=None):
33 | self.shapes = ()
34 | self.imagePath = None
35 | self.imageData = None
36 | self.verified = False
37 |
38 | def savePascalVocFormat(self, filename, shapes, imagePath, imageData,
39 | lineColor=None, fillColor=None, databaseSrc=None):
40 | imgFolderPath = os.path.dirname(imagePath)
41 | imgFolderName = os.path.split(imgFolderPath)[-1]
42 | imgFileName = os.path.basename(imagePath)
43 | #imgFileNameWithoutExt = os.path.splitext(imgFileName)[0]
44 | # Read from file path because self.imageData might be empty if saving to
45 | # Pascal format
46 | image = QImage()
47 | image.load(imagePath)
48 | imageShape = [image.height(), image.width(),
49 | 1 if image.isGrayscale() else 3]
50 | writer = PascalVocWriter(imgFolderName, imgFileName,
51 | imageShape, localImgPath=imagePath)
52 | writer.verified = self.verified
53 |
54 | for shape in shapes:
55 | points = shape['points']
56 | label = shape['label']
57 | # Add Chris
58 | difficult = int(shape['difficult'])
59 | bndbox = LabelFile.convertPoints2BndBox(points)
60 | writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult)
61 |
62 | writer.save(targetFile=filename)
63 | return
64 |
65 | def saveYoloFormat(self, filename, shapes, imagePath, imageData, classList,
66 | lineColor=None, fillColor=None, databaseSrc=None):
67 | imgFolderPath = os.path.dirname(imagePath)
68 | imgFolderName = os.path.split(imgFolderPath)[-1]
69 | imgFileName = os.path.basename(imagePath)
70 | #imgFileNameWithoutExt = os.path.splitext(imgFileName)[0]
71 | # Read from file path because self.imageData might be empty if saving to
72 | # Pascal format
73 | image = QImage()
74 | image.load(imagePath)
75 | imageShape = [image.height(), image.width(),
76 | 1 if image.isGrayscale() else 3]
77 | writer = YOLOWriter(imgFolderName, imgFileName,
78 | imageShape, localImgPath=imagePath)
79 | writer.verified = self.verified
80 |
81 | for shape in shapes:
82 | points = shape['points']
83 | label = shape['label']
84 | # Add Chris
85 | difficult = int(shape['difficult'])
86 | bndbox = LabelFile.convertPoints2BndBox(points)
87 | writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label, difficult)
88 |
89 | writer.save(targetFile=filename, classList=classList)
90 | return
91 |
92 | def toggleVerify(self):
93 | self.verified = not self.verified
94 |
95 | ''' ttf is disable
96 | def load(self, filename):
97 | import json
98 | with open(filename, 'rb') as f:
99 | data = json.load(f)
100 | imagePath = data['imagePath']
101 | imageData = b64decode(data['imageData'])
102 | lineColor = data['lineColor']
103 | fillColor = data['fillColor']
104 | shapes = ((s['label'], s['points'], s['line_color'], s['fill_color'])\
105 | for s in data['shapes'])
106 | # Only replace data after everything is loaded.
107 | self.shapes = shapes
108 | self.imagePath = imagePath
109 | self.imageData = imageData
110 | self.lineColor = lineColor
111 | self.fillColor = fillColor
112 |
113 | def save(self, filename, shapes, imagePath, imageData, lineColor=None, fillColor=None):
114 | import json
115 | with open(filename, 'wb') as f:
116 | json.dump(dict(
117 | shapes=shapes,
118 | lineColor=lineColor, fillColor=fillColor,
119 | imagePath=imagePath,
120 | imageData=b64encode(imageData)),
121 | f, ensure_ascii=True, indent=2)
122 | '''
123 |
124 | @staticmethod
125 | def isLabelFile(filename):
126 | fileSuffix = os.path.splitext(filename)[1].lower()
127 | return fileSuffix == LabelFile.suffix
128 |
129 | @staticmethod
130 | def convertPoints2BndBox(points):
131 | xmin = float('inf')
132 | ymin = float('inf')
133 | xmax = float('-inf')
134 | ymax = float('-inf')
135 | for p in points:
136 | x = p[0]
137 | y = p[1]
138 | xmin = min(x, xmin)
139 | ymin = min(y, ymin)
140 | xmax = max(x, xmax)
141 | ymax = max(y, ymax)
142 |
143 | # Martin Kersner, 2015/11/12
144 | # 0-valued coordinates of BB caused an error while
145 | # training faster-rcnn object detector.
146 | if xmin < 1:
147 | xmin = 1
148 |
149 | if ymin < 1:
150 | ymin = 1
151 |
152 | return (int(xmin), int(ymin), int(xmax), int(ymax))
153 |
--------------------------------------------------------------------------------
/libs/pascal_voc_io.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf8 -*-
3 | import sys
4 | from xml.etree import ElementTree
5 | from xml.etree.ElementTree import Element, SubElement
6 | from lxml import etree
7 | import codecs
8 | from libs.constants import DEFAULT_ENCODING
9 | from libs.ustr import ustr
10 |
11 |
12 | XML_EXT = '.xml'
13 | ENCODE_METHOD = DEFAULT_ENCODING
14 |
15 | class PascalVocWriter:
16 |
17 | def __init__(self, foldername, filename, imgSize,databaseSrc='Unknown', localImgPath=None):
18 | self.foldername = foldername
19 | self.filename = filename
20 | self.databaseSrc = databaseSrc
21 | self.imgSize = imgSize
22 | self.boxlist = []
23 | self.localImgPath = localImgPath
24 | self.verified = False
25 |
26 | def prettify(self, elem):
27 | """
28 | Return a pretty-printed XML string for the Element.
29 | """
30 | rough_string = ElementTree.tostring(elem, 'utf8')
31 | root = etree.fromstring(rough_string)
32 | return etree.tostring(root, pretty_print=True, encoding=ENCODE_METHOD).replace(" ".encode(), "\t".encode())
33 | # minidom does not support UTF-8
34 | '''reparsed = minidom.parseString(rough_string)
35 | return reparsed.toprettyxml(indent="\t", encoding=ENCODE_METHOD)'''
36 |
37 | def genXML(self):
38 | """
39 | Return XML root
40 | """
41 | # Check conditions
42 | if self.filename is None or \
43 | self.foldername is None or \
44 | self.imgSize is None:
45 | return None
46 |
47 | top = Element('annotation')
48 | if self.verified:
49 | top.set('verified', 'yes')
50 |
51 | folder = SubElement(top, 'folder')
52 | folder.text = self.foldername
53 |
54 | filename = SubElement(top, 'filename')
55 | filename.text = self.filename
56 |
57 | if self.localImgPath is not None:
58 | localImgPath = SubElement(top, 'path')
59 | localImgPath.text = self.localImgPath
60 |
61 | source = SubElement(top, 'source')
62 | database = SubElement(source, 'database')
63 | database.text = self.databaseSrc
64 |
65 | size_part = SubElement(top, 'size')
66 | width = SubElement(size_part, 'width')
67 | height = SubElement(size_part, 'height')
68 | depth = SubElement(size_part, 'depth')
69 | width.text = str(self.imgSize[1])
70 | height.text = str(self.imgSize[0])
71 | if len(self.imgSize) == 3:
72 | depth.text = str(self.imgSize[2])
73 | else:
74 | depth.text = '1'
75 |
76 | segmented = SubElement(top, 'segmented')
77 | segmented.text = '0'
78 | return top
79 |
80 | def addBndBox(self, xmin, ymin, xmax, ymax, name, difficult):
81 | bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax}
82 | bndbox['name'] = name
83 | bndbox['difficult'] = difficult
84 | self.boxlist.append(bndbox)
85 |
86 | def appendObjects(self, top):
87 | for each_object in self.boxlist:
88 | object_item = SubElement(top, 'object')
89 | name = SubElement(object_item, 'name')
90 | name.text = ustr(each_object['name'])
91 | pose = SubElement(object_item, 'pose')
92 | pose.text = "Unspecified"
93 | truncated = SubElement(object_item, 'truncated')
94 | if int(float(each_object['ymax'])) == int(float(self.imgSize[0])) or (int(float(each_object['ymin']))== 1):
95 | truncated.text = "1" # max == height or min
96 | elif (int(float(each_object['xmax']))==int(float(self.imgSize[1]))) or (int(float(each_object['xmin']))== 1):
97 | truncated.text = "1" # max == width or min
98 | else:
99 | truncated.text = "0"
100 | difficult = SubElement(object_item, 'difficult')
101 | difficult.text = str( bool(each_object['difficult']) & 1 )
102 | bndbox = SubElement(object_item, 'bndbox')
103 | xmin = SubElement(bndbox, 'xmin')
104 | xmin.text = str(each_object['xmin'])
105 | ymin = SubElement(bndbox, 'ymin')
106 | ymin.text = str(each_object['ymin'])
107 | xmax = SubElement(bndbox, 'xmax')
108 | xmax.text = str(each_object['xmax'])
109 | ymax = SubElement(bndbox, 'ymax')
110 | ymax.text = str(each_object['ymax'])
111 |
112 | def save(self, targetFile=None):
113 | root = self.genXML()
114 | self.appendObjects(root)
115 | out_file = None
116 | if targetFile is None:
117 | out_file = codecs.open(
118 | self.filename + XML_EXT, 'w', encoding=ENCODE_METHOD)
119 | else:
120 | out_file = codecs.open(targetFile, 'w', encoding=ENCODE_METHOD)
121 |
122 | prettifyResult = self.prettify(root)
123 | out_file.write(prettifyResult.decode('utf8'))
124 | out_file.close()
125 |
126 |
127 | class PascalVocReader:
128 |
129 | def __init__(self, filepath):
130 | # shapes type:
131 | # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult]
132 | self.shapes = []
133 | self.filepath = filepath
134 | self.verified = False
135 | try:
136 | self.parseXML()
137 | except:
138 | pass
139 |
140 | def getShapes(self):
141 | return self.shapes
142 |
143 | def addShape(self, label, bndbox, difficult):
144 | xmin = int(float(bndbox.find('xmin').text))
145 | ymin = int(float(bndbox.find('ymin').text))
146 | xmax = int(float(bndbox.find('xmax').text))
147 | ymax = int(float(bndbox.find('ymax').text))
148 | points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
149 | self.shapes.append((label, points, None, None, difficult))
150 |
151 | def parseXML(self):
152 | assert self.filepath.endswith(XML_EXT), "Unsupport file format"
153 | parser = etree.XMLParser(encoding=ENCODE_METHOD)
154 | xmltree = ElementTree.parse(self.filepath, parser=parser).getroot()
155 | filename = xmltree.find('filename').text
156 | try:
157 | verified = xmltree.attrib['verified']
158 | if verified == 'yes':
159 | self.verified = True
160 | except KeyError:
161 | self.verified = False
162 |
163 | for object_iter in xmltree.findall('object'):
164 | bndbox = object_iter.find("bndbox")
165 | label = object_iter.find('name').text
166 | # Add chris
167 | difficult = False
168 | if object_iter.find('difficult') is not None:
169 | difficult = bool(int(object_iter.find('difficult').text))
170 | self.addShape(label, bndbox, difficult)
171 | return True
172 |
--------------------------------------------------------------------------------
/libs/shape.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | try:
6 | from PyQt5.QtGui import *
7 | from PyQt5.QtCore import *
8 | except ImportError:
9 | from PyQt4.QtGui import *
10 | from PyQt4.QtCore import *
11 |
12 | from libs.utils import distance
13 | import sys
14 |
15 | DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
16 | DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
17 | DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255)
18 | DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155)
19 | DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255)
20 | DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0)
21 | MIN_Y_LABEL = 10
22 |
23 |
24 | class Shape(object):
25 | P_SQUARE, P_ROUND = range(2)
26 |
27 | MOVE_VERTEX, NEAR_VERTEX = range(2)
28 |
29 | # The following class variables influence the drawing
30 | # of _all_ shape objects.
31 | line_color = DEFAULT_LINE_COLOR
32 | fill_color = DEFAULT_FILL_COLOR
33 | select_line_color = DEFAULT_SELECT_LINE_COLOR
34 | select_fill_color = DEFAULT_SELECT_FILL_COLOR
35 | vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR
36 | hvertex_fill_color = DEFAULT_HVERTEX_FILL_COLOR
37 | point_type = P_ROUND
38 | point_size = 8
39 | scale = 1.0
40 |
41 | def __init__(self, label=None, line_color=None, difficult=False, paintLabel=False):
42 | self.label = label
43 | self.points = []
44 | self.fill = False
45 | self.selected = False
46 | self.difficult = difficult
47 | self.paintLabel = paintLabel
48 |
49 | self._highlightIndex = None
50 | self._highlightMode = self.NEAR_VERTEX
51 | self._highlightSettings = {
52 | self.NEAR_VERTEX: (4, self.P_ROUND),
53 | self.MOVE_VERTEX: (1.5, self.P_SQUARE),
54 | }
55 |
56 | self._closed = False
57 |
58 | if line_color is not None:
59 | # Override the class line_color attribute
60 | # with an object attribute. Currently this
61 | # is used for drawing the pending line a different color.
62 | self.line_color = line_color
63 |
64 | def close(self):
65 | self._closed = True
66 |
67 | def reachMaxPoints(self):
68 | if len(self.points) >= 4:
69 | return True
70 | return False
71 |
72 | def addPoint(self, point):
73 | if not self.reachMaxPoints():
74 | self.points.append(point)
75 |
76 | def popPoint(self):
77 | if self.points:
78 | return self.points.pop()
79 | return None
80 |
81 | def isClosed(self):
82 | return self._closed
83 |
84 | def setOpen(self):
85 | self._closed = False
86 |
87 | def paint(self, painter):
88 | if self.points:
89 | color = self.select_line_color if self.selected else self.line_color
90 | pen = QPen(color)
91 | # Try using integer sizes for smoother drawing(?)
92 | pen.setWidth(max(1, int(round(2.0 / self.scale))))
93 | painter.setPen(pen)
94 |
95 | line_path = QPainterPath()
96 | vrtx_path = QPainterPath()
97 |
98 | line_path.moveTo(self.points[0])
99 | # Uncommenting the following line will draw 2 paths
100 | # for the 1st vertex, and make it non-filled, which
101 | # may be desirable.
102 | #self.drawVertex(vrtx_path, 0)
103 |
104 | for i, p in enumerate(self.points):
105 | line_path.lineTo(p)
106 | self.drawVertex(vrtx_path, i)
107 | if self.isClosed():
108 | line_path.lineTo(self.points[0])
109 |
110 | painter.drawPath(line_path)
111 | painter.drawPath(vrtx_path)
112 | painter.fillPath(vrtx_path, self.vertex_fill_color)
113 |
114 | # Draw text at the top-left
115 | if self.paintLabel:
116 | min_x = sys.maxsize
117 | min_y = sys.maxsize
118 | for point in self.points:
119 | min_x = min(min_x, point.x())
120 | min_y = min(min_y, point.y())
121 | if min_x != sys.maxsize and min_y != sys.maxsize:
122 | font = QFont()
123 | font.setPointSize(8)
124 | font.setBold(True)
125 | painter.setFont(font)
126 | if(self.label == None):
127 | self.label = ""
128 | if(min_y < MIN_Y_LABEL):
129 | min_y += MIN_Y_LABEL
130 | painter.drawText(min_x, min_y, self.label)
131 |
132 | if self.fill:
133 | color = self.select_fill_color if self.selected else self.fill_color
134 | painter.fillPath(line_path, color)
135 |
136 | def drawVertex(self, path, i):
137 | d = self.point_size / self.scale
138 | shape = self.point_type
139 | point = self.points[i]
140 | if i == self._highlightIndex:
141 | size, shape = self._highlightSettings[self._highlightMode]
142 | d *= size
143 | if self._highlightIndex is not None:
144 | self.vertex_fill_color = self.hvertex_fill_color
145 | else:
146 | self.vertex_fill_color = Shape.vertex_fill_color
147 | if shape == self.P_SQUARE:
148 | path.addRect(point.x() - d / 2, point.y() - d / 2, d, d)
149 | elif shape == self.P_ROUND:
150 | path.addEllipse(point, d / 2.0, d / 2.0)
151 | else:
152 | assert False, "unsupported vertex shape"
153 |
154 | def nearestVertex(self, point, epsilon):
155 | for i, p in enumerate(self.points):
156 | if distance(p - point) <= epsilon:
157 | return i
158 | return None
159 |
160 | def containsPoint(self, point):
161 | return self.makePath().contains(point)
162 |
163 | def makePath(self):
164 | path = QPainterPath(self.points[0])
165 | for p in self.points[1:]:
166 | path.lineTo(p)
167 | return path
168 |
169 | def boundingRect(self):
170 | return self.makePath().boundingRect()
171 |
172 | def moveBy(self, offset):
173 | self.points = [p + offset for p in self.points]
174 |
175 | def moveVertexBy(self, i, offset):
176 | self.points[i] = self.points[i] + offset
177 |
178 | def highlightVertex(self, i, action):
179 | self._highlightIndex = i
180 | self._highlightMode = action
181 |
182 | def highlightClear(self):
183 | self._highlightIndex = None
184 |
185 | def copy(self):
186 | shape = Shape("%s" % self.label)
187 | shape.points = [p for p in self.points]
188 | shape.fill = self.fill
189 | shape.selected = self.selected
190 | shape._closed = self._closed
191 | if self.line_color != Shape.line_color:
192 | shape.line_color = self.line_color
193 | if self.fill_color != Shape.fill_color:
194 | shape.fill_color = self.fill_color
195 | shape.difficult = self.difficult
196 | return shape
197 |
198 | def __len__(self):
199 | return len(self.points)
200 |
201 | def __getitem__(self, key):
202 | return self.points[key]
203 |
204 | def __setitem__(self, key, value):
205 | self.points[key] = value
206 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | LabelImg
2 | ========
3 |
4 | .. image:: https://img.shields.io/pypi/v/labelimg.svg
5 | :target: https://pypi.python.org/pypi/labelimg
6 |
7 | .. image:: https://img.shields.io/travis/tzutalin/labelImg.svg
8 | :target: https://travis-ci.org/tzutalin/labelImg
9 |
10 | .. image:: /resources/icons/app.png
11 | :width: 200px
12 | :align: center
13 |
14 | LabelImg is a graphical image annotation tool.
15 |
16 | It is written in Python and uses Qt for its graphical interface.
17 |
18 | Annotations are saved as XML files in PASCAL VOC format, the format used
19 | by `ImageNet `__. Besides, it also supports YOLO format
20 |
21 | .. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg
22 | :alt: Demo Image
23 |
24 | .. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg
25 | :alt: Demo Image
26 |
27 | `Watch a demo video `__
28 |
29 | Installation
30 | ------------------
31 |
32 |
33 | Build from source
34 | ~~~~~~~~~~~~~~~~~
35 |
36 | Linux/Ubuntu/Mac requires at least `Python
37 | 2.6 `__ and has been tested with `PyQt
38 | 4.8 `__. However, `Python
39 | 3 or above `__ and `PyQt5 `__ are strongly recommended.
40 |
41 |
42 | Ubuntu Linux
43 | ^^^^^^^^^^^^
44 | Python 2 + Qt4
45 |
46 | .. code:: shell
47 |
48 | sudo apt-get install pyqt4-dev-tools
49 | sudo pip install lxml
50 | make qt4py2
51 | python labelImg.py
52 | python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
53 |
54 | Python 3 + Qt5 (Recommended)
55 |
56 | .. code:: shell
57 |
58 | sudo apt-get install pyqt5-dev-tools
59 | sudo pip3 install -r requirements/requirements-linux-python3.txt
60 | make qt5py3
61 | python3 labelImg.py
62 | python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
63 |
64 | macOS
65 | ^^^^^
66 | Python 2 + Qt4
67 |
68 | .. code:: shell
69 |
70 | brew install qt qt4
71 | brew install libxml2
72 | make qt4py2
73 | python labelImg.py
74 | python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
75 |
76 | Python 3 + Qt5 (Recommended)
77 |
78 | .. code:: shell
79 |
80 | brew install qt # Install qt-5.x.x by Homebrew
81 | brew install libxml2
82 |
83 | or using pip
84 |
85 | pip3 install pyqt5 lxml # Install qt and lxml by pip
86 |
87 | make qt5py3
88 | python3 labelImg.py
89 | python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
90 |
91 |
92 | Python 3 Virtualenv (Recommended)
93 |
94 | Virtualenv can avoid a lot of the QT / Python version issues
95 |
96 | .. code:: shell
97 |
98 | brew install python3
99 | pip3 install pipenv
100 | pipenv run pip install pyqt5==5.13.2 lxml
101 | pipenv run make qt5py3
102 | python3 labelImg.py
103 | [Optional] rm -rf build dist; python setup.py py2app -A;mv "dist/labelImg.app" /Applications
104 |
105 | Note: The Last command gives you a nice .app file with a new SVG Icon in your /Applications folder. You can consider using the script: build-tools/build-for-macos.sh
106 |
107 |
108 | Windows
109 | ^^^^^^^
110 |
111 | Install `Python `__,
112 | `PyQt5 `__
113 | and `install lxml `__.
114 |
115 | Open cmd and go to the `labelImg <#labelimg>`__ directory
116 |
117 | .. code:: shell
118 |
119 | pyrcc4 -o lib/resources.py resources.qrc
120 | For pyqt5, pyrcc5 -o libs/resources.py resources.qrc
121 |
122 | python labelImg.py
123 | python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
124 |
125 | Windows + Anaconda
126 | ^^^^^^^^^^^^^^^^^^
127 |
128 | Download and install `Anaconda `__ (Python 3+)
129 |
130 | Open the Anaconda Prompt and go to the `labelImg <#labelimg>`__ directory
131 |
132 | .. code:: shell
133 |
134 | conda install pyqt=5
135 | conda install -c anaconda lxml
136 | pyrcc5 -o libs/resources.py resources.qrc
137 | python labelImg.py
138 | python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
139 |
140 | Get from PyPI but only python3.0 or above
141 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
142 | This is the simplest (one-command) install method on modern Linux distributions such as Ubuntu and Fedora.
143 |
144 | .. code:: shell
145 |
146 | pip3 install labelImg
147 | labelImg
148 | labelImg [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
149 |
150 |
151 | Use Docker
152 | ~~~~~~~~~~~~~~~~~
153 | .. code:: shell
154 |
155 | docker run -it \
156 | --user $(id -u) \
157 | -e DISPLAY=unix$DISPLAY \
158 | --workdir=$(pwd) \
159 | --volume="/home/$USER:/home/$USER" \
160 | --volume="/etc/group:/etc/group:ro" \
161 | --volume="/etc/passwd:/etc/passwd:ro" \
162 | --volume="/etc/shadow:/etc/shadow:ro" \
163 | --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \
164 | -v /tmp/.X11-unix:/tmp/.X11-unix \
165 | tzutalin/py2qt4
166 |
167 | make qt4py2;./labelImg.py
168 |
169 | You can pull the image which has all of the installed and required dependencies. `Watch a demo video `__
170 |
171 |
172 | Usage
173 | -----
174 |
175 | Steps (PascalVOC)
176 | ~~~~~~~~~~~~~~~~~
177 |
178 | 1. Build and launch using the instructions above.
179 | 2. Click 'Change default saved annotation folder' in Menu/File
180 | 3. Click 'Open Dir'
181 | 4. Click 'Create RectBox'
182 | 5. Click and release left mouse to select a region to annotate the rect
183 | box
184 | 6. You can use right mouse to drag the rect box to copy or move it
185 |
186 | The annotation will be saved to the folder you specify.
187 |
188 | You can refer to the below hotkeys to speed up your workflow.
189 |
190 | Steps (YOLO)
191 | ~~~~~~~~~~~~
192 |
193 | 1. In ``data/predefined_classes.txt`` define the list of classes that will be used for your training.
194 |
195 | 2. Build and launch using the instructions above.
196 |
197 | 3. Right below "Save" button in the toolbar, click "PascalVOC" button to switch to YOLO format.
198 |
199 | 4. You may use Open/OpenDIR to process single or multiple images. When finished with a single image, click save.
200 |
201 | A txt file of YOLO format will be saved in the same folder as your image with same name. A file named "classes.txt" is saved to that folder too. "classes.txt" defines the list of class names that your YOLO label refers to.
202 |
203 | Note:
204 |
205 | - Your label list shall not change in the middle of processing a list of images. When you save an image, classes.txt will also get updated, while previous annotations will not be updated.
206 |
207 | - You shouldn't use "default class" function when saving to YOLO format, it will not be referred.
208 |
209 | - When saving as YOLO format, "difficult" flag is discarded.
210 |
211 | Create pre-defined classes
212 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
213 |
214 | You can edit the
215 | `data/predefined\_classes.txt `__
216 | to load pre-defined classes
217 |
218 | Hotkeys
219 | ~~~~~~~
220 |
221 | +------------+--------------------------------------------+
222 | | Ctrl + u | Load all of the images from a directory |
223 | +------------+--------------------------------------------+
224 | | Ctrl + r | Change the default annotation target dir |
225 | +------------+--------------------------------------------+
226 | | Ctrl + s | Save |
227 | +------------+--------------------------------------------+
228 | | Ctrl + d | Copy the current label and rect box |
229 | +------------+--------------------------------------------+
230 | | Space | Flag the current image as verified |
231 | +------------+--------------------------------------------+
232 | | w | Create a rect box |
233 | +------------+--------------------------------------------+
234 | | d | Next image |
235 | +------------+--------------------------------------------+
236 | | a | Previous image |
237 | +------------+--------------------------------------------+
238 | | del | Delete the selected rect box |
239 | +------------+--------------------------------------------+
240 | | Ctrl++ | Zoom in |
241 | +------------+--------------------------------------------+
242 | | Ctrl-- | Zoom out |
243 | +------------+--------------------------------------------+
244 | | ↑→↓← | Keyboard arrows to move selected rect box |
245 | +------------+--------------------------------------------+
246 |
247 | **Verify Image:**
248 |
249 | When pressing space, the user can flag the image as verified, a green background will appear.
250 | This is used when creating a dataset automatically, the user can then through all the pictures and flag them instead of annotate them.
251 |
252 | **Difficult:**
253 |
254 | The difficult field is set to 1 indicates that the object has been annotated as "difficult", for example, an object which is clearly visible but difficult to recognize without substantial use of context.
255 | According to your deep neural network implementation, you can include or exclude difficult objects during training.
256 |
257 | How to reset the settings
258 | ~~~~~~~~~~~~~~~~~~~~~~~~~
259 |
260 | In case there are issues with loading the classes, you can either:
261 |
262 | 1. From the top menu of the labelimg click on Menu/File/Reset All
263 | 2. Remove the `.labelImgSettings.pkl` from your home directory. In Linux and Mac you can do:
264 | `rm ~/.labelImgSettings.pkl`
265 |
266 |
267 | How to contribute
268 | ~~~~~~~~~~~~~~~~~
269 |
270 | Send a pull request
271 |
272 | License
273 | ~~~~~~~
274 | `Free software: MIT license `_
275 |
276 | Citation: Tzutalin. LabelImg. Git code (2015). https://github.com/tzutalin/labelImg
277 |
278 | Related
279 | ~~~~~~~
280 |
281 | 1. `ImageNet Utils `__ to
282 | download image, create a label text for machine learning, etc
283 | 2. `Use Docker to run labelImg `__
284 | 3. `Generating the PASCAL VOC TFRecord files `__
285 | 4. `App Icon based on Icon by Nick Roach (GPL) `__
286 | 5. `Setup python development in vscode `__
287 | 6. `The link of this project on iHub platform `__
288 |
289 |
290 | Stargazers over time
291 | ~~~~~~~~~~~~~~~~~~~~
292 |
293 | .. image:: https://starchart.cc/tzutalin/labelImg.svg
294 |
295 |
--------------------------------------------------------------------------------
/resources/icons/open.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/resources/icons/done.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
401 |
--------------------------------------------------------------------------------
/libs/canvas.py:
--------------------------------------------------------------------------------
1 |
2 | try:
3 | from PyQt5.QtGui import *
4 | from PyQt5.QtCore import *
5 | from PyQt5.QtWidgets import *
6 | except ImportError:
7 | from PyQt4.QtGui import *
8 | from PyQt4.QtCore import *
9 |
10 | #from PyQt4.QtOpenGL import *
11 |
12 | from libs.shape import Shape
13 | from libs.utils import distance
14 |
15 | CURSOR_DEFAULT = Qt.ArrowCursor
16 | CURSOR_POINT = Qt.PointingHandCursor
17 | CURSOR_DRAW = Qt.CrossCursor
18 | CURSOR_MOVE = Qt.ClosedHandCursor
19 | CURSOR_GRAB = Qt.OpenHandCursor
20 |
21 | # class Canvas(QGLWidget):
22 |
23 |
24 | class Canvas(QWidget):
25 | zoomRequest = pyqtSignal(int)
26 | scrollRequest = pyqtSignal(int, int)
27 | newShape = pyqtSignal()
28 | selectionChanged = pyqtSignal(bool)
29 | shapeMoved = pyqtSignal()
30 | drawingPolygon = pyqtSignal(bool)
31 |
32 | CREATE, EDIT = list(range(2))
33 |
34 | epsilon = 11.0
35 |
36 | def __init__(self, *args, **kwargs):
37 | super(Canvas, self).__init__(*args, **kwargs)
38 | # Initialise local state.
39 | self.mode = self.EDIT
40 | self.shapes = []
41 | self.current = None
42 | self.selectedShape = None # save the selected shape here
43 | self.selectedShapeCopy = None
44 | self.drawingLineColor = QColor(0, 0, 255)
45 | self.drawingRectColor = QColor(0, 0, 255)
46 | self.line = Shape(line_color=self.drawingLineColor)
47 | self.prevPoint = QPointF()
48 | self.offsets = QPointF(), QPointF()
49 | self.scale = 1.0
50 | self.pixmap = QPixmap()
51 | self.visible = {}
52 | self._hideBackround = False
53 | self.hideBackround = False
54 | self.hShape = None
55 | self.hVertex = None
56 | self._painter = QPainter()
57 | self._cursor = CURSOR_DEFAULT
58 | # Menus:
59 | self.menus = (QMenu(), QMenu())
60 | # Set widget options.
61 | self.setMouseTracking(True)
62 | self.setFocusPolicy(Qt.WheelFocus)
63 | self.verified = False
64 | self.drawSquare = False
65 |
66 | def setDrawingColor(self, qColor):
67 | self.drawingLineColor = qColor
68 | self.drawingRectColor = qColor
69 |
70 | def enterEvent(self, ev):
71 | self.overrideCursor(self._cursor)
72 |
73 | def leaveEvent(self, ev):
74 | self.restoreCursor()
75 |
76 | def focusOutEvent(self, ev):
77 | self.restoreCursor()
78 |
79 | def isVisible(self, shape):
80 | return self.visible.get(shape, True)
81 |
82 | def drawing(self):
83 | return self.mode == self.CREATE
84 |
85 | def editing(self):
86 | return self.mode == self.EDIT
87 |
88 | def setEditing(self, value=True):
89 | self.mode = self.EDIT if value else self.CREATE
90 | if not value: # Create
91 | self.unHighlight()
92 | self.deSelectShape()
93 | self.prevPoint = QPointF()
94 | self.repaint()
95 |
96 | def unHighlight(self):
97 | if self.hShape:
98 | self.hShape.highlightClear()
99 | self.hVertex = self.hShape = None
100 |
101 | def selectedVertex(self):
102 | return self.hVertex is not None
103 |
104 | def mouseMoveEvent(self, ev):
105 | """Update line with last point and current coordinates."""
106 | pos = self.transformPos(ev.pos())
107 |
108 | # Update coordinates in status bar if image is opened
109 | window = self.parent().window()
110 | if window.filePath is not None:
111 | self.parent().window().labelCoordinates.setText(
112 | 'X: %d; Y: %d' % (pos.x(), pos.y()))
113 |
114 | # Polygon drawing.
115 | if self.drawing():
116 | self.overrideCursor(CURSOR_DRAW)
117 | if self.current:
118 | # Display annotation width and height while drawing
119 | currentWidth = abs(self.current[0].x() - pos.x())
120 | currentHeight = abs(self.current[0].y() - pos.y())
121 | self.parent().window().labelCoordinates.setText(
122 | 'Width: %d, Height: %d / X: %d; Y: %d' % (currentWidth, currentHeight, pos.x(), pos.y()))
123 |
124 | color = self.drawingLineColor
125 | if self.outOfPixmap(pos):
126 | # Don't allow the user to draw outside the pixmap.
127 | # Clip the coordinates to 0 or max,
128 | # if they are outside the range [0, max]
129 | size = self.pixmap.size()
130 | clipped_x = min(max(0, pos.x()), size.width())
131 | clipped_y = min(max(0, pos.y()), size.height())
132 | pos = QPointF(clipped_x, clipped_y)
133 | elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
134 | # Attract line to starting point and colorise to alert the
135 | # user:
136 | pos = self.current[0]
137 | color = self.current.line_color
138 | self.overrideCursor(CURSOR_POINT)
139 | self.current.highlightVertex(0, Shape.NEAR_VERTEX)
140 |
141 | if self.drawSquare:
142 | initPos = self.current[0]
143 | minX = initPos.x()
144 | minY = initPos.y()
145 | min_size = min(abs(pos.x() - minX), abs(pos.y() - minY))
146 | directionX = -1 if pos.x() - minX < 0 else 1
147 | directionY = -1 if pos.y() - minY < 0 else 1
148 | self.line[1] = QPointF(minX + directionX * min_size, minY + directionY * min_size)
149 | else:
150 | self.line[1] = pos
151 |
152 | self.line.line_color = color
153 | self.prevPoint = QPointF()
154 | self.current.highlightClear()
155 | else:
156 | self.prevPoint = pos
157 | self.repaint()
158 | return
159 |
160 | # Polygon copy moving.
161 | if Qt.RightButton & ev.buttons():
162 | if self.selectedShapeCopy and self.prevPoint:
163 | self.overrideCursor(CURSOR_MOVE)
164 | self.boundedMoveShape(self.selectedShapeCopy, pos)
165 | self.repaint()
166 | elif self.selectedShape:
167 | self.selectedShapeCopy = self.selectedShape.copy()
168 | self.repaint()
169 | return
170 |
171 | # Polygon/Vertex moving.
172 | if Qt.LeftButton & ev.buttons():
173 | if self.selectedVertex():
174 | self.boundedMoveVertex(pos)
175 | self.shapeMoved.emit()
176 | self.repaint()
177 | elif self.selectedShape and self.prevPoint:
178 | self.overrideCursor(CURSOR_MOVE)
179 | self.boundedMoveShape(self.selectedShape, pos)
180 | self.shapeMoved.emit()
181 | self.repaint()
182 | return
183 |
184 | # Just hovering over the canvas, 2 posibilities:
185 | # - Highlight shapes
186 | # - Highlight vertex
187 | # Update shape/vertex fill and tooltip value accordingly.
188 | self.setToolTip("Image")
189 | for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
190 | # Look for a nearby vertex to highlight. If that fails,
191 | # check if we happen to be inside a shape.
192 | index = shape.nearestVertex(pos, self.epsilon)
193 | if index is not None:
194 | if self.selectedVertex():
195 | self.hShape.highlightClear()
196 | self.hVertex, self.hShape = index, shape
197 | shape.highlightVertex(index, shape.MOVE_VERTEX)
198 | self.overrideCursor(CURSOR_POINT)
199 | self.setToolTip("Click & drag to move point")
200 | self.setStatusTip(self.toolTip())
201 | self.update()
202 | break
203 | elif shape.containsPoint(pos):
204 | if self.selectedVertex():
205 | self.hShape.highlightClear()
206 | self.hVertex, self.hShape = None, shape
207 | self.setToolTip(
208 | "Click & drag to move shape '%s'" % shape.label)
209 | self.setStatusTip(self.toolTip())
210 | self.overrideCursor(CURSOR_GRAB)
211 | self.update()
212 | break
213 | else: # Nothing found, clear highlights, reset state.
214 | if self.hShape:
215 | self.hShape.highlightClear()
216 | self.update()
217 | self.hVertex, self.hShape = None, None
218 | self.overrideCursor(CURSOR_DEFAULT)
219 |
220 | def mousePressEvent(self, ev):
221 | pos = self.transformPos(ev.pos())
222 |
223 | if ev.button() == Qt.LeftButton:
224 | if self.drawing():
225 | self.handleDrawing(pos)
226 | else:
227 | self.selectShapePoint(pos)
228 | self.prevPoint = pos
229 | self.repaint()
230 | elif ev.button() == Qt.RightButton and self.editing():
231 | self.selectShapePoint(pos)
232 | self.prevPoint = pos
233 | self.repaint()
234 |
235 | def mouseReleaseEvent(self, ev):
236 | if ev.button() == Qt.RightButton:
237 | menu = self.menus[bool(self.selectedShapeCopy)]
238 | self.restoreCursor()
239 | if not menu.exec_(self.mapToGlobal(ev.pos()))\
240 | and self.selectedShapeCopy:
241 | # Cancel the move by deleting the shadow copy.
242 | self.selectedShapeCopy = None
243 | self.repaint()
244 | elif ev.button() == Qt.LeftButton and self.selectedShape:
245 | if self.selectedVertex():
246 | self.overrideCursor(CURSOR_POINT)
247 | else:
248 | self.overrideCursor(CURSOR_GRAB)
249 | elif ev.button() == Qt.LeftButton:
250 | pos = self.transformPos(ev.pos())
251 | if self.drawing():
252 | self.handleDrawing(pos)
253 |
254 | def endMove(self, copy=False):
255 | assert self.selectedShape and self.selectedShapeCopy
256 | shape = self.selectedShapeCopy
257 | #del shape.fill_color
258 | #del shape.line_color
259 | if copy:
260 | self.shapes.append(shape)
261 | self.selectedShape.selected = False
262 | self.selectedShape = shape
263 | self.repaint()
264 | else:
265 | self.selectedShape.points = [p for p in shape.points]
266 | self.selectedShapeCopy = None
267 |
268 | def hideBackroundShapes(self, value):
269 | self.hideBackround = value
270 | if self.selectedShape:
271 | # Only hide other shapes if there is a current selection.
272 | # Otherwise the user will not be able to select a shape.
273 | self.setHiding(True)
274 | self.repaint()
275 |
276 | def handleDrawing(self, pos):
277 | if self.current and self.current.reachMaxPoints() is False:
278 | initPos = self.current[0]
279 | minX = initPos.x()
280 | minY = initPos.y()
281 | targetPos = self.line[1]
282 | maxX = targetPos.x()
283 | maxY = targetPos.y()
284 | self.current.addPoint(QPointF(maxX, minY))
285 | self.current.addPoint(targetPos)
286 | self.current.addPoint(QPointF(minX, maxY))
287 | self.finalise()
288 | elif not self.outOfPixmap(pos):
289 | self.current = Shape()
290 | self.current.addPoint(pos)
291 | self.line.points = [pos, pos]
292 | self.setHiding()
293 | self.drawingPolygon.emit(True)
294 | self.update()
295 |
296 | def setHiding(self, enable=True):
297 | self._hideBackround = self.hideBackround if enable else False
298 |
299 | def canCloseShape(self):
300 | return self.drawing() and self.current and len(self.current) > 2
301 |
302 | def mouseDoubleClickEvent(self, ev):
303 | # We need at least 4 points here, since the mousePress handler
304 | # adds an extra one before this handler is called.
305 | if self.canCloseShape() and len(self.current) > 3:
306 | self.current.popPoint()
307 | self.finalise()
308 |
309 | def selectShape(self, shape):
310 | self.deSelectShape()
311 | shape.selected = True
312 | self.selectedShape = shape
313 | self.setHiding()
314 | self.selectionChanged.emit(True)
315 | self.update()
316 |
317 | def selectShapePoint(self, point):
318 | """Select the first shape created which contains this point."""
319 | self.deSelectShape()
320 | if self.selectedVertex(): # A vertex is marked for selection.
321 | index, shape = self.hVertex, self.hShape
322 | shape.highlightVertex(index, shape.MOVE_VERTEX)
323 | self.selectShape(shape)
324 | return
325 | for shape in reversed(self.shapes):
326 | if self.isVisible(shape) and shape.containsPoint(point):
327 | self.selectShape(shape)
328 | self.calculateOffsets(shape, point)
329 | return
330 |
331 | def calculateOffsets(self, shape, point):
332 | rect = shape.boundingRect()
333 | x1 = rect.x() - point.x()
334 | y1 = rect.y() - point.y()
335 | x2 = (rect.x() + rect.width()) - point.x()
336 | y2 = (rect.y() + rect.height()) - point.y()
337 | self.offsets = QPointF(x1, y1), QPointF(x2, y2)
338 |
339 | def snapPointToCanvas(self, x, y):
340 | """
341 | Moves a point x,y to within the boundaries of the canvas.
342 | :return: (x,y,snapped) where snapped is True if x or y were changed, False if not.
343 | """
344 | if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height():
345 | x = max(x, 0)
346 | y = max(y, 0)
347 | x = min(x, self.pixmap.width())
348 | y = min(y, self.pixmap.height())
349 | return x, y, True
350 |
351 | return x, y, False
352 |
353 | def boundedMoveVertex(self, pos):
354 | index, shape = self.hVertex, self.hShape
355 | point = shape[index]
356 | if self.outOfPixmap(pos):
357 | size = self.pixmap.size()
358 | clipped_x = min(max(0, pos.x()), size.width())
359 | clipped_y = min(max(0, pos.y()), size.height())
360 | pos = QPointF(clipped_x, clipped_y)
361 |
362 | if self.drawSquare:
363 | opposite_point_index = (index + 2) % 4
364 | opposite_point = shape[opposite_point_index]
365 |
366 | min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y()))
367 | directionX = -1 if pos.x() - opposite_point.x() < 0 else 1
368 | directionY = -1 if pos.y() - opposite_point.y() < 0 else 1
369 | shiftPos = QPointF(opposite_point.x() + directionX * min_size - point.x(),
370 | opposite_point.y() + directionY * min_size - point.y())
371 | else:
372 | shiftPos = pos - point
373 |
374 | shape.moveVertexBy(index, shiftPos)
375 |
376 | lindex = (index + 1) % 4
377 | rindex = (index + 3) % 4
378 | lshift = None
379 | rshift = None
380 | if index % 2 == 0:
381 | rshift = QPointF(shiftPos.x(), 0)
382 | lshift = QPointF(0, shiftPos.y())
383 | else:
384 | lshift = QPointF(shiftPos.x(), 0)
385 | rshift = QPointF(0, shiftPos.y())
386 | shape.moveVertexBy(rindex, rshift)
387 | shape.moveVertexBy(lindex, lshift)
388 |
389 | def boundedMoveShape(self, shape, pos):
390 | if self.outOfPixmap(pos):
391 | return False # No need to move
392 | o1 = pos + self.offsets[0]
393 | if self.outOfPixmap(o1):
394 | pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
395 | o2 = pos + self.offsets[1]
396 | if self.outOfPixmap(o2):
397 | pos += QPointF(min(0, self.pixmap.width() - o2.x()),
398 | min(0, self.pixmap.height() - o2.y()))
399 | # The next line tracks the new position of the cursor
400 | # relative to the shape, but also results in making it
401 | # a bit "shaky" when nearing the border and allows it to
402 | # go outside of the shape's area for some reason. XXX
403 | #self.calculateOffsets(self.selectedShape, pos)
404 | dp = pos - self.prevPoint
405 | if dp:
406 | shape.moveBy(dp)
407 | self.prevPoint = pos
408 | return True
409 | return False
410 |
411 | def deSelectShape(self):
412 | if self.selectedShape:
413 | self.selectedShape.selected = False
414 | self.selectedShape = None
415 | self.setHiding(False)
416 | self.selectionChanged.emit(False)
417 | self.update()
418 |
419 | def deleteSelected(self):
420 | if self.selectedShape:
421 | shape = self.selectedShape
422 | self.shapes.remove(self.selectedShape)
423 | self.selectedShape = None
424 | self.update()
425 | return shape
426 |
427 | def copySelectedShape(self):
428 | if self.selectedShape:
429 | shape = self.selectedShape.copy()
430 | self.deSelectShape()
431 | self.shapes.append(shape)
432 | shape.selected = True
433 | self.selectedShape = shape
434 | self.boundedShiftShape(shape)
435 | return shape
436 |
437 | def boundedShiftShape(self, shape):
438 | # Try to move in one direction, and if it fails in another.
439 | # Give up if both fail.
440 | point = shape[0]
441 | offset = QPointF(2.0, 2.0)
442 | self.calculateOffsets(shape, point)
443 | self.prevPoint = point
444 | if not self.boundedMoveShape(shape, point - offset):
445 | self.boundedMoveShape(shape, point + offset)
446 |
447 | def paintEvent(self, event):
448 | if not self.pixmap:
449 | return super(Canvas, self).paintEvent(event)
450 |
451 | p = self._painter
452 | p.begin(self)
453 | p.setRenderHint(QPainter.Antialiasing)
454 | p.setRenderHint(QPainter.HighQualityAntialiasing)
455 | p.setRenderHint(QPainter.SmoothPixmapTransform)
456 |
457 | p.scale(self.scale, self.scale)
458 | p.translate(self.offsetToCenter())
459 |
460 | p.drawPixmap(0, 0, self.pixmap)
461 | Shape.scale = self.scale
462 | for shape in self.shapes:
463 | if (shape.selected or not self._hideBackround) and self.isVisible(shape):
464 | shape.fill = shape.selected or shape == self.hShape
465 | shape.paint(p)
466 | if self.current:
467 | self.current.paint(p)
468 | self.line.paint(p)
469 | if self.selectedShapeCopy:
470 | self.selectedShapeCopy.paint(p)
471 |
472 | # Paint rect
473 | if self.current is not None and len(self.line) == 2:
474 | leftTop = self.line[0]
475 | rightBottom = self.line[1]
476 | rectWidth = rightBottom.x() - leftTop.x()
477 | rectHeight = rightBottom.y() - leftTop.y()
478 | p.setPen(self.drawingRectColor)
479 | brush = QBrush(Qt.BDiagPattern)
480 | p.setBrush(brush)
481 | p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight)
482 |
483 | if self.drawing() and not self.prevPoint.isNull() and not self.outOfPixmap(self.prevPoint):
484 | p.setPen(QColor(0, 0, 0))
485 | p.drawLine(self.prevPoint.x(), 0, self.prevPoint.x(), self.pixmap.height())
486 | p.drawLine(0, self.prevPoint.y(), self.pixmap.width(), self.prevPoint.y())
487 |
488 | self.setAutoFillBackground(True)
489 | if self.verified:
490 | pal = self.palette()
491 | pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128))
492 | self.setPalette(pal)
493 | else:
494 | pal = self.palette()
495 | pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255))
496 | self.setPalette(pal)
497 |
498 | p.end()
499 |
500 | def transformPos(self, point):
501 | """Convert from widget-logical coordinates to painter-logical coordinates."""
502 | return point / self.scale - self.offsetToCenter()
503 |
504 | def offsetToCenter(self):
505 | s = self.scale
506 | area = super(Canvas, self).size()
507 | w, h = self.pixmap.width() * s, self.pixmap.height() * s
508 | aw, ah = area.width(), area.height()
509 | x = (aw - w) / (2 * s) if aw > w else 0
510 | y = (ah - h) / (2 * s) if ah > h else 0
511 | return QPointF(x, y)
512 |
513 | def outOfPixmap(self, p):
514 | w, h = self.pixmap.width(), self.pixmap.height()
515 | return not (0 <= p.x() <= w and 0 <= p.y() <= h)
516 |
517 | def finalise(self):
518 | assert self.current
519 | if self.current.points[0] == self.current.points[-1]:
520 | self.current = None
521 | self.drawingPolygon.emit(False)
522 | self.update()
523 | return
524 |
525 | self.current.close()
526 | self.shapes.append(self.current)
527 | self.current = None
528 | self.setHiding(False)
529 | self.newShape.emit()
530 | self.update()
531 |
532 | def closeEnough(self, p1, p2):
533 | #d = distance(p1 - p2)
534 | #m = (p1-p2).manhattanLength()
535 | # print "d %.2f, m %d, %.2f" % (d, m, d - m)
536 | return distance(p1 - p2) < self.epsilon
537 |
538 | # These two, along with a call to adjustSize are required for the
539 | # scroll area.
540 | def sizeHint(self):
541 | return self.minimumSizeHint()
542 |
543 | def minimumSizeHint(self):
544 | if self.pixmap:
545 | return self.scale * self.pixmap.size()
546 | return super(Canvas, self).minimumSizeHint()
547 |
548 | def wheelEvent(self, ev):
549 | qt_version = 4 if hasattr(ev, "delta") else 5
550 | if qt_version == 4:
551 | if ev.orientation() == Qt.Vertical:
552 | v_delta = ev.delta()
553 | h_delta = 0
554 | else:
555 | h_delta = ev.delta()
556 | v_delta = 0
557 | else:
558 | delta = ev.angleDelta()
559 | h_delta = delta.x()
560 | v_delta = delta.y()
561 |
562 | mods = ev.modifiers()
563 | if Qt.ControlModifier == int(mods) and v_delta:
564 | self.zoomRequest.emit(v_delta)
565 | else:
566 | v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical)
567 | h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal)
568 | ev.accept()
569 |
570 | def keyPressEvent(self, ev):
571 | key = ev.key()
572 | if key == Qt.Key_Escape and self.current:
573 | print('ESC press')
574 | self.current = None
575 | self.drawingPolygon.emit(False)
576 | self.update()
577 | elif key == Qt.Key_Return and self.canCloseShape():
578 | self.finalise()
579 | elif key == Qt.Key_Left and self.selectedShape:
580 | self.moveOnePixel('Left')
581 | elif key == Qt.Key_Right and self.selectedShape:
582 | self.moveOnePixel('Right')
583 | elif key == Qt.Key_Up and self.selectedShape:
584 | self.moveOnePixel('Up')
585 | elif key == Qt.Key_Down and self.selectedShape:
586 | self.moveOnePixel('Down')
587 |
588 | def moveOnePixel(self, direction):
589 | # print(self.selectedShape.points)
590 | if direction == 'Left' and not self.moveOutOfBound(QPointF(-1.0, 0)):
591 | # print("move Left one pixel")
592 | self.selectedShape.points[0] += QPointF(-1.0, 0)
593 | self.selectedShape.points[1] += QPointF(-1.0, 0)
594 | self.selectedShape.points[2] += QPointF(-1.0, 0)
595 | self.selectedShape.points[3] += QPointF(-1.0, 0)
596 | elif direction == 'Right' and not self.moveOutOfBound(QPointF(1.0, 0)):
597 | # print("move Right one pixel")
598 | self.selectedShape.points[0] += QPointF(1.0, 0)
599 | self.selectedShape.points[1] += QPointF(1.0, 0)
600 | self.selectedShape.points[2] += QPointF(1.0, 0)
601 | self.selectedShape.points[3] += QPointF(1.0, 0)
602 | elif direction == 'Up' and not self.moveOutOfBound(QPointF(0, -1.0)):
603 | # print("move Up one pixel")
604 | self.selectedShape.points[0] += QPointF(0, -1.0)
605 | self.selectedShape.points[1] += QPointF(0, -1.0)
606 | self.selectedShape.points[2] += QPointF(0, -1.0)
607 | self.selectedShape.points[3] += QPointF(0, -1.0)
608 | elif direction == 'Down' and not self.moveOutOfBound(QPointF(0, 1.0)):
609 | # print("move Down one pixel")
610 | self.selectedShape.points[0] += QPointF(0, 1.0)
611 | self.selectedShape.points[1] += QPointF(0, 1.0)
612 | self.selectedShape.points[2] += QPointF(0, 1.0)
613 | self.selectedShape.points[3] += QPointF(0, 1.0)
614 | self.shapeMoved.emit()
615 | self.repaint()
616 |
617 | def moveOutOfBound(self, step):
618 | points = [p1+p2 for p1, p2 in zip(self.selectedShape.points, [step]*4)]
619 | return True in map(self.outOfPixmap, points)
620 |
621 | def setLastLabel(self, text, line_color = None, fill_color = None):
622 | assert text
623 | self.shapes[-1].label = text
624 | if line_color:
625 | self.shapes[-1].line_color = line_color
626 |
627 | if fill_color:
628 | self.shapes[-1].fill_color = fill_color
629 |
630 | return self.shapes[-1]
631 |
632 | def undoLastLine(self):
633 | assert self.shapes
634 | self.current = self.shapes.pop()
635 | self.current.setOpen()
636 | self.line.points = [self.current[-1], self.current[0]]
637 | self.drawingPolygon.emit(True)
638 |
639 | def resetAllLines(self):
640 | assert self.shapes
641 | self.current = self.shapes.pop()
642 | self.current.setOpen()
643 | self.line.points = [self.current[-1], self.current[0]]
644 | self.drawingPolygon.emit(True)
645 | self.current = None
646 | self.drawingPolygon.emit(False)
647 | self.update()
648 |
649 | def loadPixmap(self, pixmap):
650 | self.pixmap = pixmap
651 | self.shapes = []
652 | self.repaint()
653 |
654 | def loadShapes(self, shapes):
655 | self.shapes = list(shapes)
656 | self.current = None
657 | self.repaint()
658 |
659 | def setShapeVisible(self, shape, value):
660 | self.visible[shape] = value
661 | self.repaint()
662 |
663 | def currentCursor(self):
664 | cursor = QApplication.overrideCursor()
665 | if cursor is not None:
666 | cursor = cursor.shape()
667 | return cursor
668 |
669 | def overrideCursor(self, cursor):
670 | self._cursor = cursor
671 | if self.currentCursor() is None:
672 | QApplication.setOverrideCursor(cursor)
673 | else:
674 | QApplication.changeOverrideCursor(cursor)
675 |
676 | def restoreCursor(self):
677 | QApplication.restoreOverrideCursor()
678 |
679 | def resetState(self):
680 | self.restoreCursor()
681 | self.pixmap = None
682 | self.update()
683 |
684 | def setDrawingShapeToSquare(self, status):
685 | self.drawSquare = status
686 |
--------------------------------------------------------------------------------
/resources/icons/save.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
680 |
--------------------------------------------------------------------------------