commit
43b730b6e6
303
.travis.yml
303
.travis.yml
@ -1,14 +1,293 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
# vim: set ts=2 et:
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- pyqt4-dev-tools
|
||||
# run xvfb with 32-bit color
|
||||
# xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here
|
||||
|
||||
# command to install dependencies
|
||||
#install: "pip install --user -r requirements.txt"
|
||||
# command to run tests
|
||||
script: make all
|
||||
matrix:
|
||||
include:
|
||||
|
||||
# Python 2.7 + QT4
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "2.7"
|
||||
env:
|
||||
- QT=4
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- python-qt4
|
||||
- pyqt4-dev-tools
|
||||
- xvfb
|
||||
before_install:
|
||||
- sudo pip install lxml
|
||||
- make qt4py2
|
||||
- xvfb-run make testpy2
|
||||
|
||||
# Python 2.7 + QT4
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "2.7"
|
||||
env:
|
||||
- QT=4
|
||||
- CONDA=4.2.0
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
#- python-qt4
|
||||
#- pyqt4-dev-tools
|
||||
- xvfb
|
||||
before_install:
|
||||
# ref: https://www.continuum.io/downloads
|
||||
- curl -O https://repo.continuum.io/archive/Anaconda2-4.2.0-Linux-x86_64.sh
|
||||
# ref: http://conda.pydata.org/docs/help/silent.html
|
||||
- /bin/bash Anaconda2-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda2
|
||||
- export PATH="$HOME/anaconda2/bin:$PATH"
|
||||
# ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda
|
||||
- conda create -y -n labelImg-py2qt4 python=2.7
|
||||
- source activate labelImg-py2qt4
|
||||
- conda install -y pyqt=4
|
||||
- conda install -y lxml
|
||||
- make qt4py2
|
||||
- xvfb-run make testpy2
|
||||
|
||||
# Python 2 + QT5
|
||||
# disabled; can't get it to work
|
||||
#- os: linux
|
||||
# dist: trusty
|
||||
# sudo: required
|
||||
# language: generic
|
||||
# python: "2.7"
|
||||
# env:
|
||||
# - QT=5
|
||||
# addons:
|
||||
# apt:
|
||||
# packages:
|
||||
# - cmake
|
||||
# - pyqt5-dev-tools
|
||||
# - xvfb
|
||||
# before_install:
|
||||
# - sudo apt-get update
|
||||
# - sudo apt-get install -y python-pip
|
||||
# - sudo pip install lxml
|
||||
# - pyrcc5 --help || true # does QT5 support python2 out of the box?
|
||||
# - make qt5py3
|
||||
# - xvfb-run make testpy2
|
||||
|
||||
# Python 3 + QT4
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.5"
|
||||
env:
|
||||
- QT=4
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- python3-pyqt4
|
||||
- pyqt4-dev-tools
|
||||
- xvfb
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y python3-pip
|
||||
- sudo pip3 install lxml
|
||||
- make qt4py3
|
||||
- xvfb-run make testpy3
|
||||
|
||||
# Python 3 + QT5
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.5"
|
||||
env:
|
||||
- QT=5
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- pyqt5-dev-tools
|
||||
- xvfb
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y python3-pip
|
||||
- sudo pip3 install lxml
|
||||
- make qt5py3
|
||||
- xvfb-run make testpy3
|
||||
|
||||
# Python 3 + QT5
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.5"
|
||||
env:
|
||||
- QT=5
|
||||
- CONDA=4.2.0
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- cmake
|
||||
- xvfb
|
||||
before_install:
|
||||
# ref: https://www.continuum.io/downloads
|
||||
- curl -O https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh
|
||||
# ref: http://conda.pydata.org/docs/help/silent.html
|
||||
- /bin/bash Anaconda3-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda3
|
||||
- export PATH="$HOME/anaconda3/bin:$PATH"
|
||||
# ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda
|
||||
- conda create -y -n labelImg-py3qt5 python=3.5
|
||||
- source activate labelImg-py3qt5
|
||||
- conda install -y pyqt=5
|
||||
- conda install -y lxml
|
||||
- make qt5py3
|
||||
- xvfb-run make testpy3
|
||||
|
||||
# OS X 10.10 Python 3 + QT5
|
||||
- os: osx
|
||||
osx_image: xcode6.4 # Xcode 6.4, OS X 10.10
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.6"
|
||||
env:
|
||||
- QT=5
|
||||
before_install:
|
||||
#- brew update
|
||||
- brew install libxml2
|
||||
- brew install pyqt5
|
||||
- which python3 pip3
|
||||
- python3 --version
|
||||
#- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
|
||||
- sudo -H easy_install-3.6 lxml || true
|
||||
- python3 -c 'import sys; print(sys.path)'
|
||||
- python3 -c 'import lxml'
|
||||
- make qt5py3
|
||||
- python3 -c 'help("modules")'
|
||||
- make testpy3 # FIXME: does not work, segfault on travis-ci
|
||||
|
||||
# OS X 10.11 Python 3 + QT5
|
||||
- os: osx
|
||||
osx_image: xcode8 # Xcode 8, OS X 10.11
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.6"
|
||||
env:
|
||||
- QT=5
|
||||
before_install:
|
||||
#- brew update
|
||||
- brew install libxml2
|
||||
- brew install pyqt5
|
||||
- which python3 pip3
|
||||
- python3 --version
|
||||
#- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
|
||||
- sudo -H easy_install-3.6 lxml || true
|
||||
- python3 -c 'import sys; print(sys.path)'
|
||||
- python3 -c 'import lxml'
|
||||
- make qt5py3
|
||||
- python3 -c 'help("modules")'
|
||||
- make testpy3 # FIXME: does not work, segfault on travis-ci
|
||||
|
||||
# OS X 10.12 Python 3 + QT5
|
||||
- os: osx
|
||||
osx_image: xcode8.2 # OS X 10.12
|
||||
sudo: required
|
||||
language: generic
|
||||
python: "3.6"
|
||||
env:
|
||||
- QT=5
|
||||
before_install:
|
||||
#- brew update
|
||||
- brew install libxml2
|
||||
- brew install pyqt5
|
||||
- which python3 pip3
|
||||
- python3 --version
|
||||
#- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
|
||||
- sudo -H easy_install-3.6 lxml || true
|
||||
- python3 -c 'import sys; print(sys.path)'
|
||||
- python3 -c 'import lxml'
|
||||
- make qt5py3
|
||||
- python3 -c 'help("modules")'
|
||||
#- make testpy3 # FIXME: does not work, segfault on travis-ci
|
||||
# just make sure the app runs... :-/
|
||||
- ( python3 labelImg.py ) & sleep 10; kill $!
|
||||
|
||||
# XXX: building QT4 from source takes forever...
|
||||
|
||||
# OS X 10.11 Python 2 + QT4
|
||||
#- os: osx
|
||||
# osx_image: xcode7.3 # OS X 10.11
|
||||
# sudo: required
|
||||
# language: generic
|
||||
# python: "2.7"
|
||||
# env:
|
||||
# - QT=4
|
||||
# before_install:
|
||||
# - brew install libxml2
|
||||
# # build PyQT4...
|
||||
# - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz
|
||||
# - tar zxvf sip-4.19.tar.gz
|
||||
# - (cd sip-4.19 && python configure.py -d /Library/Python/2.7/site-packages --arch x86_64 && make && sudo make install)
|
||||
# # NOTE: produces insane amounts of output...
|
||||
# - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /'
|
||||
# - brew linkapps qt
|
||||
# - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz
|
||||
# - tar zxvf PyQt4_gpl_mac-4.12.tar.gz
|
||||
# - cd PyQt4_gpl_mac-4.12
|
||||
# - python configure.py --help
|
||||
# - python configure.py -d /Library/Python/2.7/site-packages --use-arch=x86_64 --confirm-license
|
||||
# - make
|
||||
# - sudo make install
|
||||
# - cd -
|
||||
# - which python pip
|
||||
# - python --version
|
||||
# - sudo -H easy_install-2.7 lxml || true
|
||||
# - python -c 'import sys; print(sys.path)'
|
||||
# - python -c 'import lxml'
|
||||
# - make qt4py2
|
||||
# - python -c 'help("modules")'
|
||||
# - make testpy2
|
||||
|
||||
# OS X 10.11 Python 3 + QT4
|
||||
#- os: osx
|
||||
# osx_image: xcode7.3 # OS X 10.11
|
||||
# sudo: required
|
||||
# language: generic
|
||||
# python: "3.6"
|
||||
# env:
|
||||
# - QT=4
|
||||
# before_install:
|
||||
# - brew install libxml2
|
||||
# - brew install python3
|
||||
# - which python pip python3 pip3 || true
|
||||
# - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz
|
||||
# - tar zxvf sip-4.19.tar.gz
|
||||
# - ( cd sip-4.19 && python3 configure.py -d /Library/Python/3.6/site-packages --arch x86_64 && make && sudo make install )
|
||||
# # NOTE: produces insane amounts of output...
|
||||
# - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /'
|
||||
# - brew linkapps qt
|
||||
# - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz
|
||||
# - tar zxvf PyQt4_gpl_mac-4.12.tar.gz
|
||||
# - cd PyQt4_gpl_mac-4.12
|
||||
# - python3 configure.py --help
|
||||
# - python3 configure.py -d /Library/Python/3.6/site-packages --use-arch=x86_64 --confirm-license
|
||||
# - make
|
||||
# - sudo make install
|
||||
# - cd -
|
||||
# - which python3 pip3
|
||||
# - python3 --version
|
||||
# - sudo -H easy_install-3.6 lxml || true
|
||||
# - python3 -c 'import sys; print(sys.path)'
|
||||
# - python3 -c 'import lxml'
|
||||
# - make qt4py3
|
||||
# - python3 -c 'help("modules")'
|
||||
# - make testpy3
|
||||
|
||||
script:
|
||||
- exit 0
|
||||
|
||||
26
Makefile
26
Makefile
@ -1,6 +1,26 @@
|
||||
# ex: set ts=8 noet:
|
||||
|
||||
all: resources.py
|
||||
all: qt4
|
||||
|
||||
%.py: %.qrc
|
||||
pyrcc4 -o $@ $<
|
||||
test: testpy2
|
||||
|
||||
testpy2:
|
||||
python -m unittest discover tests
|
||||
|
||||
testpy3:
|
||||
python3 -m unittest discover tests
|
||||
|
||||
qt4: qt4py2
|
||||
|
||||
qt5: qt4py3
|
||||
|
||||
qt4py2:
|
||||
pyrcc4 -py2 -o resources.py resources.qrc
|
||||
|
||||
qt4py3:
|
||||
pyrcc4 -py3 -o resources.py resources.qrc
|
||||
|
||||
qt5py3:
|
||||
pyrcc5 -o resources.py resources.qrc
|
||||
|
||||
.PHONY: test
|
||||
|
||||
198
labelImg.py
198
labelImg.py
@ -10,8 +10,19 @@ import subprocess
|
||||
from functools import partial
|
||||
from collections import defaultdict
|
||||
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
# needed for py3+qt4
|
||||
# ref: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
|
||||
# ref: http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string
|
||||
if sys.version_info.major >= 3:
|
||||
import sip
|
||||
sip.setapi('QVariant', 2)
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
import resources
|
||||
|
||||
@ -29,6 +40,24 @@ __appname__ = 'labelImg'
|
||||
|
||||
### Utility functions and classes.
|
||||
|
||||
|
||||
def u(x):
|
||||
'''py2/py3 unicode helper'''
|
||||
try:
|
||||
return x.decode('utf8') # py2
|
||||
except AttributeError:
|
||||
return x # py3
|
||||
|
||||
|
||||
def have_qstring():
|
||||
'''p3/qt5 get rid of QString wrapper as py3 has native unicode str type'''
|
||||
return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.'))
|
||||
|
||||
|
||||
def util_qt_strlistclass():
|
||||
return QStringList if have_qstring() else list
|
||||
|
||||
|
||||
class WindowMixin(object):
|
||||
def menu(self, title, actions=None):
|
||||
menu = self.menuBar().addMenu(title)
|
||||
@ -47,8 +76,16 @@ class WindowMixin(object):
|
||||
return toolbar
|
||||
|
||||
|
||||
# PyQt5: TypeError: unhashable type: 'QListWidgetItem'
|
||||
class HashableQListWidgetItem(QListWidgetItem):
|
||||
def __init__(self, *args):
|
||||
super(HashableQListWidgetItem, self).__init__(*args)
|
||||
def __hash__(self):
|
||||
return hash(id(self))
|
||||
|
||||
|
||||
class MainWindow(QMainWindow, WindowMixin):
|
||||
FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = range(3)
|
||||
FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = list(range(3))
|
||||
|
||||
def __init__(self, filename=None):
|
||||
super(MainWindow, self).__init__()
|
||||
@ -323,39 +360,65 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
|
||||
# XXX: Could be completely declarative.
|
||||
# Restore application settings.
|
||||
types = {
|
||||
'filename': QString,
|
||||
'recentFiles': QStringList,
|
||||
'window/size': QSize,
|
||||
'window/position': QPoint,
|
||||
'window/geometry': QByteArray,
|
||||
# Docks and toolbars:
|
||||
'window/state': QByteArray,
|
||||
'savedir': QString,
|
||||
'lastOpenDir': QString,
|
||||
}
|
||||
|
||||
if have_qstring():
|
||||
types = {
|
||||
'filename': QString,
|
||||
'recentFiles': QStringList,
|
||||
'window/size': QSize,
|
||||
'window/position': QPoint,
|
||||
'window/geometry': QByteArray,
|
||||
'line/color': QColor,
|
||||
'fill/color': QColor,
|
||||
'advanced': bool,
|
||||
# Docks and toolbars:
|
||||
'window/state': QByteArray,
|
||||
'savedir': QString,
|
||||
'lastOpenDir': QString,
|
||||
}
|
||||
else:
|
||||
types = {
|
||||
'filename': str,
|
||||
'recentFiles': list,
|
||||
'window/size': QSize,
|
||||
'window/position': QPoint,
|
||||
'window/geometry': QByteArray,
|
||||
'line/color': QColor,
|
||||
'fill/color': QColor,
|
||||
'advanced': bool,
|
||||
# Docks and toolbars:
|
||||
'window/state': QByteArray,
|
||||
'savedir': str,
|
||||
'lastOpenDir': str,
|
||||
}
|
||||
|
||||
self.settings = settings = Settings(types)
|
||||
self.recentFiles = list(settings['recentFiles'])
|
||||
self.recentFiles = list(settings.get('recentFiles', []))
|
||||
size = settings.get('window/size', QSize(600, 500))
|
||||
position = settings.get('window/position', QPoint(0, 0))
|
||||
self.resize(size)
|
||||
self.move(position)
|
||||
saveDir = settings.get('savedir', None)
|
||||
self.lastOpenDir = settings.get('lastOpenDir', None)
|
||||
if os.path.exists(unicode(saveDir)):
|
||||
self.defaultSaveDir = unicode(saveDir)
|
||||
if os.path.exists(str(saveDir)):
|
||||
self.defaultSaveDir = str(saveDir)
|
||||
self.statusBar().showMessage('%s started. Annotation will be saved to %s' %(__appname__, self.defaultSaveDir))
|
||||
self.statusBar().show()
|
||||
|
||||
# or simply:
|
||||
#self.restoreGeometry(settings['window/geometry']
|
||||
self.restoreState(settings['window/state'])
|
||||
self.restoreState(settings.get('window/state', QByteArray()))
|
||||
self.lineColor = QColor(settings.get('line/color', Shape.line_color))
|
||||
self.fillColor = QColor(settings.get('fill/color', Shape.fill_color))
|
||||
Shape.line_color = self.lineColor
|
||||
Shape.fill_color = self.fillColor
|
||||
|
||||
if settings.get('advanced', QVariant()).toBool():
|
||||
def xbool(x):
|
||||
if isinstance(x, QVariant):
|
||||
return x.toBool()
|
||||
return bool(x)
|
||||
|
||||
if xbool(settings.get('advanced', False)):
|
||||
self.actions.advancedMode.setChecked(True)
|
||||
self.toggleAdvancedMode()
|
||||
|
||||
@ -472,7 +535,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
self.actions.editMode.setEnabled(not drawing)
|
||||
if not drawing and self.beginner():
|
||||
# Cancel creation.
|
||||
print 'Cancel creation.'
|
||||
print('Cancel creation.')
|
||||
self.canvas.setEditing(True)
|
||||
self.canvas.restoreCursor()
|
||||
self.actions.create.setEnabled(True)
|
||||
@ -493,7 +556,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
def updateFileMenu(self):
|
||||
current = self.filename
|
||||
def exists(filename):
|
||||
return os.path.exists(unicode(filename))
|
||||
return os.path.exists(filename)
|
||||
menu = self.menus.recentFiles
|
||||
menu.clear()
|
||||
files = [f for f in self.recentFiles if f != current and exists(f)]
|
||||
@ -518,7 +581,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
|
||||
# Tzutalin 20160906 : Add file list and dock to move faster
|
||||
def fileitemDoubleClicked(self, item=None):
|
||||
currIndex = self.mImgList.index(str(item.text()))
|
||||
currIndex = self.mImgList.index(item.text())
|
||||
if currIndex < len(self.mImgList):
|
||||
filename = self.mImgList[currIndex]
|
||||
if filename:
|
||||
@ -531,7 +594,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
else:
|
||||
shape = self.canvas.selectedShape
|
||||
if shape:
|
||||
self.labelList.setItemSelected(self.shapesToItems[shape], True)
|
||||
self.shapesToItems[shape].setSelected(True)
|
||||
else:
|
||||
self.labelList.clearSelection()
|
||||
self.actions.delete.setEnabled(selected)
|
||||
@ -541,7 +604,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
self.actions.shapeFillColor.setEnabled(selected)
|
||||
|
||||
def addLabel(self, shape):
|
||||
item = QListWidgetItem(shape.label)
|
||||
item = HashableQListWidgetItem(shape.label)
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked)
|
||||
self.itemsToShapes[item] = shape
|
||||
@ -574,7 +637,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
def saveLabels(self, filename):
|
||||
lf = LabelFile()
|
||||
def format_shape(s):
|
||||
return dict(label=unicode(s.label),
|
||||
return dict(label=s.label,
|
||||
line_color=s.line_color.getRgb()\
|
||||
if s.line_color != self.lineColor else None,
|
||||
fill_color=s.fill_color.getRgb()\
|
||||
@ -585,16 +648,16 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
# Can add differrent annotation formats here
|
||||
try:
|
||||
if self.usingPascalVocFormat is True:
|
||||
print 'savePascalVocFormat save to:' + filename
|
||||
lf.savePascalVocFormat(filename, shapes, unicode(self.filename), self.imageData,
|
||||
print('savePascalVocFormat save to:' + filename)
|
||||
lf.savePascalVocFormat(filename, shapes, str(self.filename), self.imageData,
|
||||
self.lineColor.getRgb(), self.fillColor.getRgb())
|
||||
else:
|
||||
lf.save(filename, shapes, unicode(self.filename), self.imageData,
|
||||
lf.save(filename, shapes, str(self.filename), self.imageData,
|
||||
self.lineColor.getRgb(), self.fillColor.getRgb())
|
||||
self.labelFile = lf
|
||||
self.filename = filename
|
||||
return True
|
||||
except LabelFileError, e:
|
||||
except LabelFileError as e:
|
||||
self.errorMessage(u'Error saving label data',
|
||||
u'<b>%s</b>' % e)
|
||||
return False
|
||||
@ -612,9 +675,9 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
|
||||
def labelItemChanged(self, item):
|
||||
shape = self.itemsToShapes[item]
|
||||
label = unicode(item.text())
|
||||
label = item.text()
|
||||
if label != shape.label:
|
||||
shape.label = unicode(item.text())
|
||||
shape.label = item.text()
|
||||
self.setDirty()
|
||||
else: # User probably changed item visibility
|
||||
self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
|
||||
@ -677,7 +740,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
self.adjustScale()
|
||||
|
||||
def togglePolygons(self, value):
|
||||
for item, shape in self.itemsToShapes.iteritems():
|
||||
for item, shape in self.itemsToShapes.items():
|
||||
item.setCheckState(Qt.Checked if value else Qt.Unchecked)
|
||||
|
||||
def loadFile(self, filename=None):
|
||||
@ -685,21 +748,20 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
self.resetState()
|
||||
self.canvas.setEnabled(False)
|
||||
if filename is None:
|
||||
filename = self.settings['filename']
|
||||
filename = unicode(filename)
|
||||
filename = self.settings.get('filename')
|
||||
|
||||
# Tzutalin 20160906 : Add file list and dock to move faster
|
||||
# Highlight the file item
|
||||
if filename and self.fileListWidget.count() > 0:
|
||||
index = self.mImgList.index(filename)
|
||||
fileWidgetItem = self.fileListWidget.item(index)
|
||||
self.fileListWidget.setItemSelected(fileWidgetItem, True)
|
||||
fileWidgetItem.setSelected(True)
|
||||
|
||||
if QFile.exists(filename):
|
||||
if filename and QFile.exists(filename):
|
||||
if LabelFile.isLabelFile(filename):
|
||||
try:
|
||||
self.labelFile = LabelFile(filename)
|
||||
except LabelFileError, e:
|
||||
except LabelFileError as e:
|
||||
self.errorMessage(u'Error opening file',
|
||||
(u"<p><b>%s</b></p>"
|
||||
u"<p>Make sure <i>%s</i> is a valid label file.")\
|
||||
@ -720,7 +782,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
u"<p>Make sure <i>%s</i> is a valid image file." % filename)
|
||||
self.status("Error reading %s" % filename)
|
||||
return False
|
||||
self.status("Loaded %s" % os.path.basename(unicode(filename)))
|
||||
self.status("Loaded %s" % os.path.basename(str(filename)))
|
||||
self.image = image
|
||||
self.filename = filename
|
||||
self.canvas.loadPixmap(QPixmap.fromImage(image))
|
||||
@ -782,7 +844,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
s = self.settings
|
||||
# If it loads images from dir, don't load it at the begining
|
||||
if self.dirname is None:
|
||||
s['filename'] = self.filename if self.filename else QString()
|
||||
s['filename'] = self.filename if self.filename else ''
|
||||
else:
|
||||
s['filename'] = ''
|
||||
|
||||
@ -820,17 +882,18 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
for file in files:
|
||||
if file.lower().endswith(tuple(extensions)):
|
||||
relatviePath = os.path.join(root, file)
|
||||
images.append(os.path.abspath(relatviePath))
|
||||
path = u(os.path.abspath(relatviePath))
|
||||
images.append(path)
|
||||
images.sort(key=lambda x: x.lower())
|
||||
return images
|
||||
|
||||
def changeSavedir(self, _value=False):
|
||||
if self.defaultSaveDir is not None:
|
||||
path = unicode(self.defaultSaveDir)
|
||||
path = str(self.defaultSaveDir)
|
||||
else:
|
||||
path = '.'
|
||||
|
||||
dirpath = unicode(QFileDialog.getExistingDirectory(self,
|
||||
dirpath = str(QFileDialog.getExistingDirectory(self,
|
||||
'%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly
|
||||
| QFileDialog.DontResolveSymlinks))
|
||||
|
||||
@ -844,14 +907,14 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
if self.filename is None:
|
||||
return
|
||||
|
||||
path = os.path.dirname(unicode(self.filename))\
|
||||
path = os.path.dirname(str(self.filename))\
|
||||
if self.filename else '.'
|
||||
if self.usingPascalVocFormat:
|
||||
formats = ['*.%s' % unicode(fmt).lower()\
|
||||
formats = ['*.%s' % str(fmt).lower()\
|
||||
for fmt in QImageReader.supportedImageFormats()]
|
||||
filters = "Open Annotation XML file (%s)" % \
|
||||
' '.join(formats + ['*.xml'])
|
||||
filename = unicode(QFileDialog.getOpenFileName(self,
|
||||
filename = str(QFileDialog.getOpenFileName(self,
|
||||
'%s - Choose a xml file' % __appname__, path, filters))
|
||||
self.loadPascalXMLByFilename(filename)
|
||||
|
||||
@ -859,13 +922,13 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
if not self.mayContinue():
|
||||
return
|
||||
|
||||
path = os.path.dirname(unicode(self.filename))\
|
||||
path = os.path.dirname(self.filename)\
|
||||
if self.filename else '.'
|
||||
|
||||
if self.lastOpenDir is not None and len(self.lastOpenDir) > 1:
|
||||
path = self.lastOpenDir
|
||||
|
||||
dirpath = unicode(QFileDialog.getExistingDirectory(self,
|
||||
dirpath = str(QFileDialog.getExistingDirectory(self,
|
||||
'%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly
|
||||
| QFileDialog.DontResolveSymlinks))
|
||||
|
||||
@ -921,13 +984,13 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
def openFile(self, _value=False):
|
||||
if not self.mayContinue():
|
||||
return
|
||||
path = os.path.dirname(unicode(self.filename))\
|
||||
path = os.path.dirname(str(self.filename))\
|
||||
if self.filename else '.'
|
||||
formats = ['*.%s' % unicode(fmt).lower()\
|
||||
formats = ['*.%s' % str(fmt).lower()\
|
||||
for fmt in QImageReader.supportedImageFormats()]
|
||||
filters = "Image & Label files (%s)" % \
|
||||
' '.join(formats + ['*%s' % LabelFile.suffix])
|
||||
filename = unicode(QFileDialog.getOpenFileName(self,
|
||||
filename = str(QFileDialog.getOpenFileName(self,
|
||||
'%s - Choose Image or Label file' % __appname__, path, filters))
|
||||
if filename:
|
||||
self.loadFile(filename)
|
||||
@ -936,7 +999,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
assert not self.image.isNull(), "cannot save empty image"
|
||||
if self.hasLabels():
|
||||
if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)):
|
||||
print 'handle the image:' + self.filename
|
||||
print('handle the image:' + self.filename)
|
||||
imgFileName = os.path.basename(self.filename)
|
||||
savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix
|
||||
savedPath = os.path.join(str(self.defaultSaveDir), savedFileName)
|
||||
@ -957,7 +1020,6 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
dlg = QFileDialog(self, caption, openDialogPath, filters)
|
||||
dlg.setDefaultSuffix(LabelFile.suffix[1:])
|
||||
dlg.setAcceptMode(QFileDialog.AcceptSave)
|
||||
dlg.setConfirmOverwrite(True)
|
||||
filenameWithoutExtension = os.path.splitext(self.filename)[0]
|
||||
dlg.selectFile(filenameWithoutExtension)
|
||||
dlg.setOption(QFileDialog.DontUseNativeDialog, False)
|
||||
@ -1002,7 +1064,7 @@ class MainWindow(QMainWindow, WindowMixin):
|
||||
'<p><b>%s</b></p>%s' % (title, message))
|
||||
|
||||
def currentPath(self):
|
||||
return os.path.dirname(unicode(self.filename)) if self.filename else '.'
|
||||
return os.path.dirname(str(self.filename)) if self.filename else '.'
|
||||
|
||||
def chooseColor1(self):
|
||||
color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
|
||||
@ -1098,10 +1160,20 @@ class Settings(object):
|
||||
|
||||
def _cast(self, key, value):
|
||||
# XXX: Very nasty way of converting types to QVariant methods :P
|
||||
t = self.types[key]
|
||||
if t != QVariant:
|
||||
method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1))
|
||||
return method(value)
|
||||
print('_cast', key, repr(value))
|
||||
t = self.types.get(key)
|
||||
print('t', t)
|
||||
if t is not None and t != QVariant:
|
||||
if t is str:
|
||||
return str(value)
|
||||
else:
|
||||
# XXX: awful
|
||||
try:
|
||||
method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1))
|
||||
return method(value)
|
||||
except AttributeError as e:
|
||||
print(e)
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
@ -1115,13 +1187,21 @@ def read(filename, default=None):
|
||||
except:
|
||||
return default
|
||||
|
||||
def main(argv):
|
||||
"""Standard boilerplate Qt application code."""
|
||||
def get_main_app(argv=[]):
|
||||
"""
|
||||
Standard boilerplate Qt application code.
|
||||
Do everything but app.exec_() -- so that we can test the application in one thread
|
||||
"""
|
||||
app = QApplication(argv)
|
||||
app.setApplicationName(__appname__)
|
||||
app.setWindowIcon(newIcon("app"))
|
||||
win = MainWindow(argv[1] if len(argv) == 2 else None)
|
||||
win.show()
|
||||
return app, win
|
||||
|
||||
def main(argv):
|
||||
'''construct main app and run it'''
|
||||
app, _win = get_main_app(argv)
|
||||
return app.exec_()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
#from PyQt4.QtOpenGL import *
|
||||
|
||||
from shape import Shape
|
||||
@ -20,7 +27,7 @@ class Canvas(QWidget):
|
||||
shapeMoved = pyqtSignal()
|
||||
drawingPolygon = pyqtSignal(bool)
|
||||
|
||||
CREATE, EDIT = range(2)
|
||||
CREATE, EDIT = list(range(2))
|
||||
|
||||
epsilon = 11.0
|
||||
|
||||
@ -85,7 +92,7 @@ class Canvas(QWidget):
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
"""Update line with last point and current coordinates."""
|
||||
pos = self.transformPos(ev.posF())
|
||||
pos = self.transformPos(ev.pos())
|
||||
|
||||
self.restoreCursor()
|
||||
|
||||
@ -169,7 +176,8 @@ class Canvas(QWidget):
|
||||
self.hVertex, self.hShape = None, None
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
pos = self.transformPos(ev.posF())
|
||||
pos = self.transformPos(ev.pos())
|
||||
|
||||
if ev.button() == Qt.LeftButton:
|
||||
if self.drawing():
|
||||
if self.current and self.current.reachMaxPoints() is False:
|
||||
@ -456,12 +464,14 @@ class Canvas(QWidget):
|
||||
return QPointF(min(max(0, x2), max(x3, x4)), y3)
|
||||
return QPointF(x, y)
|
||||
|
||||
def intersectingEdges(self, (x1, y1), (x2, y2), points):
|
||||
def intersectingEdges(self, x1y1, x2y2, points):
|
||||
"""For each edge formed by `points', yield the intersection
|
||||
with the line segment `(x1,y1) - (x2,y2)`, if it exists.
|
||||
Also return the distance of `(x2,y2)' to the middle of the
|
||||
edge along with its index, so that the one closest can be chosen."""
|
||||
for i in xrange(4):
|
||||
x1, y1 = x1y1
|
||||
x2, y2 = x2y2
|
||||
for i in range(4):
|
||||
x3, y3 = points[i]
|
||||
x4, y4 = points[(i+1) % 4]
|
||||
denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
@ -506,7 +516,7 @@ class Canvas(QWidget):
|
||||
def keyPressEvent(self, ev):
|
||||
key = ev.key()
|
||||
if key == Qt.Key_Escape and self.current:
|
||||
print 'ESC press'
|
||||
print('ESC press')
|
||||
self.current = None
|
||||
self.drawingPolygon.emit(False)
|
||||
self.update()
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
BB = QDialogButtonBox
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
from lib import newIcon, labelValidator
|
||||
|
||||
@ -32,11 +37,20 @@ class LabelDialog(QDialog):
|
||||
self.setLayout(layout)
|
||||
|
||||
def validate(self):
|
||||
if self.edit.text().trimmed():
|
||||
self.accept()
|
||||
try:
|
||||
if self.edit.text().trimmed():
|
||||
self.accept()
|
||||
except AttributeError:
|
||||
# PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
|
||||
if self.edit.text().strip():
|
||||
self.accept()
|
||||
|
||||
def postProcess(self):
|
||||
self.edit.setText(self.edit.text().trimmed())
|
||||
try:
|
||||
self.edit.setText(self.edit.text().trimmed())
|
||||
except AttributeError:
|
||||
# PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
|
||||
self.edit.setText(self.edit.text())
|
||||
|
||||
def popUp(self, text='', move=True):
|
||||
self.edit.setText(text)
|
||||
@ -47,6 +61,10 @@ class LabelDialog(QDialog):
|
||||
return self.edit.text() if self.exec_() else None
|
||||
|
||||
def listItemClick(self, tQListWidgetItem):
|
||||
text = tQListWidgetItem.text().trimmed()
|
||||
try:
|
||||
text = tQListWidgetItem.text().trimmed()
|
||||
except AttributeError:
|
||||
# PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
|
||||
text = tQListWidgetItem.text().strip()
|
||||
self.edit.setText(text)
|
||||
self.validate()
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
# Copyright (c) 2016 Tzutalin
|
||||
# Create by TzuTaLin <tzu.ta.lin@gmail.com>
|
||||
|
||||
from PyQt4.QtGui import QImage
|
||||
try:
|
||||
from PyQt5.QtGui import QImage
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import QImage
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
from pascal_voc_io import PascalVocWriter
|
||||
import os.path
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
from math import sqrt
|
||||
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
|
||||
def newIcon(icon):
|
||||
|
||||
@ -74,7 +74,11 @@ class PascalVocWriter:
|
||||
for each_object in self.boxlist:
|
||||
object_item = SubElement(top, 'object')
|
||||
name = SubElement(object_item, 'name')
|
||||
name.text = unicode(each_object['name'])
|
||||
try:
|
||||
name.text = unicode(each_object['name'])
|
||||
except NameError:
|
||||
# Py3: NameError: name 'unicode' is not defined
|
||||
name.text = each_object['name']
|
||||
pose = SubElement(object_item, 'pose')
|
||||
pose.text = "Unspecified"
|
||||
truncated = SubElement(object_item, 'truncated')
|
||||
@ -101,7 +105,7 @@ class PascalVocWriter:
|
||||
out_file = open(targetFile, 'w')
|
||||
|
||||
prettifyResult = self.prettify(root)
|
||||
out_file.write(prettifyResult)
|
||||
out_file.write(prettifyResult.decode('utf8'))
|
||||
out_file.close()
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
from lib import distance
|
||||
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
|
||||
class ToolBar(QToolBar):
|
||||
def __init__(self, title):
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
try:
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
except ImportError:
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
class ZoomWidget(QSpinBox):
|
||||
def __init__(self, value=100):
|
||||
|
||||
21
tests/test.py
Normal file
21
tests/test.py
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from labelImg import get_main_app
|
||||
|
||||
|
||||
class TestMainWindow(TestCase):
|
||||
|
||||
app = None
|
||||
win = None
|
||||
|
||||
def setUp(self):
|
||||
self.app, self.win = get_main_app()
|
||||
|
||||
def tearDown(self):
|
||||
self.win.close()
|
||||
self.app.quit()
|
||||
|
||||
def test_noop(self):
|
||||
pass
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user