add support for QT5, fallback to QT4

This commit is contained in:
Ryan Flynn 2017-01-02 20:50:02 -05:00
parent c7ddf4a3ef
commit 2beed27cf2
11 changed files with 234 additions and 57 deletions

View File

@ -1,5 +1,8 @@
# vim: set ts=2 et:
# run xvfb with 32-bit color
# xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here
matrix:
include:
@ -15,13 +18,59 @@ matrix:
apt:
packages:
- cmake
- python-qt4
- pyqt4-dev-tools
- xvfb
before_install:
- sudo pip install lxml
- make qt4
- make qt4py2
- ( xvfb-run python2.7 labelImg.py ) & sleep 10 ; kill $!
# 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 python2.7 labelImg.py ) & sleep 10 ; kill $!
# Python 3 + QT4
- os: linux
dist: trusty
sudo: required
language: generic
python: "3.4"
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 python3 labelImg.py ) & sleep 10 ; kill $!
# Python 3 + QT5
- os: linux
dist: trusty
@ -34,13 +83,13 @@ matrix:
apt:
packages:
- cmake
- pyqt5-dev-tools
- xvfb
before_install:
- sudo apt-get update
- sudo apt-get install -y pyqt5-dev-tools
- sudo apt-get install -y python3-pip
- sudo pip3 install lxml
- make qt5
- make qt5py3
- ( xvfb-run python3 labelImg.py ) & sleep 10 ; kill $!
# OS X Python 3 + QT5
@ -61,7 +110,7 @@ matrix:
- sudo -H easy_install-3.6 lxml || true
- python3 -c 'import sys; print(sys.path)'
- python3 -c 'import lxml'
- make qt5
- make qt5py3
- python3 -c 'help("modules")'
- ( python3 labelImg.py ) & sleep 10 ; kill $!

View File

@ -1,6 +1,16 @@
all: resources.py
all: qt4
%.py: %.qrc
pyrcc4 -o $@ $<
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

View File

@ -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
@ -31,13 +42,22 @@ __appname__ = 'labelImg'
def u(x):
'''unicode helper'''
'''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)
@ -56,6 +76,14 @@ 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 = list(range(3))
@ -332,19 +360,40 @@ 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)
@ -358,13 +407,18 @@ class MainWindow(QMainWindow, WindowMixin):
# 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()
@ -540,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)
@ -550,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
@ -694,17 +748,16 @@ class MainWindow(QMainWindow, WindowMixin):
self.resetState()
self.canvas.setEnabled(False)
if filename is None:
filename = self.settings['filename']
filename = 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)
@ -791,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'] = ''
@ -967,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)
@ -1108,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

View File

@ -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
@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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):