├── __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 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 22 | 24 | 26 | 29 | 30 | 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 | image/svg+xml 42 | 43 | 45 | 52 | 54 | 55 | 56 | 58 | 60 | 67 | 68 | 70 | 77 | 81 | 85 | 89 | 92 | 95 | 98 | 101 | 104 | 105 | 109 | 116 | 120 | 124 | 128 | 132 | 135 | 138 | 141 | 144 | 147 | 150 | 153 | 154 | 158 | 165 | 169 | 173 | 177 | 181 | 185 | 188 | 191 | 194 | 195 | 199 | 206 | 210 | 214 | 218 | 222 | 226 | 229 | 232 | 235 | 236 | 240 | 247 | 251 | 255 | 258 | 261 | 264 | 265 | 269 | 276 | 280 | 284 | 288 | 291 | 294 | 297 | 300 | 303 | 304 | 308 | 315 | 319 | 323 | 327 | 330 | 333 | 336 | 339 | 342 | 343 | 347 | 354 | 358 | 362 | 365 | 368 | 371 | 372 | 376 | 383 | 387 | 391 | 395 | 399 | 403 | 407 | 411 | 415 | 418 | 421 | 424 | 425 | 429 | 433 | 440 | 444 | 448 | 451 | 454 | 457 | 458 | 462 | 466 | 473 | 477 | 481 | 484 | 487 | 490 | 491 | 495 | 502 | 506 | 510 | 514 | 517 | 520 | 523 | 526 | 529 | 530 | 534 | 541 | 545 | 549 | 552 | 555 | 558 | 559 | 563 | 567 | 574 | 575 | 576 | 577 | -------------------------------------------------------------------------------- /resources/icons/done.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 27 | 29 | 36 | 40 | 44 | 48 | 52 | 56 | 57 | 64 | 68 | 72 | 76 | 80 | 84 | 85 | 92 | 96 | 100 | 104 | 108 | 112 | 113 | 121 | 125 | 129 | 133 | 137 | 141 | 142 | 143 | 145 | 147 | begin='' id='W5M0MpCehiHzreSzNTczkc9d' 148 | 150 | 151 | 153 | 154 | Adobe PDF library 5.00 155 | 156 | 158 | 160 | 162 | 163 | 2003-12-22T22:34:35+02:00 164 | 165 | 2004-04-17T21:25:50Z 166 | 167 | Adobe Illustrator 10.0 168 | 169 | 2004-01-19T17:51:02+01:00 170 | 171 | 172 | 174 | 175 | JPEG 176 | 177 | 256 178 | 179 | 256 180 | 181 | /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA 182 | AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK 183 | DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f 184 | Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER 185 | AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA 186 | AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB 187 | UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 188 | 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ 189 | qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy 190 | obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 191 | 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo 192 | +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 193 | FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F 194 | XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX 195 | Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY 196 | 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 342 | 343 | 344 | 345 | 346 | 348 | 349 | uuid:4b4d592f-95b8-4bcd-a892-74a536c5e52f 350 | 351 | 353 | 354 | image/svg+xml 355 | 356 | 357 | 359 | test.ai 360 | 361 | 362 | 363 | 364 | 365 | end='w' 366 | 367 | 372 | 376 | 380 | 384 | 388 | 392 | 396 | 400 | 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 | 27 | 29 | 31 | 33 | begin='' id='W5M0MpCehiHzreSzNTczkc9d' 34 | 36 | 37 | 39 | 40 | Adobe PDF library 5.00 41 | 42 | 44 | 46 | 48 | 49 | 2004-02-04T02:08:51+02:00 50 | 51 | 2004-03-29T09:20:16Z 52 | 53 | Adobe Illustrator 10.0 54 | 55 | 2004-02-29T14:54:28+01:00 56 | 57 | 58 | 60 | 61 | JPEG 62 | 63 | 256 64 | 65 | 256 66 | 67 | /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA 68 | AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK 69 | DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f 70 | Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER 71 | AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA 72 | AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB 73 | UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 74 | 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ 75 | qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy 76 | obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 77 | 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo 78 | +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 79 | FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F 80 | XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX 81 | Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY 82 | 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 268 | 269 | 270 | 271 | 272 | 274 | 275 | uuid:f3c53255-be8a-4b04-817b-695bf2c54c8b 276 | 277 | 279 | 280 | image/svg+xml 281 | 282 | 283 | 285 | filesave.ai 286 | 287 | 288 | 289 | 290 | 291 | end='w' 292 | 293 | 295 | 299 | 303 | 307 | 311 | 315 | 323 | 327 | 331 | 335 | 339 | 343 | 344 | 348 | 352 | 359 | 363 | 367 | 371 | 375 | 379 | 380 | 384 | 391 | 395 | 399 | 403 | 407 | 411 | 412 | 416 | 420 | 428 | 432 | 436 | 440 | 444 | 448 | 449 | 453 | 457 | 464 | 468 | 472 | 476 | 480 | 484 | 488 | 492 | 496 | 497 | 501 | 509 | 513 | 517 | 521 | 525 | 529 | 533 | 537 | 541 | 542 | 549 | 556 | 560 | 564 | 568 | 572 | 576 | 577 | 581 | 589 | 593 | 597 | 601 | 605 | 609 | 613 | 617 | 621 | 625 | 629 | 633 | 634 | 638 | 646 | 650 | 654 | 658 | 662 | 666 | 667 | 674 | 678 | 679 | 680 | --------------------------------------------------------------------------------