Merge pull request #45 from rflynn/py3qt5

Py3qt5
This commit is contained in:
darrenl 2017-01-09 14:18:29 +08:00 committed by GitHub
commit 43b730b6e6
13 changed files with 563 additions and 101 deletions

View File

@ -1,14 +1,293 @@
language: python # vim: set ts=2 et:
python:
- "2.7"
# run xvfb with 32-bit color
# xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here
matrix:
include:
# Python 2.7 + QT4
- os: linux
dist: trusty
sudo: required
language: generic
python: "2.7"
env:
- QT=4
addons: addons:
apt: apt:
packages: packages:
- cmake - cmake
- python-qt4
- pyqt4-dev-tools - pyqt4-dev-tools
- xvfb
before_install:
- sudo pip install lxml
- make qt4py2
- xvfb-run make testpy2
# command to install dependencies # Python 2.7 + QT4
#install: "pip install --user -r requirements.txt" - os: linux
# command to run tests dist: trusty
script: make all 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

View File

@ -1,6 +1,26 @@
# ex: set ts=8 noet:
all: resources.py all: qt4
%.py: %.qrc test: testpy2
pyrcc4 -o $@ $<
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

View File

@ -10,6 +10,17 @@ import subprocess
from functools import partial from functools import partial
from collections import defaultdict from collections import defaultdict
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.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
@ -29,6 +40,24 @@ __appname__ = 'labelImg'
### Utility functions and classes. ### 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): class WindowMixin(object):
def menu(self, title, actions=None): def menu(self, title, actions=None):
menu = self.menuBar().addMenu(title) menu = self.menuBar().addMenu(title)
@ -47,8 +76,16 @@ class WindowMixin(object):
return toolbar 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): 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): def __init__(self, filename=None):
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
@ -323,39 +360,65 @@ class MainWindow(QMainWindow, WindowMixin):
# XXX: Could be completely declarative. # XXX: Could be completely declarative.
# Restore application settings. # Restore application settings.
if have_qstring():
types = { types = {
'filename': QString, 'filename': QString,
'recentFiles': QStringList, 'recentFiles': QStringList,
'window/size': QSize, 'window/size': QSize,
'window/position': QPoint, 'window/position': QPoint,
'window/geometry': QByteArray, 'window/geometry': QByteArray,
'line/color': QColor,
'fill/color': QColor,
'advanced': bool,
# Docks and toolbars: # Docks and toolbars:
'window/state': QByteArray, 'window/state': QByteArray,
'savedir': QString, 'savedir': QString,
'lastOpenDir': 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.settings = settings = Settings(types)
self.recentFiles = list(settings['recentFiles']) self.recentFiles = list(settings.get('recentFiles', []))
size = settings.get('window/size', QSize(600, 500)) size = settings.get('window/size', QSize(600, 500))
position = settings.get('window/position', QPoint(0, 0)) position = settings.get('window/position', QPoint(0, 0))
self.resize(size) self.resize(size)
self.move(position) self.move(position)
saveDir = settings.get('savedir', None) saveDir = settings.get('savedir', None)
self.lastOpenDir = settings.get('lastOpenDir', None) self.lastOpenDir = settings.get('lastOpenDir', None)
if os.path.exists(unicode(saveDir)): if os.path.exists(str(saveDir)):
self.defaultSaveDir = unicode(saveDir) self.defaultSaveDir = str(saveDir)
self.statusBar().showMessage('%s started. Annotation will be saved to %s' %(__appname__, self.defaultSaveDir)) self.statusBar().showMessage('%s started. Annotation will be saved to %s' %(__appname__, self.defaultSaveDir))
self.statusBar().show() self.statusBar().show()
# or simply: # or simply:
#self.restoreGeometry(settings['window/geometry'] #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.lineColor = QColor(settings.get('line/color', Shape.line_color))
self.fillColor = QColor(settings.get('fill/color', Shape.fill_color)) self.fillColor = QColor(settings.get('fill/color', Shape.fill_color))
Shape.line_color = self.lineColor Shape.line_color = self.lineColor
Shape.fill_color = self.fillColor 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.actions.advancedMode.setChecked(True)
self.toggleAdvancedMode() self.toggleAdvancedMode()
@ -472,7 +535,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.actions.editMode.setEnabled(not drawing) self.actions.editMode.setEnabled(not drawing)
if not drawing and self.beginner(): if not drawing and self.beginner():
# Cancel creation. # Cancel creation.
print 'Cancel creation.' print('Cancel creation.')
self.canvas.setEditing(True) self.canvas.setEditing(True)
self.canvas.restoreCursor() self.canvas.restoreCursor()
self.actions.create.setEnabled(True) self.actions.create.setEnabled(True)
@ -493,7 +556,7 @@ class MainWindow(QMainWindow, WindowMixin):
def updateFileMenu(self): def updateFileMenu(self):
current = self.filename current = self.filename
def exists(filename): def exists(filename):
return os.path.exists(unicode(filename)) return os.path.exists(filename)
menu = self.menus.recentFiles menu = self.menus.recentFiles
menu.clear() menu.clear()
files = [f for f in self.recentFiles if f != current and exists(f)] 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 # Tzutalin 20160906 : Add file list and dock to move faster
def fileitemDoubleClicked(self, item=None): def fileitemDoubleClicked(self, item=None):
currIndex = self.mImgList.index(str(item.text())) currIndex = self.mImgList.index(item.text())
if currIndex < len(self.mImgList): if currIndex < len(self.mImgList):
filename = self.mImgList[currIndex] filename = self.mImgList[currIndex]
if filename: if filename:
@ -531,7 +594,7 @@ class MainWindow(QMainWindow, WindowMixin):
else: else:
shape = self.canvas.selectedShape shape = self.canvas.selectedShape
if shape: if shape:
self.labelList.setItemSelected(self.shapesToItems[shape], True) self.shapesToItems[shape].setSelected(True)
else: else:
self.labelList.clearSelection() self.labelList.clearSelection()
self.actions.delete.setEnabled(selected) self.actions.delete.setEnabled(selected)
@ -541,7 +604,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.actions.shapeFillColor.setEnabled(selected) self.actions.shapeFillColor.setEnabled(selected)
def addLabel(self, shape): def addLabel(self, shape):
item = QListWidgetItem(shape.label) item = HashableQListWidgetItem(shape.label)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked) item.setCheckState(Qt.Checked)
self.itemsToShapes[item] = shape self.itemsToShapes[item] = shape
@ -574,7 +637,7 @@ class MainWindow(QMainWindow, WindowMixin):
def saveLabels(self, filename): def saveLabels(self, filename):
lf = LabelFile() lf = LabelFile()
def format_shape(s): def format_shape(s):
return dict(label=unicode(s.label), return dict(label=s.label,
line_color=s.line_color.getRgb()\ line_color=s.line_color.getRgb()\
if s.line_color != self.lineColor else None, if s.line_color != self.lineColor else None,
fill_color=s.fill_color.getRgb()\ fill_color=s.fill_color.getRgb()\
@ -585,16 +648,16 @@ class MainWindow(QMainWindow, WindowMixin):
# Can add differrent annotation formats here # Can add differrent annotation formats here
try: try:
if self.usingPascalVocFormat is True: if self.usingPascalVocFormat is True:
print 'savePascalVocFormat save to:' + filename print('savePascalVocFormat save to:' + filename)
lf.savePascalVocFormat(filename, shapes, unicode(self.filename), self.imageData, lf.savePascalVocFormat(filename, shapes, str(self.filename), self.imageData,
self.lineColor.getRgb(), self.fillColor.getRgb()) self.lineColor.getRgb(), self.fillColor.getRgb())
else: 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.lineColor.getRgb(), self.fillColor.getRgb())
self.labelFile = lf self.labelFile = lf
self.filename = filename self.filename = filename
return True return True
except LabelFileError, e: except LabelFileError as e:
self.errorMessage(u'Error saving label data', self.errorMessage(u'Error saving label data',
u'<b>%s</b>' % e) u'<b>%s</b>' % e)
return False return False
@ -612,9 +675,9 @@ class MainWindow(QMainWindow, WindowMixin):
def labelItemChanged(self, item): def labelItemChanged(self, item):
shape = self.itemsToShapes[item] shape = self.itemsToShapes[item]
label = unicode(item.text()) label = item.text()
if label != shape.label: if label != shape.label:
shape.label = unicode(item.text()) shape.label = item.text()
self.setDirty() self.setDirty()
else: # User probably changed item visibility else: # User probably changed item visibility
self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
@ -677,7 +740,7 @@ class MainWindow(QMainWindow, WindowMixin):
self.adjustScale() self.adjustScale()
def togglePolygons(self, value): 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) item.setCheckState(Qt.Checked if value else Qt.Unchecked)
def loadFile(self, filename=None): def loadFile(self, filename=None):
@ -685,21 +748,20 @@ class MainWindow(QMainWindow, WindowMixin):
self.resetState() self.resetState()
self.canvas.setEnabled(False) self.canvas.setEnabled(False)
if filename is None: if filename is None:
filename = self.settings['filename'] filename = self.settings.get('filename')
filename = unicode(filename)
# Tzutalin 20160906 : Add file list and dock to move faster # Tzutalin 20160906 : Add file list and dock to move faster
# Highlight the file item # Highlight the file item
if filename and self.fileListWidget.count() > 0: if filename and self.fileListWidget.count() > 0:
index = self.mImgList.index(filename) index = self.mImgList.index(filename)
fileWidgetItem = self.fileListWidget.item(index) 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): if LabelFile.isLabelFile(filename):
try: try:
self.labelFile = LabelFile(filename) self.labelFile = LabelFile(filename)
except LabelFileError, e: except LabelFileError as e:
self.errorMessage(u'Error opening file', self.errorMessage(u'Error opening file',
(u"<p><b>%s</b></p>" (u"<p><b>%s</b></p>"
u"<p>Make sure <i>%s</i> is a valid label file.")\ 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) u"<p>Make sure <i>%s</i> is a valid image file." % filename)
self.status("Error reading %s" % filename) self.status("Error reading %s" % filename)
return False return False
self.status("Loaded %s" % os.path.basename(unicode(filename))) self.status("Loaded %s" % os.path.basename(str(filename)))
self.image = image self.image = image
self.filename = filename self.filename = filename
self.canvas.loadPixmap(QPixmap.fromImage(image)) self.canvas.loadPixmap(QPixmap.fromImage(image))
@ -782,7 +844,7 @@ class MainWindow(QMainWindow, WindowMixin):
s = self.settings s = self.settings
# If it loads images from dir, don't load it at the begining # If it loads images from dir, don't load it at the begining
if self.dirname is None: if self.dirname is None:
s['filename'] = self.filename if self.filename else QString() s['filename'] = self.filename if self.filename else ''
else: else:
s['filename'] = '' s['filename'] = ''
@ -820,17 +882,18 @@ class MainWindow(QMainWindow, WindowMixin):
for file in files: for file in files:
if file.lower().endswith(tuple(extensions)): if file.lower().endswith(tuple(extensions)):
relatviePath = os.path.join(root, file) 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()) images.sort(key=lambda x: x.lower())
return images return images
def changeSavedir(self, _value=False): def changeSavedir(self, _value=False):
if self.defaultSaveDir is not None: if self.defaultSaveDir is not None:
path = unicode(self.defaultSaveDir) path = str(self.defaultSaveDir)
else: else:
path = '.' path = '.'
dirpath = unicode(QFileDialog.getExistingDirectory(self, dirpath = str(QFileDialog.getExistingDirectory(self,
'%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly '%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly
| QFileDialog.DontResolveSymlinks)) | QFileDialog.DontResolveSymlinks))
@ -844,14 +907,14 @@ class MainWindow(QMainWindow, WindowMixin):
if self.filename is None: if self.filename is None:
return return
path = os.path.dirname(unicode(self.filename))\ path = os.path.dirname(str(self.filename))\
if self.filename else '.' if self.filename else '.'
if self.usingPascalVocFormat: if self.usingPascalVocFormat:
formats = ['*.%s' % unicode(fmt).lower()\ formats = ['*.%s' % str(fmt).lower()\
for fmt in QImageReader.supportedImageFormats()] for fmt in QImageReader.supportedImageFormats()]
filters = "Open Annotation XML file (%s)" % \ filters = "Open Annotation XML file (%s)" % \
' '.join(formats + ['*.xml']) ' '.join(formats + ['*.xml'])
filename = unicode(QFileDialog.getOpenFileName(self, filename = str(QFileDialog.getOpenFileName(self,
'%s - Choose a xml file' % __appname__, path, filters)) '%s - Choose a xml file' % __appname__, path, filters))
self.loadPascalXMLByFilename(filename) self.loadPascalXMLByFilename(filename)
@ -859,13 +922,13 @@ class MainWindow(QMainWindow, WindowMixin):
if not self.mayContinue(): if not self.mayContinue():
return return
path = os.path.dirname(unicode(self.filename))\ path = os.path.dirname(self.filename)\
if self.filename else '.' if self.filename else '.'
if self.lastOpenDir is not None and len(self.lastOpenDir) > 1: if self.lastOpenDir is not None and len(self.lastOpenDir) > 1:
path = self.lastOpenDir path = self.lastOpenDir
dirpath = unicode(QFileDialog.getExistingDirectory(self, dirpath = str(QFileDialog.getExistingDirectory(self,
'%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly '%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly
| QFileDialog.DontResolveSymlinks)) | QFileDialog.DontResolveSymlinks))
@ -921,13 +984,13 @@ class MainWindow(QMainWindow, WindowMixin):
def openFile(self, _value=False): def openFile(self, _value=False):
if not self.mayContinue(): if not self.mayContinue():
return return
path = os.path.dirname(unicode(self.filename))\ path = os.path.dirname(str(self.filename))\
if self.filename else '.' if self.filename else '.'
formats = ['*.%s' % unicode(fmt).lower()\ formats = ['*.%s' % str(fmt).lower()\
for fmt in QImageReader.supportedImageFormats()] for fmt in QImageReader.supportedImageFormats()]
filters = "Image & Label files (%s)" % \ filters = "Image & Label files (%s)" % \
' '.join(formats + ['*%s' % LabelFile.suffix]) ' '.join(formats + ['*%s' % LabelFile.suffix])
filename = unicode(QFileDialog.getOpenFileName(self, filename = str(QFileDialog.getOpenFileName(self,
'%s - Choose Image or Label file' % __appname__, path, filters)) '%s - Choose Image or Label file' % __appname__, path, filters))
if filename: if filename:
self.loadFile(filename) self.loadFile(filename)
@ -936,7 +999,7 @@ class MainWindow(QMainWindow, WindowMixin):
assert not self.image.isNull(), "cannot save empty image" assert not self.image.isNull(), "cannot save empty image"
if self.hasLabels(): if self.hasLabels():
if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): 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) imgFileName = os.path.basename(self.filename)
savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix
savedPath = os.path.join(str(self.defaultSaveDir), savedFileName) savedPath = os.path.join(str(self.defaultSaveDir), savedFileName)
@ -957,7 +1020,6 @@ class MainWindow(QMainWindow, WindowMixin):
dlg = QFileDialog(self, caption, openDialogPath, filters) dlg = QFileDialog(self, caption, openDialogPath, filters)
dlg.setDefaultSuffix(LabelFile.suffix[1:]) dlg.setDefaultSuffix(LabelFile.suffix[1:])
dlg.setAcceptMode(QFileDialog.AcceptSave) dlg.setAcceptMode(QFileDialog.AcceptSave)
dlg.setConfirmOverwrite(True)
filenameWithoutExtension = os.path.splitext(self.filename)[0] filenameWithoutExtension = os.path.splitext(self.filename)[0]
dlg.selectFile(filenameWithoutExtension) dlg.selectFile(filenameWithoutExtension)
dlg.setOption(QFileDialog.DontUseNativeDialog, False) dlg.setOption(QFileDialog.DontUseNativeDialog, False)
@ -1002,7 +1064,7 @@ class MainWindow(QMainWindow, WindowMixin):
'<p><b>%s</b></p>%s' % (title, message)) '<p><b>%s</b></p>%s' % (title, message))
def currentPath(self): 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): def chooseColor1(self):
color = self.colorDialog.getColor(self.lineColor, u'Choose line color', color = self.colorDialog.getColor(self.lineColor, u'Choose line color',
@ -1098,10 +1160,20 @@ class Settings(object):
def _cast(self, key, value): def _cast(self, key, value):
# XXX: Very nasty way of converting types to QVariant methods :P # XXX: Very nasty way of converting types to QVariant methods :P
t = self.types[key] print('_cast', key, repr(value))
if t != QVariant: 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)) method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1))
return method(value) return method(value)
except AttributeError as e:
print(e)
return value
return value return value
@ -1115,13 +1187,21 @@ def read(filename, default=None):
except: except:
return default return default
def main(argv): def get_main_app(argv=[]):
"""Standard boilerplate Qt application code.""" """
Standard boilerplate Qt application code.
Do everything but app.exec_() -- so that we can test the application in one thread
"""
app = QApplication(argv) app = QApplication(argv)
app.setApplicationName(__appname__) app.setApplicationName(__appname__)
app.setWindowIcon(newIcon("app")) app.setWindowIcon(newIcon("app"))
win = MainWindow(argv[1] if len(argv) == 2 else None) win = MainWindow(argv[1] if len(argv) == 2 else None)
win.show() win.show()
return app, win
def main(argv):
'''construct main app and run it'''
app, _win = get_main_app(argv)
return app.exec_() return app.exec_()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,5 +1,12 @@
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
#from PyQt4.QtOpenGL import * #from PyQt4.QtOpenGL import *
from shape import Shape from shape import Shape
@ -20,7 +27,7 @@ class Canvas(QWidget):
shapeMoved = pyqtSignal() shapeMoved = pyqtSignal()
drawingPolygon = pyqtSignal(bool) drawingPolygon = pyqtSignal(bool)
CREATE, EDIT = range(2) CREATE, EDIT = list(range(2))
epsilon = 11.0 epsilon = 11.0
@ -85,7 +92,7 @@ class Canvas(QWidget):
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
"""Update line with last point and current coordinates.""" """Update line with last point and current coordinates."""
pos = self.transformPos(ev.posF()) pos = self.transformPos(ev.pos())
self.restoreCursor() self.restoreCursor()
@ -169,7 +176,8 @@ class Canvas(QWidget):
self.hVertex, self.hShape = None, None self.hVertex, self.hShape = None, None
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
pos = self.transformPos(ev.posF()) pos = self.transformPos(ev.pos())
if ev.button() == Qt.LeftButton: if ev.button() == Qt.LeftButton:
if self.drawing(): if self.drawing():
if self.current and self.current.reachMaxPoints() is False: 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(min(max(0, x2), max(x3, x4)), y3)
return QPointF(x, y) 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 """For each edge formed by `points', yield the intersection
with the line segment `(x1,y1) - (x2,y2)`, if it exists. with the line segment `(x1,y1) - (x2,y2)`, if it exists.
Also return the distance of `(x2,y2)' to the middle of the 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.""" 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] x3, y3 = points[i]
x4, y4 = points[(i+1) % 4] x4, y4 = points[(i+1) % 4]
denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
@ -506,7 +516,7 @@ class Canvas(QWidget):
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
key = ev.key() key = ev.key()
if key == Qt.Key_Escape and self.current: if key == Qt.Key_Escape and self.current:
print 'ESC press' print('ESC press')
self.current = None self.current = None
self.drawingPolygon.emit(False) self.drawingPolygon.emit(False)
self.update() self.update()

View File

@ -1,3 +1,8 @@
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *

View File

@ -1,3 +1,8 @@
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
@ -32,11 +37,20 @@ class LabelDialog(QDialog):
self.setLayout(layout) self.setLayout(layout)
def validate(self): def validate(self):
try:
if self.edit.text().trimmed(): if self.edit.text().trimmed():
self.accept() self.accept()
except AttributeError:
# PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
if self.edit.text().strip():
self.accept()
def postProcess(self): def postProcess(self):
try:
self.edit.setText(self.edit.text().trimmed()) 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): def popUp(self, text='', move=True):
self.edit.setText(text) self.edit.setText(text)
@ -47,6 +61,10 @@ class LabelDialog(QDialog):
return self.edit.text() if self.exec_() else None return self.edit.text() if self.exec_() else None
def listItemClick(self, tQListWidgetItem): def listItemClick(self, tQListWidgetItem):
try:
text = tQListWidgetItem.text().trimmed() text = tQListWidgetItem.text().trimmed()
except AttributeError:
# PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
text = tQListWidgetItem.text().strip()
self.edit.setText(text) self.edit.setText(text)
self.validate() self.validate()

View File

@ -1,7 +1,11 @@
# Copyright (c) 2016 Tzutalin # Copyright (c) 2016 Tzutalin
# Create by TzuTaLin <tzu.ta.lin@gmail.com> # Create by TzuTaLin <tzu.ta.lin@gmail.com>
try:
from PyQt5.QtGui import QImage
except ImportError:
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from pascal_voc_io import PascalVocWriter from pascal_voc_io import PascalVocWriter
import os.path import os.path

View File

@ -1,5 +1,10 @@
from math import sqrt from math import sqrt
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *

View File

@ -74,7 +74,11 @@ class PascalVocWriter:
for each_object in self.boxlist: for each_object in self.boxlist:
object_item = SubElement(top, 'object') object_item = SubElement(top, 'object')
name = SubElement(object_item, 'name') name = SubElement(object_item, 'name')
try:
name.text = unicode(each_object['name']) 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 = SubElement(object_item, 'pose')
pose.text = "Unspecified" pose.text = "Unspecified"
truncated = SubElement(object_item, 'truncated') truncated = SubElement(object_item, 'truncated')
@ -101,7 +105,7 @@ class PascalVocWriter:
out_file = open(targetFile, 'w') out_file = open(targetFile, 'w')
prettifyResult = self.prettify(root) prettifyResult = self.prettify(root)
out_file.write(prettifyResult) out_file.write(prettifyResult.decode('utf8'))
out_file.close() out_file.close()

View File

@ -1,6 +1,11 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *

View File

@ -1,6 +1,12 @@
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
class ToolBar(QToolBar): class ToolBar(QToolBar):
def __init__(self, title): def __init__(self, title):
super(ToolBar, self).__init__(title) super(ToolBar, self).__init__(title)

View File

@ -1,3 +1,8 @@
try:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
except ImportError:
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *

21
tests/test.py Normal file
View 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