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]
v2 = p2 - p1
t = p1 - pts[0]
r = v2.angle(v1)
r = v1.angle(v2)
s = v2.length() / l1
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):
"""Extension of QPointF which adds a few missing methods."""
__slots__ = ()
def __init__(self, *args):
if len(args) == 1:
if isinstance(args[0], QtCore.QSizeF):
QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height()))
if isinstance(args[0], (QtCore.QSize, QtCore.QSizeF)):
super().__init__(float(args[0].width()), float(args[0].height()))
return
elif isinstance(args[0], float) or isinstance(args[0], int):
QtCore.QPointF.__init__(self, float(args[0]), float(args[0]))
elif isinstance(args[0], (int, float)):
super().__init__(float(args[0]), float(args[0]))
return
elif hasattr(args[0], '__getitem__'):
QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1]))
return
super().__init__(float(args[0][0]), float(args[0][1]))
return
elif len(args) == 2:
QtCore.QPointF.__init__(self, args[0], args[1])
super().__init__(args[0], args[1])
return
QtCore.QPointF.__init__(self, *args)
super().__init__(*args)
def __len__(self):
return 2
@ -41,6 +43,10 @@ class Point(QtCore.QPointF):
return self.y()
else:
raise IndexError("Point has no index %s" % str(i))
def __iter__(self):
yield(self.x())
yield(self.y())
def __setitem__(self, i, x):
if i == 0:
@ -87,47 +93,67 @@ class Point(QtCore.QPointF):
return self._math_('__pow__', a)
def _math_(self, op, x):
x = Point(x)
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1]))
if not isinstance(x, QtCore.QPointF):
x = Point(x)
return Point(getattr(self.x(), op)(x.x()), getattr(self.y(), op)(x.y()))
def length(self):
"""Returns the vector length of this Point."""
return hypot(self[0], self[1]) # length
return hypot(self.x(), self.y()) # length
def norm(self):
"""Returns a vector in the same direction with unit length."""
return self / self.length()
def angle(self, a):
"""Returns the angle in degrees between this vector and the vector a."""
rads = atan2(self.y(), self.x()) - atan2(a.y(), a.x())
def angle(self, a, units="degrees"):
"""
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)
def dot(self, a):
"""Returns the dot product of a and this Point."""
a = Point(a)
return self[0]*a[0] + self[1]*a[1]
if not isinstance(a, QtCore.QPointF):
a = Point(a)
return Point.dotProduct(self, a)
def cross(self, a):
a = Point(a)
return self[0]*a[1] - self[1]*a[0]
if not isinstance(a, QtCore.QPointF):
a = Point(a)
return self.x() * a.y() - self.y() * a.x()
def proj(self, b):
"""Return the projection of this vector onto the vector b"""
b1 = b / b.length()
b1 = b.norm()
return self.dot(b1) * b1
def __repr__(self):
return "Point(%f, %f)" % (self[0], self[1])
return "Point(%f, %f)" % (self.x(), self.y())
def min(self):
return min(self[0], self[1])
return min(self.x(), self.y())
def max(self):
return max(self[0], self[1])
return max(self.x(), self.y())
def copy(self):
return Point(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)
## detect flipped axes
if dp2.angle(dp3) > 0:
#da = 180
if dp3.angle(dp2, units="radians") > 0:
da = 0
sy = -1.0
else:

View File

@ -438,14 +438,11 @@ class GraphicsItem(object):
"""
if relativeItem is None:
relativeItem = self.parentItem()
tr = self.itemTransform(relativeItem)
if isinstance(tr, tuple): ## difference between pyside and pyqt
tr = tr[0]
#vec = tr.map(Point(1,0)) - tr.map(Point(0,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)))
#def itemChange(self, change, value):

View File

@ -17,7 +17,7 @@ import numpy as np
#from numpy.linalg import norm
from ..Point import Point
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 .GraphicsObject import GraphicsObject
from .UIGraphicsItem import UIGraphicsItem
@ -936,7 +936,7 @@ class ROI(GraphicsObject):
return
## 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..
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -972,7 +972,7 @@ class ROI(GraphicsObject):
except OverflowError:
return
ang = newState['angle'] - lp0.angle(lp1)
ang = newState['angle'] - lp1.angle(lp0)
if ang is None:
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
@ -1663,12 +1663,11 @@ class LineROI(ROI):
pos2 = Point(pos2)
d = pos2-pos1
l = d.length()
ang = Point(1, 0).angle(d)
ra = radians(ang if ang is not None else 0.)
ra = d.angle(Point(1, 0), units="radians")
c = Point(-width/2. * sin(ra), -width/2. * cos(ra))
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([1, 0.5], [0, 0.5])
self.addScaleHandle([0.5, 1], [0.5, 0.5])
@ -2359,7 +2358,7 @@ class RulerROI(LineSegmentROI):
vec = Point(h2) - Point(h1)
length = vec.length()
angle = vec.angle(Point(1, 0))
angle = Point(1, 0).angle(vec)
pvec = p2 - p1
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