From: <md...@us...> - 2007-09-10 17:40:51
|
Revision: 3823 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3823&view=rev Author: mdboom Date: 2007-09-10 10:40:47 -0700 (Mon, 10 Sep 2007) Log Message: ----------- Adding new files that will eventually replace transforms.py/cpp Added Paths: ----------- branches/transforms/lib/matplotlib/affine.py branches/transforms/lib/matplotlib/bbox.py Added: branches/transforms/lib/matplotlib/affine.py =================================================================== --- branches/transforms/lib/matplotlib/affine.py (rev 0) +++ branches/transforms/lib/matplotlib/affine.py 2007-09-10 17:40:47 UTC (rev 3823) @@ -0,0 +1,207 @@ +""" +A set of classes to handle transformations. + +2007 Michael Droettboom +""" + +import numpy as N +from numpy.linalg import inv + +class Transform(object): + def __call__(self, points): + raise NotImplementedError() + + def __add__(self, other): + if isinstance(other, Transform): + return CompositeTransform(self, other) + raise TypeError("Can not add Transform to object of type '%s'" % type(other)) + + def __radd__(self, other): + if isinstance(other, Transform): + return CompositeTransform(other, self) + raise TypeError("Can not add Transform to object of type '%s'" % type(other)) + + def has_inverse(self): + raise NotImplementedError() + + def inverted(self): + raise NotImplementedError() + + def is_separable(self): + return False + +class CompositeTransform(Transform): + def __init__(self, a, b): + assert a.output_dims == b.input_dims + self.a = a + self.b = b + + def __call__(self, points): + return self.b(self.a(points)) + +class Affine2D(Transform): + input_dims = 2 + output_dims = 2 + + def __init__(self, matrix = None): + """ + Initialize an Affine transform from a 3x3 numpy float array. + + a c e + b d f + 0 0 1 + """ + if matrix is None: + matrix = N.identity(3) + else: + assert matrix.shape == (3, 3) + self.mtx = matrix + + def __repr__(self): + return repr(self.mtx) + + def __str__(self): + return str(self.mtx) + + #@staticmethod + def from_values(a, b, c, d, e, f): + return Affine2D(Affine2D.matrix_from_values(a, b, c, d, e, f)) + from_values = staticmethod(from_values) + + #@staticmethod + def matrix_from_values(a, b, c, d, e, f): + affine = N.zeros((3,3), N.float_) + affine[0,] = a, c, e + affine[1,] = b, d, f + affine[2,2] = 1 + return affine + matrix_from_values = staticmethod(matrix_from_values) + + def __call__(self, points): + """ + Applies the transformation to a set of 2D points and + returns the result. + + points must be a numpy array of shape (N, 2), where N is the + number of points. + """ + # MGDTODO: This involves a copy. We may need to do something like + # http://neuroimaging.scipy.org/svn/ni/ni/trunk/neuroimaging/core/reference/mapping.py + # to separate the matrix out into the translation and scale components + # and apply each separately (which is still sub-optimal) + + # This is nicer for now, however, since we can just keep a + # regular affine matrix around + new_points = points.swapaxes(0, 1) + new_points = N.vstack((new_points, N.ones((1, points.shape[0])))) + result = N.dot(self.mtx, new_points)[:2] + result.swapaxes(0, 1) + return result + + #@staticmethod + def _concat(a, b): + return N.dot(b, a) + _concat = staticmethod(_concat) + + def concat(a, b): + return Affine2D(Affine2D._concat(a.mtx, b.mtx)) + concat = staticmethod(concat) + + #@staticmethod + def identity(): + return Affine2D(N.identity(3)) + identity = staticmethod(identity) + + def __add__(self, other): + if isinstance(other, Affine2D): + return Affine2D.concat(self, other) + return Transform.__add__(self, other) + + def __radd__(self, other): + if isinstance(other, Affine2D): + return Affine2D.concat(other, self) + return Transform.__radd__(self, other) + + def rotated(self, theta): + a = N.cos(theta) + b = N.sin(theta) + rotate_mtx = self.matrix_from_values(a, b, -b, a, 0, 0) + return Affine2D(self._concat(self.mtx, rotate_mtx)) + + def rotated_deg(self, degrees): + return self.rotated(degrees*N.pi/180.) + + def translated(self, tx, ty): + translate_mtx = self.matrix_from_values(1., 0., 0., 1., tx, ty) + return Affine2D(self._concat(self.mtx, translate_mtx)) + + def scaled(self, sx, sy=None): + if sy is None: + sy = sx + scale_mtx = self.matrix_from_values(sx, 0., 0., sy, 0., 0.) + return Affine2D(self._concat(self.mtx, scale_mtx)) + + def inverted(self): + # MGDTODO: We may want to optimize by storing the inverse + # of the transform with every transform + return Affine2D(inv(self.mtx)) + + def is_separable(self): + mtx = self.mtx + return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 + +class BlendedAffine2D(Affine2D): + def __init__(self, x_transform, y_transform): + assert isinstance(x_transform, Affine2D) + assert isinstance(y_transform, Affine2D) + assert x_transform.is_separable() + assert y_transform.is_separable() + x_mtx = x_transform.mtx + y_mtx = y_transform.mtx + self.mtx = self.matrix_from_values( + x_mtx[0,0], 0.0, 0.0, y_mtx[1,1], x_mtx[0,2], y_mtx[1,2]) + +# This is a placeholder since eventually we may need to handle the +# more general case of two transforms that aren't affines +BlendedTransform = BlendedAffine2D + +def blend_xy_sep_transform(x_transform, y_transform): + return BlendedAffine2D(x_transform, y_transform) + +def get_bbox_transform(boxin, boxout): + x_scale = boxout.width() / boxin.width() + y_scale = boxout.height() / boxin.height() + + # MGDTODO: Optimize + return Affine2D() \ + .translated(-boxin.xmin(), -boxin.ymin()) \ + .scaled(x_scale, y_scale) \ + .translated(boxout.xmin(), boxout.ymin()) + +if __name__ == '__main__': + print Affine2D.from_values(1., 0, 0, 1, 0, 0) + + print "translated", Affine2D.identity().translated(5, 4) + print "rotated", Affine2D.identity().rotated_deg(30) + print "scaled", Affine2D.identity().scaled(5, 4) + + transform = Affine2D.identity().rotated_deg(30).translated(5, 4) + + points = N.array([[1, 2], [3, 4], [5, 6]]) + + print inv(transform.mtx) + + print transform(points) + + transform = Affine2D.identity().scaled(5., 1.).translated(10, 0) + print transform + print transform.inverted() + + from bbox import Bbox + boxin = Bbox([[10, 10], [320, 240]]) + boxout = Bbox([[25, 25], [640, 400]]) + trans = bbox_transform(boxin, boxout) + print trans + print trans(N.array([[10, 10], [320, 240]])) + +__all__ = ['Transform', 'Affine2D'] Added: branches/transforms/lib/matplotlib/bbox.py =================================================================== --- branches/transforms/lib/matplotlib/bbox.py (rev 0) +++ branches/transforms/lib/matplotlib/bbox.py 2007-09-10 17:40:47 UTC (rev 3823) @@ -0,0 +1,108 @@ +""" +A convenience class for handling bounding boxes + +2007 Michael Droettboom +""" + +import numpy as N + +class Bbox: + def __init__(self, points): + self._points = N.array(points) + + #@staticmethod + def unit(): + return Bbox([[0,0], [1,1]]) + unit = staticmethod(unit) + + #@staticmethod + def from_lbwh(left, bottom, width, height): + return Bbox([[left, bottom], [left + width, bottom + height]]) + from_lbwh = staticmethod(from_lbwh) + + #@staticmethod + def from_lbrt(left, bottom, right, top): + return Bbox([[left, bottom], [right, top]]) + from_lbwh = staticmethod(from_lbwh) + + # MGDTODO: Probably a more efficient ways to do this... + def xmin(self): + return self._points[0,0] + + def ymin(self): + return self._points[0,1] + + def xmax(self): + return self._points[1,0] + + def ymax(self): + return self._points[1,1] + + def width(self): + return self.xmax() - self.xmin() + + def height(self): + return self.ymax() - self.ymin() + + def transform(self, transform): + return Bbox(transform(points)) + + def inverse_transform(self, transform): + return Bbox(transform.inverted()(points)) + + def get_bounds(self): + return (self.xmin(), self.ymin(), + self.xmax() - self.xmin(), self.ymax() - self.ymin()) + +def lbwh_to_bbox(left, bottom, width, height): + return Bbox([[left, bottom], [left + width, bottom + height]]) + +def bbox_union(bboxes): + """ + Return the Bbox that bounds all bboxes + """ + assert(len(bboxes)) + + if len(bboxes) == 1: + return bboxes[0] + + bbox = bboxes[0] + xmin = bbox.xmin + ymin = bbox.ymin + xmax = bbox.xmax + ymax = bbox.ymax + + for bbox in bboxes[1:]: + xmin = min(xmin, bbox.xmin) + ymin = min(ymin, bbox.ymin) + xmax = max(xmax, bbox.xmax) + ymax = max(ymax, bbox.ymax) + + return Bbox(xmin, ymin, xmax, ymax) + +# MGDTODO: There's probably a better place for this +def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): + ''' + Ensure the endpoints of a range are not too close together. + + "too close" means the interval is smaller than 'tiny' times + the maximum absolute value. + + If they are too close, each will be moved by the 'expander'. + If 'increasing' is True and vmin > vmax, they will be swapped, + regardless of whether they are too close. + ''' + swapped = False + if vmax < vmin: + vmin, vmax = vmax, vmin + swapped = True + if vmax - vmin <= max(abs(vmin), abs(vmax)) * tiny: + if vmin==0.0: + vmin = -expander + vmax = expander + else: + vmin -= expander*abs(vmin) + vmax += expander*abs(vmax) + if swapped and not increasing: + vmin, vmax = vmax, vmin + return vmin, vmax This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |