Various performance improvements to pg.Point (#1741)

This change makes use of QPointF methods which perform faster than the python
equivalent methods.  Furthermore, some tests are added.

* Set __slots__ to empty tuple for pg.Point
* Make Point.angle() behave as Vector.angle()
This commit is contained in:
Ogi Moore 2021-04-28 21:29:47 -07:00 committed by GitHub
parent fe66f3fd12
commit a534132c62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 36 deletions

View File

@ -56,7 +56,7 @@ def update():
p2 = pts[i+1] p2 = pts[i+1]
v2 = p2 - p1 v2 = p2 - p1
t = p1 - pts[0] t = p1 - pts[0]
r = v2.angle(v1) r = v1.angle(v2)
s = v2.length() / l1 s = v2.length() / l1
trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r})) trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r}))

View File

@ -12,21 +12,23 @@ from math import atan2, hypot, degrees
class Point(QtCore.QPointF): class Point(QtCore.QPointF):
"""Extension of QPointF which adds a few missing methods.""" """Extension of QPointF which adds a few missing methods."""
__slots__ = ()
def __init__(self, *args): def __init__(self, *args):
if len(args) == 1: if len(args) == 1:
if isinstance(args[0], QtCore.QSizeF): if isinstance(args[0], (QtCore.QSize, QtCore.QSizeF)):
QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height())) super().__init__(float(args[0].width()), float(args[0].height()))
return return
elif isinstance(args[0], float) or isinstance(args[0], int): elif isinstance(args[0], (int, float)):
QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) super().__init__(float(args[0]), float(args[0]))
return return
elif hasattr(args[0], '__getitem__'): elif hasattr(args[0], '__getitem__'):
QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1])) super().__init__(float(args[0][0]), float(args[0][1]))
return return
elif len(args) == 2: elif len(args) == 2:
QtCore.QPointF.__init__(self, args[0], args[1]) super().__init__(args[0], args[1])
return return
QtCore.QPointF.__init__(self, *args) super().__init__(*args)
def __len__(self): def __len__(self):
return 2 return 2
@ -42,6 +44,10 @@ class Point(QtCore.QPointF):
else: else:
raise IndexError("Point has no index %s" % str(i)) raise IndexError("Point has no index %s" % str(i))
def __iter__(self):
yield(self.x())
yield(self.y())
def __setitem__(self, i, x): def __setitem__(self, i, x):
if i == 0: if i == 0:
return self.setX(x) return self.setX(x)
@ -87,47 +93,67 @@ class Point(QtCore.QPointF):
return self._math_('__pow__', a) return self._math_('__pow__', a)
def _math_(self, op, x): def _math_(self, op, x):
x = Point(x) if not isinstance(x, QtCore.QPointF):
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1])) x = Point(x)
return Point(getattr(self.x(), op)(x.x()), getattr(self.y(), op)(x.y()))
def length(self): def length(self):
"""Returns the vector length of this Point.""" """Returns the vector length of this Point."""
return hypot(self[0], self[1]) # length return hypot(self.x(), self.y()) # length
def norm(self): def norm(self):
"""Returns a vector in the same direction with unit length.""" """Returns a vector in the same direction with unit length."""
return self / self.length() return self / self.length()
def angle(self, a): def angle(self, a, units="degrees"):
"""Returns the angle in degrees between this vector and the vector a.""" """
rads = atan2(self.y(), self.x()) - atan2(a.y(), a.x()) Returns the angle in degrees between this vector and the vector a.
Parameters
----------
a : Point, QPointF or QPoint
The Point to return the angle with
units : str, optional
The units with which to compute the angle with, "degrees" or "radians",
default "degrees"
Returns
-------
float
The angle between the two points
"""
rads = atan2(a.y(), a.x()) - atan2(self.y(), self.x())
if units == "radians":
return rads
return degrees(rads) return degrees(rads)
def dot(self, a): def dot(self, a):
"""Returns the dot product of a and this Point.""" """Returns the dot product of a and this Point."""
a = Point(a) if not isinstance(a, QtCore.QPointF):
return self[0]*a[0] + self[1]*a[1] a = Point(a)
return Point.dotProduct(self, a)
def cross(self, a): def cross(self, a):
a = Point(a) if not isinstance(a, QtCore.QPointF):
return self[0]*a[1] - self[1]*a[0] a = Point(a)
return self.x() * a.y() - self.y() * a.x()
def proj(self, b): def proj(self, b):
"""Return the projection of this vector onto the vector b""" """Return the projection of this vector onto the vector b"""
b1 = b / b.length() b1 = b.norm()
return self.dot(b1) * b1 return self.dot(b1) * b1
def __repr__(self): def __repr__(self):
return "Point(%f, %f)" % (self[0], self[1]) return "Point(%f, %f)" % (self.x(), self.y())
def min(self): def min(self):
return min(self[0], self[1]) return min(self.x(), self.y())
def max(self): def max(self):
return max(self[0], self[1]) return max(self.x(), self.y())
def copy(self): def copy(self):
return Point(self) return Point(self)
def toQPoint(self): def toQPoint(self):
return QtCore.QPoint(int(self[0]), int(self[1])) return self.toPoint()

View File

@ -66,8 +66,7 @@ class SRTTransform(QtGui.QTransform):
dp3 = Point(p3-p1) dp3 = Point(p3-p1)
## detect flipped axes ## detect flipped axes
if dp2.angle(dp3) > 0: if dp3.angle(dp2, units="radians") > 0:
#da = 180
da = 0 da = 0
sy = -1.0 sy = -1.0
else: else:

View File

@ -439,13 +439,10 @@ class GraphicsItem(object):
if relativeItem is None: if relativeItem is None:
relativeItem = self.parentItem() relativeItem = self.parentItem()
tr = self.itemTransform(relativeItem) tr = self.itemTransform(relativeItem)
if isinstance(tr, tuple): ## difference between pyside and pyqt if isinstance(tr, tuple): ## difference between pyside and pyqt
tr = tr[0] tr = tr[0]
#vec = tr.map(Point(1,0)) - tr.map(Point(0,0))
vec = tr.map(QtCore.QLineF(0,0,1,0)) vec = tr.map(QtCore.QLineF(0,0,1,0))
#return Point(vec).angle(Point(1,0))
return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0))) return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0)))
#def itemChange(self, change, value): #def itemChange(self, change, value):

View File

@ -17,7 +17,7 @@ import numpy as np
#from numpy.linalg import norm #from numpy.linalg import norm
from ..Point import Point from ..Point import Point
from ..SRTTransform import SRTTransform from ..SRTTransform import SRTTransform
from math import atan2, cos, sin, hypot, radians from math import atan2, cos, degrees, sin, hypot
from .. import functions as fn from .. import functions as fn
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from .UIGraphicsItem import UIGraphicsItem from .UIGraphicsItem import UIGraphicsItem
@ -936,7 +936,7 @@ class ROI(GraphicsObject):
return return
## determine new rotation angle, constrained if necessary ## determine new rotation angle, constrained if necessary
ang = newState['angle'] - lp0.angle(lp1) ang = newState['angle'] - lp1.angle(lp0)
if ang is None: ## this should never happen.. if ang is None: ## this should never happen..
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -972,7 +972,7 @@ class ROI(GraphicsObject):
except OverflowError: except OverflowError:
return return
ang = newState['angle'] - lp0.angle(lp1) ang = newState['angle'] - lp1.angle(lp0)
if ang is None: if ang is None:
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -1663,12 +1663,11 @@ class LineROI(ROI):
pos2 = Point(pos2) pos2 = Point(pos2)
d = pos2-pos1 d = pos2-pos1
l = d.length() l = d.length()
ang = Point(1, 0).angle(d) ra = d.angle(Point(1, 0), units="radians")
ra = radians(ang if ang is not None else 0.)
c = Point(-width/2. * sin(ra), -width/2. * cos(ra)) c = Point(-width/2. * sin(ra), -width/2. * cos(ra))
pos1 = pos1 + c pos1 = pos1 + c
ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args) ROI.__init__(self, pos1, size=Point(l, width), angle=degrees(ra), **args)
self.addScaleRotateHandle([0, 0.5], [1, 0.5]) self.addScaleRotateHandle([0, 0.5], [1, 0.5])
self.addScaleRotateHandle([1, 0.5], [0, 0.5]) self.addScaleRotateHandle([1, 0.5], [0, 0.5])
self.addScaleHandle([0.5, 1], [0.5, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5])
@ -2359,7 +2358,7 @@ class RulerROI(LineSegmentROI):
vec = Point(h2) - Point(h1) vec = Point(h2) - Point(h1)
length = vec.length() length = vec.length()
angle = vec.angle(Point(1, 0)) angle = Point(1, 0).angle(vec)
pvec = p2 - p1 pvec = p2 - p1
pvecT = Point(pvec.y(), -pvec.x()) pvecT = Point(pvec.y(), -pvec.x())

View File

@ -0,0 +1,63 @@
import pytest
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import math
angles = [
((1, 0), (0, 1), 90),
((0, 1), (1, 0), -90),
((-1, 0), (-1, 0), 0),
((0, -1), (0, 1), 180),
]
@pytest.mark.parametrize("p1, p2, angle", angles)
def test_Point_angle(p1, p2, angle):
p1 = pg.Point(*p1)
p2 = pg.Point(*p2)
assert p1.angle(p2) == angle
inits = [
(QtCore.QSizeF(1, 0), (1.0, 0.0)),
((0, -1), (0.0, -1.0)),
([1, 1], (1.0, 1.0)),
]
@pytest.mark.parametrize("initArgs, positions", inits)
def test_Point_init(initArgs, positions):
if isinstance(initArgs, QtCore.QSizeF):
point = pg.Point(initArgs)
else:
point = pg.Point(*initArgs)
assert (point.x(), point.y()) == positions
lengths = [
((0, 1), 1),
((1, 0), 1),
((0, 0), 0),
((1, 1), math.sqrt(2)),
((-1, -1), math.sqrt(2))
]
@pytest.mark.parametrize("initArgs, length", lengths)
def test_Point_length(initArgs, length):
point = pg.Point(initArgs)
assert point.length() == length
min_max = [
((0, 1), 0, 1),
((1, 0), 0, 1),
((-math.inf, 0), -math.inf, 0),
((0, math.inf), 0, math.inf)
]
@pytest.mark.parametrize("initArgs, min_, max_", min_max)
def test_Point_min_max(initArgs, min_, max_):
point = pg.Point(initArgs)
assert min(point) == min_
assert max(point) == max_
projections = [
((0, 1), (1, 0), (1, 1))
]
@pytest.mark.parametrize("p1_arg, p2_arg, projection", projections)
def test_Point_projection(p1_arg, p2_arg, projection):
p1 = pg.Point(p1_arg)
p2 = pg.Point(p2_arg)
p1.proj(p2) == projection