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:
parent
fe66f3fd12
commit
a534132c62
@ -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}))
|
||||
|
||||
|
@ -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]))
|
||||
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
|
||||
@ -42,6 +44,10 @@ class Point(QtCore.QPointF):
|
||||
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:
|
||||
return self.setX(x)
|
||||
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -439,13 +439,10 @@ 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):
|
||||
|
@ -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())
|
||||
|
63
pyqtgraph/tests/test_Point.py
Normal file
63
pyqtgraph/tests/test_Point.py
Normal 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
|
Loading…
Reference in New Issue
Block a user