transform.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. """Affine 2D transformation matrix class.
  2. The Transform class implements various transformation matrix operations,
  3. both on the matrix itself, as well as on 2D coordinates.
  4. Transform instances are effectively immutable: all methods that operate on the
  5. transformation itself always return a new instance. This has as the
  6. interesting side effect that Transform instances are hashable, ie. they can be
  7. used as dictionary keys.
  8. This module exports the following symbols:
  9. Transform
  10. this is the main class
  11. Identity
  12. Transform instance set to the identity transformation
  13. Offset
  14. Convenience function that returns a translating transformation
  15. Scale
  16. Convenience function that returns a scaling transformation
  17. The DecomposedTransform class implements a transformation with separate
  18. translate, rotation, scale, skew, and transformation-center components.
  19. :Example:
  20. >>> t = Transform(2, 0, 0, 3, 0, 0)
  21. >>> t.transformPoint((100, 100))
  22. (200, 300)
  23. >>> t = Scale(2, 3)
  24. >>> t.transformPoint((100, 100))
  25. (200, 300)
  26. >>> t.transformPoint((0, 0))
  27. (0, 0)
  28. >>> t = Offset(2, 3)
  29. >>> t.transformPoint((100, 100))
  30. (102, 103)
  31. >>> t.transformPoint((0, 0))
  32. (2, 3)
  33. >>> t2 = t.scale(0.5)
  34. >>> t2.transformPoint((100, 100))
  35. (52.0, 53.0)
  36. >>> import math
  37. >>> t3 = t2.rotate(math.pi / 2)
  38. >>> t3.transformPoint((0, 0))
  39. (2.0, 3.0)
  40. >>> t3.transformPoint((100, 100))
  41. (-48.0, 53.0)
  42. >>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
  43. >>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
  44. [(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
  45. >>>
  46. """
  47. import math
  48. from typing import NamedTuple
  49. from dataclasses import dataclass
  50. __all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
  51. _EPSILON = 1e-15
  52. _ONE_EPSILON = 1 - _EPSILON
  53. _MINUS_ONE_EPSILON = -1 + _EPSILON
  54. def _normSinCos(v):
  55. if abs(v) < _EPSILON:
  56. v = 0
  57. elif v > _ONE_EPSILON:
  58. v = 1
  59. elif v < _MINUS_ONE_EPSILON:
  60. v = -1
  61. return v
  62. class Transform(NamedTuple):
  63. """2x2 transformation matrix plus offset, a.k.a. Affine transform.
  64. Transform instances are immutable: all transforming methods, eg.
  65. rotate(), return a new Transform instance.
  66. :Example:
  67. >>> t = Transform()
  68. >>> t
  69. <Transform [1 0 0 1 0 0]>
  70. >>> t.scale(2)
  71. <Transform [2 0 0 2 0 0]>
  72. >>> t.scale(2.5, 5.5)
  73. <Transform [2.5 0 0 5.5 0 0]>
  74. >>>
  75. >>> t.scale(2, 3).transformPoint((100, 100))
  76. (200, 300)
  77. Transform's constructor takes six arguments, all of which are
  78. optional, and can be used as keyword arguments::
  79. >>> Transform(12)
  80. <Transform [12 0 0 1 0 0]>
  81. >>> Transform(dx=12)
  82. <Transform [1 0 0 1 12 0]>
  83. >>> Transform(yx=12)
  84. <Transform [1 0 12 1 0 0]>
  85. Transform instances also behave like sequences of length 6::
  86. >>> len(Identity)
  87. 6
  88. >>> list(Identity)
  89. [1, 0, 0, 1, 0, 0]
  90. >>> tuple(Identity)
  91. (1, 0, 0, 1, 0, 0)
  92. Transform instances are comparable::
  93. >>> t1 = Identity.scale(2, 3).translate(4, 6)
  94. >>> t2 = Identity.translate(8, 18).scale(2, 3)
  95. >>> t1 == t2
  96. 1
  97. But beware of floating point rounding errors::
  98. >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
  99. >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
  100. >>> t1
  101. <Transform [0.2 0 0 0.3 0.08 0.18]>
  102. >>> t2
  103. <Transform [0.2 0 0 0.3 0.08 0.18]>
  104. >>> t1 == t2
  105. 0
  106. Transform instances are hashable, meaning you can use them as
  107. keys in dictionaries::
  108. >>> d = {Scale(12, 13): None}
  109. >>> d
  110. {<Transform [12 0 0 13 0 0]>: None}
  111. But again, beware of floating point rounding errors::
  112. >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
  113. >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
  114. >>> t1
  115. <Transform [0.2 0 0 0.3 0.08 0.18]>
  116. >>> t2
  117. <Transform [0.2 0 0 0.3 0.08 0.18]>
  118. >>> d = {t1: None}
  119. >>> d
  120. {<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
  121. >>> d[t2]
  122. Traceback (most recent call last):
  123. File "<stdin>", line 1, in ?
  124. KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
  125. """
  126. xx: float = 1
  127. xy: float = 0
  128. yx: float = 0
  129. yy: float = 1
  130. dx: float = 0
  131. dy: float = 0
  132. def transformPoint(self, p):
  133. """Transform a point.
  134. :Example:
  135. >>> t = Transform()
  136. >>> t = t.scale(2.5, 5.5)
  137. >>> t.transformPoint((100, 100))
  138. (250.0, 550.0)
  139. """
  140. (x, y) = p
  141. xx, xy, yx, yy, dx, dy = self
  142. return (xx * x + yx * y + dx, xy * x + yy * y + dy)
  143. def transformPoints(self, points):
  144. """Transform a list of points.
  145. :Example:
  146. >>> t = Scale(2, 3)
  147. >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
  148. [(0, 0), (0, 300), (200, 300), (200, 0)]
  149. >>>
  150. """
  151. xx, xy, yx, yy, dx, dy = self
  152. return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points]
  153. def transformVector(self, v):
  154. """Transform an (dx, dy) vector, treating translation as zero.
  155. :Example:
  156. >>> t = Transform(2, 0, 0, 2, 10, 20)
  157. >>> t.transformVector((3, -4))
  158. (6, -8)
  159. >>>
  160. """
  161. (dx, dy) = v
  162. xx, xy, yx, yy = self[:4]
  163. return (xx * dx + yx * dy, xy * dx + yy * dy)
  164. def transformVectors(self, vectors):
  165. """Transform a list of (dx, dy) vector, treating translation as zero.
  166. :Example:
  167. >>> t = Transform(2, 0, 0, 2, 10, 20)
  168. >>> t.transformVectors([(3, -4), (5, -6)])
  169. [(6, -8), (10, -12)]
  170. >>>
  171. """
  172. xx, xy, yx, yy = self[:4]
  173. return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors]
  174. def translate(self, x=0, y=0):
  175. """Return a new transformation, translated (offset) by x, y.
  176. :Example:
  177. >>> t = Transform()
  178. >>> t.translate(20, 30)
  179. <Transform [1 0 0 1 20 30]>
  180. >>>
  181. """
  182. return self.transform((1, 0, 0, 1, x, y))
  183. def scale(self, x=1, y=None):
  184. """Return a new transformation, scaled by x, y. The 'y' argument
  185. may be None, which implies to use the x value for y as well.
  186. :Example:
  187. >>> t = Transform()
  188. >>> t.scale(5)
  189. <Transform [5 0 0 5 0 0]>
  190. >>> t.scale(5, 6)
  191. <Transform [5 0 0 6 0 0]>
  192. >>>
  193. """
  194. if y is None:
  195. y = x
  196. return self.transform((x, 0, 0, y, 0, 0))
  197. def rotate(self, angle):
  198. """Return a new transformation, rotated by 'angle' (radians).
  199. :Example:
  200. >>> import math
  201. >>> t = Transform()
  202. >>> t.rotate(math.pi / 2)
  203. <Transform [0 1 -1 0 0 0]>
  204. >>>
  205. """
  206. import math
  207. c = _normSinCos(math.cos(angle))
  208. s = _normSinCos(math.sin(angle))
  209. return self.transform((c, s, -s, c, 0, 0))
  210. def skew(self, x=0, y=0):
  211. """Return a new transformation, skewed by x and y.
  212. :Example:
  213. >>> import math
  214. >>> t = Transform()
  215. >>> t.skew(math.pi / 4)
  216. <Transform [1 0 1 1 0 0]>
  217. >>>
  218. """
  219. import math
  220. return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
  221. def transform(self, other):
  222. """Return a new transformation, transformed by another
  223. transformation.
  224. :Example:
  225. >>> t = Transform(2, 0, 0, 3, 1, 6)
  226. >>> t.transform((4, 3, 2, 1, 5, 6))
  227. <Transform [8 9 4 3 11 24]>
  228. >>>
  229. """
  230. xx1, xy1, yx1, yy1, dx1, dy1 = other
  231. xx2, xy2, yx2, yy2, dx2, dy2 = self
  232. return self.__class__(
  233. xx1 * xx2 + xy1 * yx2,
  234. xx1 * xy2 + xy1 * yy2,
  235. yx1 * xx2 + yy1 * yx2,
  236. yx1 * xy2 + yy1 * yy2,
  237. xx2 * dx1 + yx2 * dy1 + dx2,
  238. xy2 * dx1 + yy2 * dy1 + dy2,
  239. )
  240. def reverseTransform(self, other):
  241. """Return a new transformation, which is the other transformation
  242. transformed by self. self.reverseTransform(other) is equivalent to
  243. other.transform(self).
  244. :Example:
  245. >>> t = Transform(2, 0, 0, 3, 1, 6)
  246. >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
  247. <Transform [8 6 6 3 21 15]>
  248. >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
  249. <Transform [8 6 6 3 21 15]>
  250. >>>
  251. """
  252. xx1, xy1, yx1, yy1, dx1, dy1 = self
  253. xx2, xy2, yx2, yy2, dx2, dy2 = other
  254. return self.__class__(
  255. xx1 * xx2 + xy1 * yx2,
  256. xx1 * xy2 + xy1 * yy2,
  257. yx1 * xx2 + yy1 * yx2,
  258. yx1 * xy2 + yy1 * yy2,
  259. xx2 * dx1 + yx2 * dy1 + dx2,
  260. xy2 * dx1 + yy2 * dy1 + dy2,
  261. )
  262. def inverse(self):
  263. """Return the inverse transformation.
  264. :Example:
  265. >>> t = Identity.translate(2, 3).scale(4, 5)
  266. >>> t.transformPoint((10, 20))
  267. (42, 103)
  268. >>> it = t.inverse()
  269. >>> it.transformPoint((42, 103))
  270. (10.0, 20.0)
  271. >>>
  272. """
  273. if self == Identity:
  274. return self
  275. xx, xy, yx, yy, dx, dy = self
  276. det = xx * yy - yx * xy
  277. xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det
  278. dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy
  279. return self.__class__(xx, xy, yx, yy, dx, dy)
  280. def toPS(self):
  281. """Return a PostScript representation
  282. :Example:
  283. >>> t = Identity.scale(2, 3).translate(4, 5)
  284. >>> t.toPS()
  285. '[2 0 0 3 8 15]'
  286. >>>
  287. """
  288. return "[%s %s %s %s %s %s]" % self
  289. def toDecomposed(self) -> "DecomposedTransform":
  290. """Decompose into a DecomposedTransform."""
  291. return DecomposedTransform.fromTransform(self)
  292. def __bool__(self):
  293. """Returns True if transform is not identity, False otherwise.
  294. :Example:
  295. >>> bool(Identity)
  296. False
  297. >>> bool(Transform())
  298. False
  299. >>> bool(Scale(1.))
  300. False
  301. >>> bool(Scale(2))
  302. True
  303. >>> bool(Offset())
  304. False
  305. >>> bool(Offset(0))
  306. False
  307. >>> bool(Offset(2))
  308. True
  309. """
  310. return self != Identity
  311. def __repr__(self):
  312. return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
  313. Identity = Transform()
  314. def Offset(x=0, y=0):
  315. """Return the identity transformation offset by x, y.
  316. :Example:
  317. >>> Offset(2, 3)
  318. <Transform [1 0 0 1 2 3]>
  319. >>>
  320. """
  321. return Transform(1, 0, 0, 1, x, y)
  322. def Scale(x, y=None):
  323. """Return the identity transformation scaled by x, y. The 'y' argument
  324. may be None, which implies to use the x value for y as well.
  325. :Example:
  326. >>> Scale(2, 3)
  327. <Transform [2 0 0 3 0 0]>
  328. >>>
  329. """
  330. if y is None:
  331. y = x
  332. return Transform(x, 0, 0, y, 0, 0)
  333. @dataclass
  334. class DecomposedTransform:
  335. """The DecomposedTransform class implements a transformation with separate
  336. translate, rotation, scale, skew, and transformation-center components.
  337. """
  338. translateX: float = 0
  339. translateY: float = 0
  340. rotation: float = 0 # in degrees, counter-clockwise
  341. scaleX: float = 1
  342. scaleY: float = 1
  343. skewX: float = 0 # in degrees, clockwise
  344. skewY: float = 0 # in degrees, counter-clockwise
  345. tCenterX: float = 0
  346. tCenterY: float = 0
  347. @classmethod
  348. def fromTransform(self, transform):
  349. # Adapted from an answer on
  350. # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
  351. a, b, c, d, x, y = transform
  352. sx = math.copysign(1, a)
  353. if sx < 0:
  354. a *= sx
  355. b *= sx
  356. delta = a * d - b * c
  357. rotation = 0
  358. scaleX = scaleY = 0
  359. skewX = skewY = 0
  360. # Apply the QR-like decomposition.
  361. if a != 0 or b != 0:
  362. r = math.sqrt(a * a + b * b)
  363. rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
  364. scaleX, scaleY = (r, delta / r)
  365. skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0)
  366. elif c != 0 or d != 0:
  367. s = math.sqrt(c * c + d * d)
  368. rotation = math.pi / 2 - (
  369. math.acos(-c / s) if d >= 0 else -math.acos(c / s)
  370. )
  371. scaleX, scaleY = (delta / s, s)
  372. skewX, skewY = (0, math.atan((a * c + b * d) / (s * s)))
  373. else:
  374. # a = b = c = d = 0
  375. pass
  376. return DecomposedTransform(
  377. x,
  378. y,
  379. math.degrees(rotation),
  380. scaleX * sx,
  381. scaleY,
  382. math.degrees(skewX) * sx,
  383. math.degrees(skewY),
  384. 0,
  385. 0,
  386. )
  387. def toTransform(self):
  388. """Return the Transform() equivalent of this transformation.
  389. :Example:
  390. >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
  391. <Transform [2 0 0 2 0 0]>
  392. >>>
  393. """
  394. t = Transform()
  395. t = t.translate(
  396. self.translateX + self.tCenterX, self.translateY + self.tCenterY
  397. )
  398. t = t.rotate(math.radians(self.rotation))
  399. t = t.scale(self.scaleX, self.scaleY)
  400. t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
  401. t = t.translate(-self.tCenterX, -self.tCenterY)
  402. return t
  403. if __name__ == "__main__":
  404. import sys
  405. import doctest
  406. sys.exit(doctest.testmod().failed)