_geometric.py 90 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692
  1. from copy import copy
  2. import math
  3. import textwrap
  4. from abc import ABC, abstractmethod
  5. from typing import Self
  6. import warnings
  7. import numpy as np
  8. from scipy import spatial
  9. from .._shared.utils import (
  10. safe_as_int,
  11. _deprecate_estimate,
  12. _update_from_estimate_docstring,
  13. _deprecate_inherited_estimate,
  14. FailedEstimation,
  15. )
  16. from .._shared.compat import NP_COPY_IF_NEEDED
  17. def _affine_matrix_from_vector(v):
  18. """Affine matrix from linearized (d, d + 1) matrix entries."""
  19. nparam = v.size
  20. # solve for d in: d * (d + 1) = nparam
  21. d = (1 + np.sqrt(1 + 4 * nparam)) / 2 - 1
  22. dimensionality = int(np.round(d)) # round to prevent approx errors
  23. if d != dimensionality:
  24. raise ValueError(
  25. 'Invalid number of elements for ' f'linearized matrix: {nparam}'
  26. )
  27. matrix = np.eye(dimensionality + 1)
  28. matrix[:-1, :] = np.reshape(v, (dimensionality, dimensionality + 1))
  29. return matrix
  30. def _calc_center_normalize(points, scaling='rms'):
  31. """Calculate transformation `matrix` to center and normalize image points.
  32. Points are an array of shape (N, D).
  33. For `scaling` of 'raw', transformation returned `matrix` will be ``np.eye(D
  34. + 1)``. For other values of `scaling`, `matrix` expresses a two-step
  35. translation and scaling procedure. Points transformed with this `matrix`
  36. usually give better conditioning for fundamental matrix estimation than the
  37. original `points` [1]_.
  38. The two steps of transformation, for `scaling` other than 'raw', are:
  39. * Center the image points, such that the new coordinate system has its
  40. origin at the centroid of the image points.
  41. * Normalize the image points, such that the mean coordinate value of the
  42. centered points is 1 (`scaling` == 'rms') or such that the
  43. mean distance from the points to the origin of the coordinate system is
  44. ``sqrt(D)`` (`scaling` == 'mrs').
  45. If `scaling` != 'raw' and the points are all identical, the returned
  46. `matrix` will be all ``np.nan``.
  47. The 'mrs' scaling corresponds to the isotropic transformation
  48. algorithm in [1]_. 'rms' is the default, and gives very similar
  49. conditioning.
  50. Parameters
  51. ----------
  52. points : (N, D) array
  53. The coordinates of the image points.
  54. scaling : {'rms', 'mrs', 'raw'}, optional
  55. Scaling algorithm adjusting for magnitude of `points` after applying
  56. calculated translation. See above for explanation.
  57. Returns
  58. -------
  59. matrix : (D+1, D+1) array_like
  60. The transformation matrix to obtain the new points.
  61. References
  62. ----------
  63. .. [1] Hartley, Richard I. "In defense of the eight-point algorithm."
  64. Pattern Analysis and Machine Intelligence, IEEE Transactions on 19.6
  65. (1997): 580-593.
  66. """
  67. n, d = points.shape
  68. scaling = scaling.lower()
  69. matrix = np.eye(d + 1)
  70. if scaling == 'raw':
  71. return matrix
  72. centroid = np.mean(points, axis=0)
  73. centered = points - centroid
  74. if scaling == 'rms':
  75. divisor = np.sqrt(np.mean(centered**2))
  76. elif scaling == 'mrs':
  77. divisor = np.mean(np.sqrt(np.sum(centered**2, axis=1))) / np.sqrt(d)
  78. else:
  79. raise ValueError(f'Unexpected "scaling" of "{scaling}"')
  80. # if all the points are the same, the transformation matrix cannot be
  81. # created. We return an equivalent matrix with np.nans as sentinel values.
  82. # This obviates the need for try/except blocks in functions calling this
  83. # one, and those are only needed when actual 0 is reached, rather than some
  84. # small value; ie, we don't need to worry about numerical stability here,
  85. # only actual 0.
  86. if divisor == 0:
  87. return matrix + np.nan
  88. matrix[:d, d] = -centroid
  89. matrix[:d, :] /= divisor
  90. return matrix
  91. def _center_and_normalize_points(points, scaling='rms'):
  92. """Convenience function to calculate and apply scaling
  93. See: :func:`_calc_center_normalize` for details of the algorithm.
  94. """
  95. matrix = _calc_center_normalize(points, scaling)
  96. if not np.all(np.isfinite(matrix)):
  97. return matrix + np.nan, np.full_like(points, np.nan)
  98. return matrix, _apply_homogeneous(matrix, points)
  99. def _apply_homogeneous(matrix, points):
  100. """Transform (N, D) `points` array with homogeneous (D+1, D+1) `matrix`.
  101. Parameters
  102. ----------
  103. matrix : (D+1, D+1) array_like
  104. The transformation matrix to obtain the new points. Note that any
  105. object with an `__array__` method [1]_ that returns a matrix with the
  106. correct dimensions can be used as input here. This includes all
  107. subclasses of :class:`ProjectiveTransform`, for example.
  108. points : (N, D) array
  109. The coordinates of the image points.
  110. Returns
  111. -------
  112. new_points : (N, D) array
  113. The transformed image points.
  114. References
  115. ----------
  116. .. [1]:
  117. https://numpy.org/doc/stable/user/basics.interoperability.html#using-arbitrary-objects-in-numpy
  118. """
  119. points = np.array(points, copy=NP_COPY_IF_NEEDED, ndmin=2)
  120. points_h = _append_homogeneous_dim(points)
  121. new_points_h = points_h @ matrix.T
  122. # We divide by the last dimension of the homogeneous
  123. # coordinate matrix. In order to avoid division by zero,
  124. # we replace exact zeros in this column with a very small number.
  125. divs = new_points_h[:, -1]
  126. divs = np.where(divs == 0, np.finfo(float).eps, divs)
  127. return new_points_h[:, :-1] / divs[:, None]
  128. def _append_homogeneous_dim(points):
  129. """Append a column of ones to the right of `points`.
  130. This creates the representation of the points in the homogeneous coordinate
  131. space used by homogeneous matrix transforms.
  132. Parameters
  133. ----------
  134. points : array, shape (N, D)
  135. The input coordinates, where N is the number of points and D is the
  136. dimension of the coordinate space.
  137. Returns
  138. -------
  139. points_h : array, shape (N, D+1)
  140. The same points as homogeneous coordinates.
  141. """
  142. return np.hstack((points, np.ones((len(points), 1))))
  143. def _umeyama(src, dst, estimate_scale):
  144. """Estimate N-D similarity transformation with or without scaling.
  145. Parameters
  146. ----------
  147. src : (M, N) array_like
  148. Source coordinates.
  149. dst : (M, N) array_like
  150. Destination coordinates.
  151. estimate_scale : bool
  152. Whether to estimate scaling factor.
  153. Returns
  154. -------
  155. T : (N + 1, N + 1)
  156. The homogeneous similarity transformation matrix. The matrix contains
  157. NaN values only if the problem is not well-conditioned.
  158. References
  159. ----------
  160. .. [1] "Least-squares estimation of transformation parameters between two
  161. point patterns", Shinji Umeyama, PAMI 1991, :DOI:`10.1109/34.88573`
  162. """
  163. src = np.asarray(src)
  164. dst = np.asarray(dst)
  165. num = src.shape[0]
  166. dim = src.shape[1]
  167. # Compute mean of src and dst.
  168. src_mean = src.mean(axis=0)
  169. dst_mean = dst.mean(axis=0)
  170. # Subtract mean from src and dst.
  171. src_demean = src - src_mean
  172. dst_demean = dst - dst_mean
  173. # Eq. (38).
  174. A = dst_demean.T @ src_demean / num
  175. # Eq. (39).
  176. d = np.ones((dim,), dtype=np.float64)
  177. if np.linalg.det(A) < 0:
  178. d[dim - 1] = -1
  179. T = np.eye(dim + 1, dtype=np.float64)
  180. U, S, V = np.linalg.svd(A)
  181. # Eq. (40) and (43).
  182. # Matrix rank calculation from SVD (see numpy.linalg._linalg::matrix_rank code).
  183. # (this does SVD to check for small singular values, replicated here).
  184. tol = S.max() * np.max(A.shape) * np.finfo(float).eps
  185. rank = np.count_nonzero(S > tol)
  186. if rank == 0:
  187. return np.nan * T
  188. elif rank == dim - 1:
  189. if np.linalg.det(U) * np.linalg.det(V) > 0:
  190. T[:dim, :dim] = U @ V
  191. else:
  192. s = d[dim - 1]
  193. d[dim - 1] = -1
  194. T[:dim, :dim] = U @ np.diag(d) @ V
  195. d[dim - 1] = s
  196. else:
  197. T[:dim, :dim] = U @ np.diag(d) @ V
  198. if estimate_scale:
  199. # Eq. (41) and (42).
  200. scale = 1.0 / src_demean.var(axis=0).sum() * (S @ d)
  201. else:
  202. scale = 1.0
  203. T[:dim, dim] = dst_mean - scale * (T[:dim, :dim] @ src_mean.T)
  204. T[:dim, :dim] *= scale
  205. return T
  206. class _GeometricTransform(ABC):
  207. """Abstract base class for geometric transformations."""
  208. @abstractmethod
  209. def __call__(self, coords):
  210. """Apply forward transformation.
  211. Parameters
  212. ----------
  213. coords : (N, 2) array_like
  214. Source coordinates.
  215. Returns
  216. -------
  217. coords : (N, 2) array
  218. Destination coordinates.
  219. """
  220. @property
  221. @abstractmethod
  222. def inverse(self):
  223. """Return a transform object representing the inverse."""
  224. def residuals(self, src, dst):
  225. """Determine residuals of transformed destination coordinates.
  226. For each transformed source coordinate the Euclidean distance to the
  227. respective destination coordinate is determined.
  228. Parameters
  229. ----------
  230. src : (N, 2) array
  231. Source coordinates.
  232. dst : (N, 2) array
  233. Destination coordinates.
  234. Returns
  235. -------
  236. residuals : (N,) array
  237. Residual for coordinate.
  238. """
  239. return np.sqrt(np.sum((self(src) - dst) ** 2, axis=1))
  240. @classmethod
  241. @abstractmethod
  242. def identity(cls, dimensionality=None):
  243. """Identity transform
  244. Parameters
  245. ----------
  246. dimensionality : {None, 2}, optional
  247. This transform only allows dimensionality of 2, where None
  248. corresponds to 2. The parameter exists for compatibility with other
  249. transforms.
  250. Returns
  251. -------
  252. tform : transform
  253. Transform such that ``np.all(tform(pts) == pts)``.
  254. """
  255. @classmethod
  256. def _prepare_estimation(cls, src, dst):
  257. """Create identity transform and make sure points are arrays."""
  258. src = np.asarray(src)
  259. dst = np.asarray(dst)
  260. return cls.identity(src.shape[1]), src, dst
  261. @classmethod
  262. def from_estimate(cls, src, dst, *args, **kwargs) -> Self | FailedEstimation:
  263. r"""Estimate transform.
  264. Parameters
  265. ----------
  266. src : (N, M) array_like
  267. Source coordinates.
  268. dst : (N, M) array_like
  269. Destination coordinates.
  270. \*args : sequence
  271. Any other positional arguments.
  272. \*\*kwargs : dict
  273. Any other keyword arguments.
  274. Returns
  275. -------
  276. tf : Self or ``FailedEstimation``
  277. An instance of the transformation if the estimation succeeded.
  278. Otherwise, we return a special ``FailedEstimation`` object to
  279. signal a failed estimation. Testing the truth value of the failed
  280. estimation object will return ``False``. E.g.
  281. .. code-block:: python
  282. tf = TransformClass.from_estimate(...)
  283. if not tf:
  284. raise RuntimeError(f"Failed estimation: {tf}")
  285. """
  286. return _from_estimate(cls, src, dst, *args, **kwargs)
  287. def _from_estimate(cls, src, dst, *args, **kwargs):
  288. """Detached function for from_estimate base implementation."""
  289. tf, src, dst = cls._prepare_estimation(src, dst)
  290. msg = tf._estimate(src, dst, *args, **kwargs)
  291. return tf if msg is None else FailedEstimation(f'{cls.__name__}: {msg}')
  292. class _HMatrixTransform(_GeometricTransform):
  293. """Transform accepting homogeneous matrix as input."""
  294. def __init__(self, matrix=None, *, dimensionality=None):
  295. if matrix is None:
  296. d = 2 if dimensionality is None else dimensionality
  297. matrix = np.eye(d + 1)
  298. else:
  299. matrix = np.asarray(matrix)
  300. self._check_matrix(matrix, dimensionality)
  301. self._check_dims(matrix.shape[0] - 1)
  302. self.params = matrix
  303. def _check_matrix(self, matrix, dimensionality):
  304. if dimensionality is not None:
  305. if dimensionality != matrix.shape[0] - 1:
  306. raise ValueError(
  307. f'Dimensionality {dimensionality} does not match matrix '
  308. f'{matrix}'
  309. )
  310. m = matrix.shape[0]
  311. if matrix.shape != (m, m):
  312. raise ValueError("Invalid shape of transformation matrix")
  313. def _check_dims(self, d):
  314. if d == 2:
  315. return
  316. raise NotImplementedError(
  317. f'Input for {type(self)} should result in 2D transform'
  318. )
  319. @classmethod
  320. def identity(cls, dimensionality=None):
  321. """Identity transform
  322. Parameters
  323. ----------
  324. dimensionality : {None, 2}, optional
  325. This transform only allows dimensionality of 2, where None
  326. corresponds to 2. The parameter exists for compatibility with other
  327. transforms.
  328. Returns
  329. -------
  330. tform : transform
  331. Transform such that ``np.all(tform(pts) == pts)``.
  332. """
  333. d = 2 if dimensionality is None else dimensionality
  334. return cls(matrix=np.eye(d + 1))
  335. @property
  336. def dimensionality(self):
  337. return self.matrix.shape[0] - 1
  338. class FundamentalMatrixTransform(_HMatrixTransform):
  339. """Fundamental matrix transformation.
  340. The fundamental matrix relates corresponding points between a pair of
  341. uncalibrated images. The matrix transforms homogeneous image points in one
  342. image to epipolar lines in the other image.
  343. The fundamental matrix is only defined for a pair of moving images. In the
  344. case of pure rotation or planar scenes, the homography describes the
  345. geometric relation between two images (`ProjectiveTransform`). If the
  346. intrinsic calibration of the images is known, the essential matrix describes
  347. the metric relation between the two images (`EssentialMatrixTransform`).
  348. Notes
  349. -----
  350. See [1]_ and [2]_ for details of the estimation procedure. [2]_ is a good
  351. place to start.
  352. References
  353. ----------
  354. .. [1] Hartley, Richard, and Andrew Zisserman. Multiple view geometry in
  355. computer vision. Cambridge university press, 2003.
  356. .. [2] Zhang, Zhengyou. "Determining the epipolar geometry and its
  357. uncertainty: A review." International journal of computer vision 27
  358. (1998): 161-195.
  359. :DOI:`10.1023/A:1007941100561`
  360. https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/RR-2927.pdf
  361. Parameters
  362. ----------
  363. matrix : (3, 3) array_like, optional
  364. Fundamental matrix.
  365. dimensionality : int, optional
  366. Fallback number of dimensions when `matrix` not specified, in which
  367. case, must equal 2 (the default).
  368. Attributes
  369. ----------
  370. params : (3, 3) array
  371. Fundamental matrix.
  372. Examples
  373. --------
  374. >>> import numpy as np
  375. >>> import skimage as ski
  376. Define source and destination points:
  377. >>> src = np.array([1.839035, 1.924743,
  378. ... 0.543582, 0.375221,
  379. ... 0.473240, 0.142522,
  380. ... 0.964910, 0.598376,
  381. ... 0.102388, 0.140092,
  382. ... 15.994343, 9.622164,
  383. ... 0.285901, 0.430055,
  384. ... 0.091150, 0.254594]).reshape(-1, 2)
  385. >>> dst = np.array([1.002114, 1.129644,
  386. ... 1.521742, 1.846002,
  387. ... 1.084332, 0.275134,
  388. ... 0.293328, 0.588992,
  389. ... 0.839509, 0.087290,
  390. ... 1.779735, 1.116857,
  391. ... 0.878616, 0.602447,
  392. ... 0.642616, 1.028681]).reshape(-1, 2)
  393. Estimate the transformation matrix:
  394. >>> tform = ski.transform.FundamentalMatrixTransform.from_estimate(
  395. ... src, dst)
  396. >>> tform.params
  397. array([[-0.21785884, 0.41928191, -0.03430748],
  398. [-0.07179414, 0.04516432, 0.02160726],
  399. [ 0.24806211, -0.42947814, 0.02210191]])
  400. Compute the Sampson distance:
  401. >>> tform.residuals(src, dst)
  402. array([0.0053886 , 0.00526101, 0.08689701, 0.01850534, 0.09418259,
  403. 0.00185967, 0.06160489, 0.02655136])
  404. Apply inverse transformation:
  405. >>> tform.inverse(dst)
  406. array([[-0.0513591 , 0.04170974, 0.01213043],
  407. [-0.21599496, 0.29193419, 0.00978184],
  408. [-0.0079222 , 0.03758889, -0.00915389],
  409. [ 0.14187184, -0.27988959, 0.02476507],
  410. [ 0.05890075, -0.07354481, -0.00481342],
  411. [-0.21985267, 0.36717464, -0.01482408],
  412. [ 0.01339569, -0.03388123, 0.00497605],
  413. [ 0.03420927, -0.1135812 , 0.02228236]])
  414. The estimation can fail - for example, if all the input or output points
  415. are the same. If this happens, you will get a transform that is not
  416. "truthy" - meaning that ``bool(tform)`` is ``False``:
  417. >>> # A successfully estimated model is truthy (applying ``bool()``
  418. >>> # gives ``True``):
  419. >>> if tform:
  420. ... print("Estimation succeeded.")
  421. Estimation succeeded.
  422. >>> # Not so for a degenerate transform with identical points.
  423. >>> bad_src = np.ones((8, 2))
  424. >>> bad_tform = ski.transform.FundamentalMatrixTransform.from_estimate(
  425. ... bad_src, dst)
  426. >>> if not bad_tform:
  427. ... print("Estimation failed.")
  428. Estimation failed.
  429. Trying to use this failed estimation transform result will give a suitable
  430. error:
  431. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  432. Traceback (most recent call last):
  433. ...
  434. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  435. """
  436. scaling = 'rms'
  437. def __call__(self, coords):
  438. """Apply forward transformation.
  439. Parameters
  440. ----------
  441. coords : (N, 2) array_like
  442. Source coordinates.
  443. Returns
  444. -------
  445. coords : (N, 3) array
  446. Epipolar lines in the destination image.
  447. """
  448. return _append_homogeneous_dim(coords) @ self.params.T
  449. @property
  450. def inverse(self):
  451. """Return a transform object representing the inverse.
  452. See Hartley & Zisserman, Ch. 8: Epipolar Geometry and the Fundamental
  453. Matrix, for an explanation of why F.T gives the inverse.
  454. """
  455. return type(self)(matrix=self.params.T)
  456. def _setup_constraint_matrix(self, src, dst):
  457. """Setup and solve the homogeneous epipolar constraint matrix::
  458. dst' * F * src = 0.
  459. Parameters
  460. ----------
  461. src : (N, 2) array_like
  462. Source coordinates.
  463. dst : (N, 2) array_like
  464. Destination coordinates.
  465. Returns
  466. -------
  467. F_normalized : (3, 3) array
  468. The normalized solution to the homogeneous system. If the system
  469. is not well-conditioned, this matrix contains NaNs.
  470. src_matrix : (3, 3) array
  471. The transformation matrix to obtain the normalized source
  472. coordinates.
  473. dst_matrix : (3, 3) array
  474. The transformation matrix to obtain the normalized destination
  475. coordinates.
  476. """
  477. src = np.asarray(src)
  478. dst = np.asarray(dst)
  479. if src.shape != dst.shape:
  480. raise ValueError('src and dst shapes must be identical.')
  481. if src.shape[0] < 8:
  482. raise ValueError('src.shape[0] must be equal or larger than 8.')
  483. # Center and normalize image points for better numerical stability.
  484. src_matrix = _calc_center_normalize(src, self.scaling)
  485. dst_matrix = _calc_center_normalize(dst, self.scaling)
  486. if np.any(np.isnan(src_matrix + dst_matrix)):
  487. self.params = np.full((3, 3), np.nan)
  488. return 3 * [np.full((3, 3), np.nan)]
  489. src_h = _append_homogeneous_dim(_apply_homogeneous(src_matrix, src))
  490. dst_h = _append_homogeneous_dim(_apply_homogeneous(dst_matrix, dst))
  491. # Setup homogeneous linear equation as dst' * F * src = 0.
  492. # Hartley notation u -> src[:, 0], v -> src[:, 1],
  493. # u' -> dst[:, 0], v' -> dst[:, 1]. Required output cols are:
  494. # uu', vu', u', uv', vv', v', u, v, 1
  495. cols = [(d_v * s_v) for d_v in dst_h.T for s_v in src_h.T]
  496. A = np.stack(cols, axis=1)
  497. # Solve for the nullspace of the constraint matrix.
  498. _, _, V = np.linalg.svd(A)
  499. F_normalized = V[-1, :].reshape(3, 3)
  500. return F_normalized, src_matrix, dst_matrix
  501. @classmethod
  502. def from_estimate(cls, src, dst):
  503. """Estimate fundamental matrix using 8-point algorithm.
  504. The 8-point algorithm requires at least 8 corresponding point pairs.
  505. Parameters
  506. ----------
  507. src : (N, 2) array_like
  508. Source coordinates.
  509. dst : (N, 2) array_like
  510. Destination coordinates.
  511. Returns
  512. -------
  513. tf : Self or ``FailedEstimation``
  514. An instance of the transformation if the estimation succeeded.
  515. Otherwise, we return a special ``FailedEstimation`` object to
  516. signal a failed estimation. Testing the truth value of the failed
  517. estimation object will return ``False``. E.g.
  518. .. code-block:: python
  519. tf = FundamentalMatrixTransform.from_estimate(...)
  520. if not tf:
  521. raise RuntimeError(f"Failed estimation: {tf}")
  522. Raises
  523. ------
  524. ValueError
  525. If `src` has fewer than 8 rows.
  526. """
  527. return super().from_estimate(src, dst)
  528. def _estimate(self, src, dst):
  529. F_normalized, src_matrix, dst_matrix = self._setup_constraint_matrix(src, dst)
  530. if np.any(np.isnan(F_normalized + src_matrix + dst_matrix)):
  531. return 'Scaling failed for input points'
  532. # Enforcing the internal constraint that two singular values must be
  533. # non-zero and one must be zero (rank 2).
  534. U, S, V = np.linalg.svd(F_normalized)
  535. S[2] = 0
  536. F = U @ np.diag(S) @ V
  537. self.params = dst_matrix.T @ F @ src_matrix
  538. return None
  539. def residuals(self, src, dst):
  540. """Compute the Sampson distance.
  541. The Sampson distance is the first approximation to the geometric error.
  542. Parameters
  543. ----------
  544. src : (N, 2) array
  545. Source coordinates.
  546. dst : (N, 2) array
  547. Destination coordinates.
  548. Returns
  549. -------
  550. residuals : (N,) array
  551. Sampson distance.
  552. """
  553. src_homogeneous = _append_homogeneous_dim(src)
  554. dst_homogeneous = _append_homogeneous_dim(dst)
  555. F_src = self.params @ src_homogeneous.T
  556. Ft_dst = self.params.T @ dst_homogeneous.T
  557. dst_F_src = np.sum(dst_homogeneous * F_src.T, axis=1)
  558. return np.abs(dst_F_src) / np.sqrt(
  559. F_src[0] ** 2 + F_src[1] ** 2 + Ft_dst[0] ** 2 + Ft_dst[1] ** 2
  560. )
  561. @_deprecate_estimate
  562. def estimate(self, src, dst):
  563. """Estimate fundamental matrix using 8-point algorithm.
  564. The 8-point algorithm requires at least 8 corresponding point pairs for
  565. a well-conditioned solution, otherwise the over-determined solution is
  566. estimated.
  567. Parameters
  568. ----------
  569. src : (N, 2) array_like
  570. Source coordinates.
  571. dst : (N, 2) array_like
  572. Destination coordinates.
  573. Returns
  574. -------
  575. success : bool
  576. True, if model estimation succeeds.
  577. """
  578. return self._estimate(src, dst) is None
  579. class EssentialMatrixTransform(FundamentalMatrixTransform):
  580. """Essential matrix transformation.
  581. The essential matrix relates corresponding points between a pair of
  582. calibrated images. The matrix transforms normalized, homogeneous image
  583. points in one image to epipolar lines in the other image.
  584. The essential matrix is only defined for a pair of moving images capturing a
  585. non-planar scene. In the case of pure rotation or planar scenes, the
  586. homography describes the geometric relation between two images
  587. (`ProjectiveTransform`). If the intrinsic calibration of the images is
  588. unknown, the fundamental matrix describes the projective relation between
  589. the two images (`FundamentalMatrixTransform`).
  590. References
  591. ----------
  592. .. [1] Hartley, Richard, and Andrew Zisserman. Multiple view geometry in
  593. computer vision. Cambridge university press, 2003.
  594. Parameters
  595. ----------
  596. rotation : (3, 3) array_like, optional
  597. Rotation matrix of the relative camera motion.
  598. translation : (3, 1) array_like, optional
  599. Translation vector of the relative camera motion. The vector must
  600. have unit length.
  601. matrix : (3, 3) array_like, optional
  602. Essential matrix.
  603. dimensionality : int, optional
  604. Fallback number of dimensions when `matrix` not specified, in which
  605. case, must equal 2 (the default).
  606. Attributes
  607. ----------
  608. params : (3, 3) array
  609. Essential matrix.
  610. Examples
  611. --------
  612. >>> import numpy as np
  613. >>> import skimage as ski
  614. >>>
  615. >>> tform = ski.transform.EssentialMatrixTransform(
  616. ... rotation=np.eye(3), translation=np.array([0, 0, 1])
  617. ... )
  618. >>> tform.params
  619. array([[ 0., -1., 0.],
  620. [ 1., 0., 0.],
  621. [ 0., 0., 0.]])
  622. >>> src = np.array([[ 1.839035, 1.924743],
  623. ... [ 0.543582, 0.375221],
  624. ... [ 0.47324 , 0.142522],
  625. ... [ 0.96491 , 0.598376],
  626. ... [ 0.102388, 0.140092],
  627. ... [15.994343, 9.622164],
  628. ... [ 0.285901, 0.430055],
  629. ... [ 0.09115 , 0.254594]])
  630. >>> dst = np.array([[1.002114, 1.129644],
  631. ... [1.521742, 1.846002],
  632. ... [1.084332, 0.275134],
  633. ... [0.293328, 0.588992],
  634. ... [0.839509, 0.08729 ],
  635. ... [1.779735, 1.116857],
  636. ... [0.878616, 0.602447],
  637. ... [0.642616, 1.028681]])
  638. >>> tform = ski.transform.EssentialMatrixTransform.from_estimate(src, dst)
  639. >>> tform.residuals(src, dst)
  640. array([0.42455187, 0.01460448, 0.13847034, 0.12140951, 0.27759346,
  641. 0.32453118, 0.00210776, 0.26512283])
  642. The estimation can fail - for example, if all the input or output points
  643. are the same. If this happens, you will get a transform that is not
  644. "truthy" - meaning that ``bool(tform)`` is ``False``:
  645. >>> # A successfully estimated model is truthy (applying ``bool()``
  646. >>> # gives ``True``):
  647. >>> if tform:
  648. ... print("Estimation succeeded.")
  649. Estimation succeeded.
  650. >>> # Not so for a degenerate transform with identical points.
  651. >>> bad_src = np.ones((8, 2))
  652. >>> bad_tform = ski.transform.EssentialMatrixTransform.from_estimate(
  653. ... bad_src, dst)
  654. >>> if not bad_tform:
  655. ... print("Estimation failed.")
  656. Estimation failed.
  657. Trying to use this failed estimation transform result will give a suitable
  658. error:
  659. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  660. Traceback (most recent call last):
  661. ...
  662. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  663. """
  664. # Threshold for determinant of rotation matrix.
  665. _rot_det_tol = 1e-6
  666. # Threshold for difference of translation vector from unit length.
  667. _trans_len_tol = 1e-6
  668. def __init__(
  669. self, *, rotation=None, translation=None, matrix=None, dimensionality=None
  670. ):
  671. n_rt_none = sum(p is None for p in (rotation, translation))
  672. if n_rt_none == 1:
  673. raise ValueError(
  674. "Both rotation and translation required when one is specified."
  675. )
  676. elif n_rt_none == 0:
  677. if matrix is not None:
  678. raise ValueError(
  679. "Do not specify rotation or translation when "
  680. "matrix is specified."
  681. )
  682. matrix = self._rt2matrix(rotation, translation)
  683. super().__init__(matrix=matrix, dimensionality=dimensionality)
  684. def _rt2matrix(self, rotation, translation):
  685. rotation = np.asarray(rotation)
  686. translation = np.asarray(translation)
  687. if rotation.shape != (3, 3):
  688. raise ValueError("Invalid shape of rotation matrix")
  689. if abs(np.linalg.det(rotation) - 1) > self._rot_det_tol:
  690. raise ValueError("Rotation matrix must have unit determinant")
  691. if translation.size != 3:
  692. raise ValueError("Invalid shape of translation vector")
  693. if abs(np.linalg.norm(translation) - 1) > self._trans_len_tol:
  694. raise ValueError("Translation vector must have unit length")
  695. # Matrix representation of the cross product for t.
  696. t0, t1, t2 = translation
  697. t_arr = np.array([[0, -t2, t1], [t2, 0, -t0], [-t1, t0, 0]], dtype=float)
  698. return t_arr @ rotation
  699. @classmethod
  700. def from_estimate(cls, src, dst):
  701. """Estimate essential matrix using 8-point algorithm.
  702. The 8-point algorithm requires at least 8 corresponding point pairs for
  703. a well-conditioned solution, otherwise the over-determined solution is
  704. estimated.
  705. Parameters
  706. ----------
  707. src : (N, 2) array_like
  708. Source coordinates.
  709. dst : (N, 2) array_like
  710. Destination coordinates.
  711. Returns
  712. -------
  713. tf : Self or ``FailedEstimation``
  714. An instance of the transformation if the estimation succeeded.
  715. Otherwise, we return a special ``FailedEstimation`` object to
  716. signal a failed estimation. Testing the truth value of the failed
  717. estimation object will return ``False``. E.g.
  718. .. code-block:: python
  719. tf = EssentialMatrixTransform.from_estimate(...)
  720. if not tf:
  721. raise RuntimeError(f"Failed estimation: {tf}")
  722. Raises
  723. ------
  724. ValueError
  725. If `src` has fewer than 8 rows.
  726. """
  727. return super().from_estimate(src, dst)
  728. def _estimate(self, src, dst):
  729. E_normalized, src_matrix, dst_matrix = self._setup_constraint_matrix(src, dst)
  730. if np.any(np.isnan(E_normalized + src_matrix + dst_matrix)):
  731. return 'Scaling failed for input points'
  732. # Enforcing the internal constraint that two singular values must be
  733. # equal and one must be zero.
  734. U, S, V = np.linalg.svd(E_normalized)
  735. S[0] = (S[0] + S[1]) / 2.0
  736. S[1] = S[0]
  737. S[2] = 0
  738. E = U @ np.diag(S) @ V
  739. self.params = dst_matrix.T @ E @ src_matrix
  740. return None
  741. @_deprecate_estimate
  742. def estimate(self, src, dst):
  743. """Estimate essential matrix using 8-point algorithm.
  744. The 8-point algorithm requires at least 8 corresponding point pairs for
  745. a well-conditioned solution, otherwise the over-determined solution is
  746. estimated.
  747. Parameters
  748. ----------
  749. src : (N, 2) array_like
  750. Source coordinates.
  751. dst : (N, 2) array_like
  752. Destination coordinates.
  753. Returns
  754. -------
  755. success : bool
  756. True, if model estimation succeeds.
  757. """
  758. return self._estimate(src, dst) is None
  759. class ProjectiveTransform(_HMatrixTransform):
  760. r"""Projective transformation.
  761. Apply a projective transformation (homography) on coordinates.
  762. For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its
  763. target position is calculated by multiplying with the given matrix,
  764. :math:`H`, to give :math:`H \mathbf{x}`::
  765. [[a0 a1 a2]
  766. [b0 b1 b2]
  767. [c0 c1 1 ]].
  768. E.g., to rotate by theta degrees clockwise, the matrix should be::
  769. [[cos(theta) -sin(theta) 0]
  770. [sin(theta) cos(theta) 0]
  771. [0 0 1]]
  772. or, to translate x by 10 and y by 20::
  773. [[1 0 10]
  774. [0 1 20]
  775. [0 0 1 ]].
  776. Parameters
  777. ----------
  778. matrix : (D+1, D+1) array_like, optional
  779. Homogeneous transformation matrix.
  780. dimensionality : int, optional
  781. Fallback number of dimensions when `matrix` not specified.
  782. Attributes
  783. ----------
  784. params : (D+1, D+1) array
  785. Homogeneous transformation matrix.
  786. Examples
  787. --------
  788. >>> import numpy as np
  789. >>> import skimage as ski
  790. Define a transform with an homogeneous transformation matrix:
  791. >>> tform = ski.transform.ProjectiveTransform(np.diag([2., 3., 1.]))
  792. >>> tform.params
  793. array([[2., 0., 0.],
  794. [0., 3., 0.],
  795. [0., 0., 1.]])
  796. You can estimate a transformation to map between source and destination
  797. points:
  798. >>> src = np.array([[150, 150],
  799. ... [250, 100],
  800. ... [150, 200]])
  801. >>> dst = np.array([[200, 200],
  802. ... [300, 150],
  803. ... [150, 400]])
  804. >>> tform = ski.transform.ProjectiveTransform.from_estimate(src, dst)
  805. >>> np.allclose(tform.params, [[ -16.56, 5.82, 895.81],
  806. ... [ -10.31, -8.29, 2075.43],
  807. ... [ -0.05, 0.02, 1. ]], atol=0.01)
  808. True
  809. Apply the transformation to some image data.
  810. >>> img = ski.data.astronaut()
  811. >>> warped = ski.transform.warp(img, inverse_map=tform.inverse)
  812. The estimation can fail - for example, if all the input or output points
  813. are the same. If this happens, you will get a transform that is not
  814. "truthy" - meaning that ``bool(tform)`` is ``False``:
  815. >>> # A successfully estimated model is truthy (applying ``bool()``
  816. >>> # gives ``True``):
  817. >>> if tform:
  818. ... print("Estimation succeeded.")
  819. Estimation succeeded.
  820. >>> # Not so for a degenerate transform with identical points.
  821. >>> bad_src = np.ones((3, 2))
  822. >>> bad_tform = ski.transform.ProjectiveTransform.from_estimate(
  823. ... bad_src, dst)
  824. >>> if not bad_tform:
  825. ... print("Estimation failed.")
  826. Estimation failed.
  827. Trying to use this failed estimation transform result will give a suitable
  828. error:
  829. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  830. Traceback (most recent call last):
  831. ...
  832. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  833. """
  834. scaling = 'rms'
  835. @property
  836. def _coeff_inds(self):
  837. """Indices into flat ``self.params`` with coefficients to estimate"""
  838. return range(self.params.size - 1)
  839. def _check_dims(self, d):
  840. if d >= 2:
  841. return
  842. raise NotImplementedError(
  843. f'Input for {type(self)} should result in transform of >=2D'
  844. )
  845. @property
  846. def _inv_matrix(self):
  847. return np.linalg.inv(self.params)
  848. def __array__(self, dtype=None, copy=None):
  849. return self.params if dtype is None else self.params.astype(dtype)
  850. def __call__(self, coords):
  851. """Apply forward transformation.
  852. Parameters
  853. ----------
  854. coords : (N, D) array_like
  855. Source coordinates.
  856. Returns
  857. -------
  858. coords_out : (N, D) array
  859. Destination coordinates.
  860. """
  861. return _apply_homogeneous(self.params, coords)
  862. @property
  863. def inverse(self):
  864. """Return a transform object representing the inverse."""
  865. return type(self)(matrix=self._inv_matrix)
  866. @classmethod
  867. def from_estimate(cls, src, dst, weights=None):
  868. """Estimate the transformation from a set of corresponding points.
  869. You can determine the over-, well- and under-determined parameters
  870. with the total least-squares method.
  871. Number of source and destination coordinates must match.
  872. The transformation is defined as::
  873. X = (a0*x + a1*y + a2) / (c0*x + c1*y + 1)
  874. Y = (b0*x + b1*y + b2) / (c0*x + c1*y + 1)
  875. These equations can be transformed to the following form::
  876. 0 = a0*x + a1*y + a2 - c0*x*X - c1*y*X - X
  877. 0 = b0*x + b1*y + b2 - c0*x*Y - c1*y*Y - Y
  878. which exist for each set of corresponding points, so we have a set of
  879. N * 2 equations. The coefficients appear linearly so we can write
  880. A x = 0, where::
  881. A = [[x y 1 0 0 0 -x*X -y*X -X]
  882. [0 0 0 x y 1 -x*Y -y*Y -Y]
  883. ...
  884. ...
  885. ]
  886. x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3]
  887. In case of total least-squares the solution of this homogeneous system
  888. of equations is the right singular vector of A which corresponds to the
  889. smallest singular value normed by the coefficient c3.
  890. Weights can be applied to each pair of corresponding points to
  891. indicate, particularly in an overdetermined system, if point pairs have
  892. higher or lower confidence or uncertainties associated with them. From
  893. the matrix treatment of least squares problems, these weight values are
  894. normalized, square-rooted, then built into a diagonal matrix, by which
  895. A is multiplied.
  896. In case of the affine transformation the coefficients c0 and c1 are 0.
  897. Thus the system of equations is::
  898. A = [[x y 1 0 0 0 -X]
  899. [0 0 0 x y 1 -Y]
  900. ...
  901. ...
  902. ]
  903. x.T = [a0 a1 a2 b0 b1 b2 c3]
  904. Parameters
  905. ----------
  906. src : (N, 2) array_like
  907. Source coordinates.
  908. dst : (N, 2) array_like
  909. Destination coordinates.
  910. weights : (N,) array_like, optional
  911. Relative weight values for each pair of points.
  912. Returns
  913. -------
  914. tf : Self or ``FailedEstimation``
  915. An instance of the transformation if the estimation succeeded.
  916. Otherwise, we return a special ``FailedEstimation`` object to
  917. signal a failed estimation. Testing the truth value of the failed
  918. estimation object will return ``False``. E.g.
  919. .. code-block:: python
  920. tf = ProjectiveTransform.from_estimate(...)
  921. if not tf:
  922. raise RuntimeError(f"Failed estimation: {tf}")
  923. """
  924. return super().from_estimate(src, dst, weights)
  925. def _estimate(self, src, dst, weights=None):
  926. src = np.asarray(src)
  927. dst = np.asarray(dst)
  928. n, d = src.shape
  929. fail_matrix = np.full((d + 1, d + 1), np.nan)
  930. src_matrix, src = _center_and_normalize_points(src)
  931. dst_matrix, dst = _center_and_normalize_points(dst)
  932. if not np.all(np.isfinite(src_matrix + dst_matrix)):
  933. self.params = fail_matrix
  934. return 'Scaling generated NaN values'
  935. # params: a0, a1, a2, b0, b1, b2, c0, c1
  936. A = np.zeros((n * d, (d + 1) ** 2))
  937. # fill the A matrix with the appropriate block matrices; see docstring
  938. # for 2D example — this can be generalised to more blocks in the 3D and
  939. # higher-dimensional cases.
  940. for ddim in range(d):
  941. A[ddim * n : (ddim + 1) * n, ddim * (d + 1) : ddim * (d + 1) + d] = src
  942. A[ddim * n : (ddim + 1) * n, ddim * (d + 1) + d] = 1
  943. A[ddim * n : (ddim + 1) * n, -d - 1 : -1] = src
  944. A[ddim * n : (ddim + 1) * n, -1] = -1
  945. A[ddim * n : (ddim + 1) * n, -d - 1 :] *= -dst[:, ddim : (ddim + 1)]
  946. # Select relevant columns, depending on params
  947. A = A[:, list(self._coeff_inds) + [-1]]
  948. # Get the vectors that correspond to singular values, also applying
  949. # the weighting if provided
  950. if weights is None:
  951. _, _, V = np.linalg.svd(A)
  952. else:
  953. weights = np.asarray(weights)
  954. W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), d))
  955. _, _, V = np.linalg.svd(W @ A)
  956. H = np.zeros((d + 1, d + 1))
  957. # Solution is right singular vector that corresponds to smallest
  958. # singular value.
  959. if np.isclose(V[-1, -1], 0):
  960. self.params = fail_matrix
  961. return 'Right singular vector has 0 final element'
  962. H.flat[list(self._coeff_inds) + [-1]] = -V[-1, :-1] / V[-1, -1]
  963. H[d, d] = 1
  964. # De-center and de-normalize
  965. H = np.linalg.inv(dst_matrix) @ H @ src_matrix
  966. # Small errors can creep in if points are not exact, causing the last
  967. # element of H to deviate from unity. Correct for that here.
  968. H /= H[-1, -1]
  969. self.params = H
  970. return None
  971. def __add__(self, other):
  972. """Combine this transformation with another."""
  973. if isinstance(other, ProjectiveTransform):
  974. # combination of the same types result in a transformation of this
  975. # type again, otherwise use general projective transformation
  976. if type(self) == type(other):
  977. tform = self.__class__
  978. else:
  979. tform = ProjectiveTransform
  980. return tform(other.params @ self.params)
  981. else:
  982. raise TypeError("Cannot combine transformations of differing " "types.")
  983. def __nice__(self):
  984. """common 'paramstr' used by __str__ and __repr__"""
  985. if not hasattr(self, 'params'):
  986. return '<not yet initialized>'
  987. npstring = np.array2string(self.params, separator=', ')
  988. return 'matrix=\n' + textwrap.indent(npstring, ' ')
  989. def __repr__(self):
  990. """Add standard repr formatting around a __nice__ string"""
  991. return f'<{type(self).__name__}({self.__nice__()}) at {hex(id(self))}>'
  992. def __str__(self):
  993. """Add standard str formatting around a __nice__ string"""
  994. return f'<{type(self).__name__}({self.__nice__()})>'
  995. @property
  996. def dimensionality(self):
  997. """The dimensionality of the transformation."""
  998. return self.params.shape[0] - 1
  999. @classmethod
  1000. def identity(cls, dimensionality=None):
  1001. """Identity transform
  1002. Parameters
  1003. ----------
  1004. dimensionality : {None, int}, optional
  1005. Dimensionality of identity transform.
  1006. Returns
  1007. -------
  1008. tform : transform
  1009. Transform such that ``np.all(tform(pts) == pts)``.
  1010. """
  1011. return super().identity(dimensionality=dimensionality)
  1012. @_deprecate_estimate
  1013. def estimate(self, src, dst, weights=None):
  1014. """Estimate the transformation from a set of corresponding points.
  1015. You can determine the over-, well- and under-determined parameters
  1016. with the total least-squares method.
  1017. Number of source and destination coordinates must match.
  1018. The transformation is defined as::
  1019. X = (a0*x + a1*y + a2) / (c0*x + c1*y + 1)
  1020. Y = (b0*x + b1*y + b2) / (c0*x + c1*y + 1)
  1021. These equations can be transformed to the following form::
  1022. 0 = a0*x + a1*y + a2 - c0*x*X - c1*y*X - X
  1023. 0 = b0*x + b1*y + b2 - c0*x*Y - c1*y*Y - Y
  1024. which exist for each set of corresponding points, so we have a set of
  1025. N * 2 equations. The coefficients appear linearly so we can write
  1026. A x = 0, where::
  1027. A = [[x y 1 0 0 0 -x*X -y*X -X]
  1028. [0 0 0 x y 1 -x*Y -y*Y -Y]
  1029. ...
  1030. ...
  1031. ]
  1032. x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3]
  1033. In case of total least-squares the solution of this homogeneous system
  1034. of equations is the right singular vector of A which corresponds to the
  1035. smallest singular value normed by the coefficient c3.
  1036. Weights can be applied to each pair of corresponding points to
  1037. indicate, particularly in an overdetermined system, if point pairs have
  1038. higher or lower confidence or uncertainties associated with them. From
  1039. the matrix treatment of least squares problems, these weight values are
  1040. normalized, square-rooted, then built into a diagonal matrix, by which
  1041. A is multiplied.
  1042. In case of the affine transformation the coefficients c0 and c1 are 0.
  1043. Thus the system of equations is::
  1044. A = [[x y 1 0 0 0 -X]
  1045. [0 0 0 x y 1 -Y]
  1046. ...
  1047. ...
  1048. ]
  1049. x.T = [a0 a1 a2 b0 b1 b2 c3]
  1050. Parameters
  1051. ----------
  1052. src : (N, 2) array_like
  1053. Source coordinates.
  1054. dst : (N, 2) array_like
  1055. Destination coordinates.
  1056. weights : (N,) array_like, optional
  1057. Relative weight values for each pair of points.
  1058. Returns
  1059. -------
  1060. success : bool
  1061. True, if model estimation succeeds.
  1062. """
  1063. return self._estimate(src, dst, weights) is None
  1064. @_update_from_estimate_docstring
  1065. @_deprecate_inherited_estimate
  1066. class AffineTransform(ProjectiveTransform):
  1067. """Affine transformation.
  1068. Has the following form::
  1069. X = a0 * x + a1 * y + a2
  1070. = sx * x * [cos(rotation) + tan(shear_y) * sin(rotation)]
  1071. - sy * y * [tan(shear_x) * cos(rotation) + sin(rotation)]
  1072. + translation_x
  1073. Y = b0 * x + b1 * y + b2
  1074. = sx * x * [sin(rotation) - tan(shear_y) * cos(rotation)]
  1075. - sy * y * [tan(shear_x) * sin(rotation) - cos(rotation)]
  1076. + translation_y
  1077. where ``sx`` and ``sy`` are scale factors in the x and y directions.
  1078. This is equivalent to applying the operations in the following order:
  1079. 1. Scale
  1080. 2. Shear
  1081. 3. Rotate
  1082. 4. Translate
  1083. The homogeneous transformation matrix is::
  1084. [[a0 a1 a2]
  1085. [b0 b1 b2]
  1086. [0 0 1]]
  1087. In 2D, the transformation parameters can be given as the homogeneous
  1088. transformation matrix, above, or as the implicit parameters, scale,
  1089. rotation, shear, and translation in x (a2) and y (b2). For 3D and higher,
  1090. only the matrix form is allowed.
  1091. In narrower transforms, such as the Euclidean (only rotation and
  1092. translation) or Similarity (rotation, translation, and a global scale
  1093. factor) transforms, it is possible to specify 3D transforms using implicit
  1094. parameters also.
  1095. Parameters
  1096. ----------
  1097. matrix : (D+1, D+1) array_like, optional
  1098. Homogeneous transformation matrix. If this matrix is provided, it is an
  1099. error to provide any of scale, rotation, shear, or translation.
  1100. scale : {s as float or (sx, sy) as array, list or tuple}, optional
  1101. Scale factor(s). If a single value, it will be assigned to both
  1102. sx and sy. Only available for 2D.
  1103. .. versionadded:: 0.17
  1104. Added support for supplying a single scalar value.
  1105. shear : float or 2-tuple of float, optional
  1106. The x and y shear angles, clockwise, by which these axes are
  1107. rotated around the origin [2].
  1108. If a single value is given, take that to be the x shear angle, with
  1109. the y angle remaining 0. Only available in 2D.
  1110. rotation : float, optional
  1111. Rotation angle, clockwise, as radians. Only available for 2D.
  1112. translation : (tx, ty) as array, list or tuple, optional
  1113. Translation parameters. Only available for 2D.
  1114. dimensionality : int, optional
  1115. Fallback number of dimensions for transform when none of `matrix`,
  1116. `scale`, `rotation`, `shear` or `translation` are specified. If any of
  1117. `scale`, `rotation`, `shear` or `translation` are specified, must equal
  1118. 2 (the default).
  1119. Attributes
  1120. ----------
  1121. params : (D+1, D+1) array
  1122. Homogeneous transformation matrix.
  1123. Raises
  1124. ------
  1125. ValueError
  1126. If both ``matrix`` and any of the other parameters are provided.
  1127. Examples
  1128. --------
  1129. >>> import numpy as np
  1130. >>> import skimage as ski
  1131. Define a transform with an homogeneous transformation matrix:
  1132. >>> tform = ski.transform.AffineTransform(np.diag([2., 3., 1.]))
  1133. >>> tform.params
  1134. array([[2., 0., 0.],
  1135. [0., 3., 0.],
  1136. [0., 0., 1.]])
  1137. Define a transform with parameters:
  1138. >>> tform = ski.transform.AffineTransform(scale=4, rotation=0.2)
  1139. >>> np.round(tform.params, 2)
  1140. array([[ 3.92, -0.79, 0. ],
  1141. [ 0.79, 3.92, 0. ],
  1142. [ 0. , 0. , 1. ]])
  1143. You can estimate a transformation to map between source and destination
  1144. points:
  1145. >>> src = np.array([[150, 150],
  1146. ... [250, 100],
  1147. ... [150, 200]])
  1148. >>> dst = np.array([[200, 200],
  1149. ... [300, 150],
  1150. ... [150, 400]])
  1151. >>> tform = ski.transform.AffineTransform.from_estimate(src, dst)
  1152. >>> np.allclose(tform.params, [[ 0.5, -1. , 275. ],
  1153. ... [ 1.5, 4. , -625. ],
  1154. ... [ 0. , 0. , 1. ]])
  1155. True
  1156. Apply the transformation to some image data.
  1157. >>> img = ski.data.astronaut()
  1158. >>> warped = ski.transform.warp(img, inverse_map=tform.inverse)
  1159. The estimation can fail - for example, if all the input or output points
  1160. are the same. If this happens, you will get a transform that is not
  1161. "truthy" - meaning that ``bool(tform)`` is ``False``:
  1162. >>> # A successfully estimated model is truthy (applying ``bool()``
  1163. >>> # gives ``True``):
  1164. >>> if tform:
  1165. ... print("Estimation succeeded.")
  1166. Estimation succeeded.
  1167. >>> # Not so for a degenerate transform with identical points.
  1168. >>> bad_src = np.ones((3, 2))
  1169. >>> bad_tform = ski.transform.AffineTransform.from_estimate(
  1170. ... bad_src, dst)
  1171. >>> if not bad_tform:
  1172. ... print("Estimation failed.")
  1173. Estimation failed.
  1174. Trying to use this failed estimation transform result will give a suitable
  1175. error:
  1176. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  1177. Traceback (most recent call last):
  1178. ...
  1179. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  1180. References
  1181. ----------
  1182. .. [1] Wikipedia, "Affine transformation",
  1183. https://en.wikipedia.org/wiki/Affine_transformation#Image_transformation
  1184. .. [2] Wikipedia, "Shear mapping",
  1185. https://en.wikipedia.org/wiki/Shear_mapping
  1186. """
  1187. def __init__(
  1188. self,
  1189. matrix=None,
  1190. *,
  1191. scale=None,
  1192. shear=None,
  1193. rotation=None,
  1194. translation=None,
  1195. dimensionality=None,
  1196. ):
  1197. n_srst_none = sum(p is None for p in (scale, rotation, shear, translation))
  1198. if n_srst_none != 4:
  1199. if matrix is not None:
  1200. raise ValueError(
  1201. "Do not specify any implicit parameters when "
  1202. "matrix is specified."
  1203. )
  1204. if dimensionality is not None and dimensionality > 2:
  1205. raise ValueError('Implicit parameters only valid for 2D transforms')
  1206. # 2D parameter checks explicit or implicit in _srst2matrix.
  1207. matrix = self._srst2matrix(scale, rotation, shear, translation)
  1208. if matrix.shape[0] != 3:
  1209. raise ValueError('Implicit parameters must give 2D transforms')
  1210. super().__init__(matrix=matrix, dimensionality=dimensionality)
  1211. @property
  1212. def _coeff_inds(self):
  1213. """Indices into flat ``self.params`` with coefficients to estimate"""
  1214. return range(self.dimensionality * (self.dimensionality + 1))
  1215. def _srst2matrix(self, scale, rotation, shear, translation):
  1216. scale = (1, 1) if scale is None else scale
  1217. sx, sy = (scale, scale) if np.isscalar(scale) else scale
  1218. rotation = 0 if rotation is None else rotation
  1219. if not np.isscalar(rotation):
  1220. raise ValueError('rotation must be scalar (2D rotation)')
  1221. shear = 0 if shear is None else shear
  1222. shear_x, shear_y = (shear, 0) if np.isscalar(shear) else shear
  1223. translation = (0, 0) if translation is None else translation
  1224. if np.isscalar(translation):
  1225. raise ValueError('translation must be length 2')
  1226. a2, b2 = translation
  1227. a0 = sx * (math.cos(rotation) + math.tan(shear_y) * math.sin(rotation))
  1228. a1 = -sy * (math.tan(shear_x) * math.cos(rotation) + math.sin(rotation))
  1229. b0 = sx * (math.sin(rotation) - math.tan(shear_y) * math.cos(rotation))
  1230. b1 = -sy * (math.tan(shear_x) * math.sin(rotation) - math.cos(rotation))
  1231. return np.array([[a0, a1, a2], [b0, b1, b2], [0, 0, 1]])
  1232. @property
  1233. def scale(self):
  1234. if self.dimensionality != 2:
  1235. return np.sqrt(np.sum(self.params**2, axis=0))[: self.dimensionality]
  1236. ss = np.sum(self.params**2, axis=0)
  1237. ss[1] = ss[1] / (math.tan(self.shear) ** 2 + 1)
  1238. return np.sqrt(ss)[: self.dimensionality]
  1239. @property
  1240. def rotation(self):
  1241. if self.dimensionality != 2:
  1242. raise NotImplementedError(
  1243. 'The rotation property is only implemented for 2D transforms.'
  1244. )
  1245. return math.atan2(self.params[1, 0], self.params[0, 0])
  1246. @property
  1247. def shear(self):
  1248. if self.dimensionality != 2:
  1249. raise NotImplementedError(
  1250. 'The shear property is only implemented for 2D transforms.'
  1251. )
  1252. beta = math.atan2(-self.params[0, 1], self.params[1, 1])
  1253. return beta - self.rotation
  1254. @property
  1255. def translation(self):
  1256. return self.params[0 : self.dimensionality, self.dimensionality]
  1257. class PiecewiseAffineTransform(_GeometricTransform):
  1258. """Piecewise affine transformation.
  1259. Control points are used to define the mapping. The transform is based on
  1260. a Delaunay triangulation of the points to form a mesh. Each triangle is
  1261. used to find a local affine transform.
  1262. Attributes
  1263. ----------
  1264. affines : list of AffineTransform objects
  1265. Affine transformations for each triangle in the mesh.
  1266. inverse_affines : list of AffineTransform objects
  1267. Inverse affine transformations for each triangle in the mesh.
  1268. Examples
  1269. --------
  1270. >>> import numpy as np
  1271. >>> import skimage as ski
  1272. Define a transformation by estimation:
  1273. >>> src = [[-12.3705, -10.5075],
  1274. ... [-10.7865, 15.4305],
  1275. ... [8.6985, 10.8675],
  1276. ... [11.4975, -9.5715],
  1277. ... [7.8435, 7.4835],
  1278. ... [-5.3325, 6.5025],
  1279. ... [6.7905, -6.3765],
  1280. ... [-6.1695, -0.8235]]
  1281. >>> dst = [[0, 0],
  1282. ... [0, 5800],
  1283. ... [4900, 5800],
  1284. ... [4900, 0],
  1285. ... [4479, 4580],
  1286. ... [1176, 3660],
  1287. ... [3754, 790],
  1288. ... [1024, 1931]]
  1289. >>> tform = ski.transform.PiecewiseAffineTransform.from_estimate(src, dst)
  1290. Calling the transform applies the transformation to the points:
  1291. >>> np.allclose(tform(src), dst)
  1292. True
  1293. You can apply the inverse transform:
  1294. >>> np.allclose(tform.inverse(dst), src)
  1295. True
  1296. The estimation can fail - for example, if all the input or output points
  1297. are the same. If this happens, you will get a transform that is not
  1298. "truthy" - meaning that ``bool(tform)`` is ``False``:
  1299. >>> # A successfully estimated model is truthy (applying ``bool()``
  1300. >>> # gives ``True``):
  1301. >>> if tform:
  1302. ... print("Estimation succeeded.")
  1303. Estimation succeeded.
  1304. >>> # Not so for a degenerate transform with identical points.
  1305. >>> bad_src = [[1, 1]] * 6 + src[6:]
  1306. >>> bad_tform = ski.transform.PiecewiseAffineTransform.from_estimate(
  1307. ... bad_src, dst)
  1308. >>> if not bad_tform:
  1309. ... print("Estimation failed.")
  1310. Estimation failed.
  1311. Trying to use this failed estimation transform result will give a suitable
  1312. error:
  1313. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  1314. Traceback (most recent call last):
  1315. ...
  1316. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  1317. """
  1318. def __init__(self):
  1319. self._tesselation = None
  1320. self._inverse_tesselation = None
  1321. self.affines = None
  1322. self.inverse_affines = None
  1323. @classmethod
  1324. def from_estimate(cls, src, dst):
  1325. """Estimate the transformation from a set of corresponding points.
  1326. Number of source and destination coordinates must match.
  1327. Parameters
  1328. ----------
  1329. src : (N, D) array_like
  1330. Source coordinates.
  1331. dst : (N, D) array_like
  1332. Destination coordinates.
  1333. Returns
  1334. -------
  1335. tf : Self or ``FailedEstimation``
  1336. An instance of the transformation if the estimation succeeded.
  1337. Otherwise, we return a special ``FailedEstimation`` object to
  1338. signal a failed estimation. Testing the truth value of the failed
  1339. estimation object will return ``False``. E.g.
  1340. .. code-block:: python
  1341. tf = PiecewiseAffineTransform.from_estimate(...)
  1342. if not tf:
  1343. raise RuntimeError(f"Failed estimation: {tf}")
  1344. """
  1345. return super().from_estimate(src, dst)
  1346. def _estimate(self, src, dst):
  1347. src = np.asarray(src)
  1348. dst = np.asarray(dst)
  1349. N, D = src.shape
  1350. # forward piecewise affine
  1351. # triangulate input positions into mesh
  1352. self._tesselation = spatial.Delaunay(src)
  1353. fail_matrix = np.full((D + 1, D + 1), np.nan)
  1354. # find affine mapping from source positions to destination
  1355. self.affines = []
  1356. messages = []
  1357. for i, tri in enumerate(self._tesselation.simplices):
  1358. affine = AffineTransform.from_estimate(src[tri, :], dst[tri, :])
  1359. if not affine:
  1360. messages.append(f'Failure at forward simplex {i}: {affine}')
  1361. affine = AffineTransform(fail_matrix.copy())
  1362. self.affines.append(affine)
  1363. # inverse piecewise affine
  1364. # triangulate input positions into mesh
  1365. self._inverse_tesselation = spatial.Delaunay(dst)
  1366. # find affine mapping from source positions to destination
  1367. self.inverse_affines = []
  1368. for i, tri in enumerate(self._inverse_tesselation.simplices):
  1369. affine = AffineTransform.from_estimate(dst[tri, :], src[tri, :])
  1370. if not affine:
  1371. messages.append(f'Failure at inverse simplex {i}: {affine}')
  1372. affine = AffineTransform(fail_matrix.copy())
  1373. self.inverse_affines.append(affine)
  1374. return '; '.join(messages) if messages else None
  1375. def __call__(self, coords):
  1376. """Apply forward transformation.
  1377. Coordinates outside of the mesh will be set to `- 1`.
  1378. Parameters
  1379. ----------
  1380. coords : (N, D) array_like
  1381. Source coordinates.
  1382. Returns
  1383. -------
  1384. coords : (N, 2) array
  1385. Transformed coordinates.
  1386. """
  1387. coords = np.asarray(coords)
  1388. out = np.empty_like(coords, np.float64)
  1389. # determine triangle index for each coordinate
  1390. simplex = self._tesselation.find_simplex(coords)
  1391. # coordinates outside of mesh
  1392. out[simplex == -1, :] = -1
  1393. for index in range(len(self._tesselation.simplices)):
  1394. # affine transform for triangle
  1395. affine = self.affines[index]
  1396. # all coordinates within triangle
  1397. index_mask = simplex == index
  1398. out[index_mask, :] = affine(coords[index_mask, :])
  1399. return out
  1400. @property
  1401. def inverse(self):
  1402. """Return a transform object representing the inverse."""
  1403. tform = type(self)()
  1404. # Copy parameters (None or list) for safety.
  1405. tform._tesselation = copy(self._inverse_tesselation)
  1406. tform._inverse_tesselation = copy(self._tesselation)
  1407. tform.affines = copy(self.inverse_affines)
  1408. tform.inverse_affines = copy(self.affines)
  1409. return tform
  1410. @classmethod
  1411. def identity(cls, dimensionality=None):
  1412. """Identity transform
  1413. Parameters
  1414. ----------
  1415. dimensionality : optional
  1416. This transform does not use the `dimensionality` parameter, so the
  1417. value is ignored. The parameter exists for compatibility with
  1418. other transforms.
  1419. Returns
  1420. -------
  1421. tform : transform
  1422. Transform such that ``np.all(tform(pts) == pts)``.
  1423. """
  1424. return cls()
  1425. @_deprecate_estimate
  1426. def estimate(self, src, dst):
  1427. """Estimate the transformation from a set of corresponding points.
  1428. Number of source and destination coordinates must match.
  1429. Parameters
  1430. ----------
  1431. src : (N, D) array_like
  1432. Source coordinates.
  1433. dst : (N, D) array_like
  1434. Destination coordinates.
  1435. Returns
  1436. -------
  1437. success : bool
  1438. True, if all pieces of the model are successfully estimated.
  1439. """
  1440. return self._estimate(src, dst) is None
  1441. def _euler_rotation_matrix(angles, degrees=False):
  1442. """Produce an Euler rotation matrix from the given intrinsic rotation angles
  1443. for the axes x, y and z.
  1444. Parameters
  1445. ----------
  1446. angles : array of float, shape (3,)
  1447. The transformation angles in radians.
  1448. degrees : bool, optional
  1449. If True, then the given angles are assumed to be in degrees. Default is False.
  1450. Returns
  1451. -------
  1452. R : array of float, shape (3, 3)
  1453. The Euler rotation matrix.
  1454. """
  1455. return spatial.transform.Rotation.from_euler(
  1456. 'XYZ', angles=angles, degrees=degrees
  1457. ).as_matrix()
  1458. class EuclideanTransform(ProjectiveTransform):
  1459. """Euclidean transformation, also known as a rigid transform.
  1460. Has the following form::
  1461. X = a0 * x - b0 * y + a1 =
  1462. = x * cos(rotation) - y * sin(rotation) + a1
  1463. Y = b0 * x + a0 * y + b1 =
  1464. = x * sin(rotation) + y * cos(rotation) + b1
  1465. where the homogeneous transformation matrix is::
  1466. [[a0 -b0 a1]
  1467. [b0 a0 b1]
  1468. [0 0 1 ]]
  1469. The Euclidean transformation is a rigid transformation with rotation and
  1470. translation parameters. The similarity transformation extends the Euclidean
  1471. transformation with a single scaling factor.
  1472. In 2D and 3D, the transformation parameters may be provided either via
  1473. `matrix`, the homogeneous transformation matrix, above, or via the
  1474. implicit parameters `rotation` and/or `translation` (where `a1` is the
  1475. translation along `x`, `b1` along `y`, etc.). Beyond 3D, if the
  1476. transformation is only a translation, you may use the implicit parameter
  1477. `translation`; otherwise, you must use `matrix`.
  1478. The implicit parameters are applied in the following order:
  1479. 1. Rotation;
  1480. 2. Translation.
  1481. Parameters
  1482. ----------
  1483. matrix : (D+1, D+1) array_like, optional
  1484. Homogeneous transformation matrix.
  1485. rotation : float or sequence of float, optional
  1486. Rotation angle, clockwise, in radians. If given as a vector, it is
  1487. interpreted as Euler rotation angles [1]_. Only 2D (single rotation)
  1488. and 3D (Euler rotations) values are supported. For higher dimensions,
  1489. you must provide or estimate the transformation matrix instead, and
  1490. pass that as `matrix` above.
  1491. translation : (x, y[, z, ...]) sequence of float, length D, optional
  1492. Translation parameters for each axis.
  1493. dimensionality : int, optional
  1494. Fallback number of dimensions for transform when no other parameter
  1495. is specified. Otherwise ignored, and we infer dimensionality from the
  1496. input parameters.
  1497. Attributes
  1498. ----------
  1499. params : (D+1, D+1) array
  1500. Homogeneous transformation matrix.
  1501. Examples
  1502. --------
  1503. >>> import numpy as np
  1504. >>> import skimage as ski
  1505. Define a transform with an homogeneous transformation matrix:
  1506. >>> tform = ski.transform.EuclideanTransform(np.diag([2., 3., 1.]))
  1507. >>> tform.params
  1508. array([[2., 0., 0.],
  1509. [0., 3., 0.],
  1510. [0., 0., 1.]])
  1511. Define a transform with parameters:
  1512. >>> tform = ski.transform.EuclideanTransform(
  1513. ... rotation=0.2, translation=[1, 2])
  1514. >>> np.round(tform.params, 2)
  1515. array([[ 0.98, -0.2 , 1. ],
  1516. [ 0.2 , 0.98, 2. ],
  1517. [ 0. , 0. , 1. ]])
  1518. You can estimate a transformation to map between source and destination
  1519. points:
  1520. >>> src = np.array([[150, 150],
  1521. ... [250, 100],
  1522. ... [150, 200]])
  1523. >>> dst = np.array([[200, 200],
  1524. ... [300, 150],
  1525. ... [150, 400]])
  1526. >>> tform = ski.transform.EuclideanTransform.from_estimate(src, dst)
  1527. >>> np.allclose(tform.params, [[ 0.99, 0.12, 16.77],
  1528. ... [-0.12, 0.99, 122.91],
  1529. ... [ 0. , 0. , 1. ]], atol=0.01)
  1530. True
  1531. Apply the transformation to some image data.
  1532. >>> img = ski.data.astronaut()
  1533. >>> warped = ski.transform.warp(img, inverse_map=tform.inverse)
  1534. The estimation can fail - for example, if all the input or output points
  1535. are the same. If this happens, you will get a transform that is not
  1536. "truthy" - meaning that ``bool(tform)`` is ``False``:
  1537. >>> # A successfully estimated model is truthy (applying ``bool()``
  1538. >>> # gives ``True``):
  1539. >>> if tform:
  1540. ... print("Estimation succeeded.")
  1541. Estimation succeeded.
  1542. >>> # Not so for a degenerate transform with identical points.
  1543. >>> bad_src = np.ones((3, 2))
  1544. >>> bad_tform = ski.transform.EuclideanTransform.from_estimate(
  1545. ... bad_src, dst)
  1546. >>> if not bad_tform:
  1547. ... print("Estimation failed.")
  1548. Estimation failed.
  1549. Trying to use this failed estimation transform result will give a suitable
  1550. error:
  1551. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  1552. Traceback (most recent call last):
  1553. ...
  1554. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  1555. References
  1556. ----------
  1557. .. [1] https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
  1558. """
  1559. # Whether to estimate scale during estimation.
  1560. _estimate_scale = False
  1561. def __init__(
  1562. self, matrix=None, *, rotation=None, translation=None, dimensionality=None
  1563. ):
  1564. n_rt_none = sum(p is None for p in (rotation, translation))
  1565. if n_rt_none != 2:
  1566. if matrix is not None:
  1567. raise ValueError(
  1568. "Do not specify any implicit parameters when "
  1569. "matrix is specified."
  1570. )
  1571. n_dims, chk_msg = self._rt2ndims_msg(rotation, translation)
  1572. if chk_msg is not None:
  1573. raise ValueError(chk_msg)
  1574. matrix = self._rt2matrix(rotation, translation, n_dims)
  1575. super().__init__(matrix=matrix, dimensionality=dimensionality)
  1576. def _rt2ndims_msg(self, rotation, translation):
  1577. if rotation is not None:
  1578. N = 1 if np.isscalar(rotation) else len(rotation)
  1579. msg = (
  1580. '``rotations`` must be scalar (3D) or length 3 (3D)'
  1581. if N not in (1, 3)
  1582. else None
  1583. )
  1584. return 2 if N == 1 else N, msg
  1585. if translation is not None:
  1586. return (2 if np.isscalar(translation) else len(translation), None)
  1587. return None, None
  1588. def _rt2matrix(self, rotation, translation, n_dims):
  1589. if translation is None:
  1590. translation = (0,) * n_dims
  1591. if rotation is None:
  1592. rotation = 0 if n_dims == 2 else np.zeros(3)
  1593. matrix = np.eye(n_dims + 1)
  1594. if n_dims == 2:
  1595. cos_r, sin_r = math.cos(rotation), math.sin(rotation)
  1596. matrix[:2, :2] = [[cos_r, -sin_r], [sin_r, cos_r]]
  1597. elif n_dims == 3:
  1598. matrix[:3, :3] = _euler_rotation_matrix(rotation)
  1599. matrix[0:n_dims, n_dims] = translation
  1600. return matrix
  1601. @classmethod
  1602. def from_estimate(cls, src, dst) -> Self | FailedEstimation:
  1603. """Estimate the transformation from a set of corresponding points.
  1604. You can determine the over-, well- and under-determined parameters
  1605. with the total least-squares method.
  1606. Number of source and destination coordinates must match.
  1607. Parameters
  1608. ----------
  1609. src : (N, 2) array_like
  1610. Source coordinates.
  1611. dst : (N, 2) array_like
  1612. Destination coordinates.
  1613. Returns
  1614. -------
  1615. tf : Self or ``FailedEstimation``
  1616. An instance of the transformation if the estimation succeeded.
  1617. Otherwise, we return a special ``FailedEstimation`` object to
  1618. signal a failed estimation. Testing the truth value of the failed
  1619. estimation object will return ``False``. E.g.
  1620. .. code-block:: python
  1621. tf = EuclideanTransform.from_estimate(...)
  1622. if not tf:
  1623. raise RuntimeError(f"Failed estimation: {tf}")
  1624. """
  1625. # Use base implementation to avoid weights argument of
  1626. # ProjectiveTransform ancestor class.
  1627. return _from_estimate(cls, src, dst)
  1628. def _estimate(self, src, dst):
  1629. self.params = _umeyama(src, dst, self._estimate_scale)
  1630. # _umeyama will return nan if the problem is not well-conditioned.
  1631. return (
  1632. 'Poor conditioning for estimation'
  1633. if np.any(np.isnan(self.params))
  1634. else None
  1635. )
  1636. @property
  1637. def rotation(self):
  1638. if self.dimensionality == 2:
  1639. return math.atan2(self.params[1, 0], self.params[1, 1])
  1640. elif self.dimensionality == 3:
  1641. # Returning 3D Euler rotation matrix
  1642. return self.params[:3, :3]
  1643. else:
  1644. raise NotImplementedError(
  1645. 'Rotation only implemented for 2D and 3D transforms.'
  1646. )
  1647. @property
  1648. def translation(self):
  1649. return self.params[0 : self.dimensionality, self.dimensionality]
  1650. @_deprecate_estimate
  1651. def estimate(self, src, dst):
  1652. """Estimate the transformation from a set of corresponding points.
  1653. You can determine the over-, well- and under-determined parameters
  1654. with the total least-squares method.
  1655. Number of source and destination coordinates must match.
  1656. Parameters
  1657. ----------
  1658. src : (N, 2) array_like
  1659. Source coordinates.
  1660. dst : (N, 2) array_like
  1661. Destination coordinates.
  1662. Returns
  1663. -------
  1664. success : bool
  1665. True, if model estimation succeeds.
  1666. """
  1667. return self._estimate(src, dst) is None
  1668. @_update_from_estimate_docstring
  1669. @_deprecate_inherited_estimate
  1670. class SimilarityTransform(EuclideanTransform):
  1671. """Similarity transformation.
  1672. Has the following form in 2D::
  1673. X = a0 * x - b0 * y + a1 =
  1674. = s * x * cos(rotation) - s * y * sin(rotation) + a1
  1675. Y = b0 * x + a0 * y + b1 =
  1676. = s * x * sin(rotation) + s * y * cos(rotation) + b1
  1677. where ``s`` is a scale factor and the homogeneous transformation matrix is::
  1678. [[a0 -b0 a1]
  1679. [b0 a0 b1]
  1680. [0 0 1 ]]
  1681. The similarity transformation extends the Euclidean transformation with a
  1682. single scaling factor in addition to the rotation and translation
  1683. parameters.
  1684. The implicit parameters are applied in the following order:
  1685. 1. Scale;
  1686. 2. Rotation;
  1687. 3. Translation.
  1688. Parameters
  1689. ----------
  1690. matrix : (dim+1, dim+1) array_like, optional
  1691. Homogeneous transformation matrix.
  1692. scale : float, optional
  1693. Scale factor. Implemented only for 2D and 3D.
  1694. rotation : float, optional
  1695. Rotation angle, clockwise, as radians.
  1696. Implemented only for 2D and 3D. For 3D, this is given in ZYX Euler
  1697. angles.
  1698. translation : (dim,) array_like, optional
  1699. x, y[, z] translation parameters. Implemented only for 2D and 3D.
  1700. dimensionality : int, optional
  1701. The dimensionality of the transform, corresponding to ``dim`` above.
  1702. Ignored if `matrix` is not None, and set to ``matrix.shape[0] - 1``.
  1703. Otherwise, must be one of 2 or 3.
  1704. Attributes
  1705. ----------
  1706. params : (dim+1, dim+1) array
  1707. Homogeneous transformation matrix.
  1708. Examples
  1709. --------
  1710. >>> import numpy as np
  1711. >>> import skimage as ski
  1712. Define a transform with an homogeneous transformation matrix:
  1713. >>> tform = ski.transform.SimilarityTransform(np.diag([2., 3., 1.]))
  1714. >>> tform.params
  1715. array([[2., 0., 0.],
  1716. [0., 3., 0.],
  1717. [0., 0., 1.]])
  1718. Define a transform with parameters:
  1719. >>> tform = ski.transform.SimilarityTransform(
  1720. ... rotation=0.2, translation=[1, 2])
  1721. >>> np.round(tform.params, 2)
  1722. array([[ 0.98, -0.2 , 1. ],
  1723. [ 0.2 , 0.98, 2. ],
  1724. [ 0. , 0. , 1. ]])
  1725. You can estimate a transformation to map between source and destination
  1726. points:
  1727. >>> src = np.array([[150, 150],
  1728. ... [250, 100],
  1729. ... [150, 200]])
  1730. >>> dst = np.array([[200, 200],
  1731. ... [300, 150],
  1732. ... [150, 400]])
  1733. >>> tform = ski.transform.SimilarityTransform.from_estimate(src, dst)
  1734. >>> np.allclose(tform.params, [[ 1.79, 0.21, -142.86],
  1735. ... [-0.21, 1.79, 21.43],
  1736. ... [ 0. , 0. , 1. ]], atol=0.01)
  1737. True
  1738. Apply the transformation to some image data.
  1739. >>> img = ski.data.astronaut()
  1740. >>> warped = ski.transform.warp(img, inverse_map=tform.inverse)
  1741. The estimation can fail - for example, if all the input or output points
  1742. are the same. If this happens, you will get a transform that is not
  1743. "truthy" - meaning that ``bool(tform)`` is ``False``:
  1744. >>> # A successfully estimated model is truthy (applying ``bool()``
  1745. >>> # gives ``True``):
  1746. >>> if tform:
  1747. ... print("Estimation succeeded.")
  1748. Estimation succeeded.
  1749. >>> # Not so for a degenerate transform with identical points.
  1750. >>> bad_src = np.ones((3, 2))
  1751. >>> bad_tform = ski.transform.SimilarityTransform.from_estimate(
  1752. ... bad_src, dst)
  1753. >>> if not bad_tform:
  1754. ... print("Estimation failed.")
  1755. Estimation failed.
  1756. Trying to use this failed estimation transform result will give a suitable
  1757. error:
  1758. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  1759. Traceback (most recent call last):
  1760. ...
  1761. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  1762. """
  1763. # Whether to estimate scale during estimation.
  1764. _estimate_scale = True
  1765. def __init__(
  1766. self,
  1767. matrix=None,
  1768. *,
  1769. scale=None,
  1770. rotation=None,
  1771. translation=None,
  1772. dimensionality=None,
  1773. ):
  1774. n_srt_none = sum(p is None for p in (scale, rotation, translation))
  1775. if n_srt_none != 3:
  1776. if matrix is not None:
  1777. raise ValueError(
  1778. "Do not specify any implicit parameters when "
  1779. "matrix is specified."
  1780. )
  1781. self._check_scale(scale, (rotation, translation), dimensionality)
  1782. # Scale is special. Scalar scale does not tell us the dimensions.
  1783. if scale is not None and not np.isscalar(scale):
  1784. n_dims, chk_msg = len(scale), None
  1785. else:
  1786. n_dims, chk_msg = self._rt2ndims_msg(rotation, translation)
  1787. if chk_msg is not None:
  1788. raise ValueError(chk_msg)
  1789. # n_dims can be None for scalar scale, other parameters are None.
  1790. n_dims = (
  1791. n_dims
  1792. if n_dims is not None
  1793. else dimensionality
  1794. if dimensionality is not None
  1795. else 2
  1796. )
  1797. matrix = self._rt2matrix(rotation, translation, n_dims)
  1798. if scale not in (None, 1):
  1799. matrix[:n_dims, :n_dims] *= scale
  1800. super().__init__(matrix=matrix, dimensionality=dimensionality)
  1801. def _check_scale(self, scale, other_params, dimensionality):
  1802. """Check, warn for scalar scaling"""
  1803. if dimensionality in (None, 2) or scale is None or not np.isscalar(scale):
  1804. return
  1805. if all(p is None for p in other_params):
  1806. warnings.warn(
  1807. 'In the future, it will be a ValueError to pass a '
  1808. 'scalar `scale` value with a ``dimensionality`` '
  1809. '> 2\n,and without other implicit parameters '
  1810. 'to indicate the dimensionality of the transform.\n'
  1811. 'Please indicate dimensionality by passing a vector '
  1812. 'of suitable length to `scale`.',
  1813. FutureWarning,
  1814. stacklevel=2,
  1815. )
  1816. @property
  1817. def scale(self):
  1818. # det = scale**(# of dimensions), therefore scale = det**(1/ndim)
  1819. if self.dimensionality == 2:
  1820. return np.sqrt(np.linalg.det(self.params))
  1821. elif self.dimensionality == 3:
  1822. return np.cbrt(np.linalg.det(self.params))
  1823. else:
  1824. raise NotImplementedError('Scale is only implemented for 2D and 3D.')
  1825. class PolynomialTransform(_GeometricTransform):
  1826. """2D polynomial transformation.
  1827. Has the following form::
  1828. X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i ))
  1829. Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i ))
  1830. Parameters
  1831. ----------
  1832. params : (2, N) array_like, optional
  1833. Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So,
  1834. a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`.
  1835. dimensionality : int, optional
  1836. Must have value 2 (the default) for polynomial transforms.
  1837. Attributes
  1838. ----------
  1839. params : (2, N) array
  1840. Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So,
  1841. a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`.
  1842. Examples
  1843. --------
  1844. >>> import numpy as np
  1845. >>> import skimage as ski
  1846. Define a transformation by estimation:
  1847. >>> src = [[-12.3705, -10.5075],
  1848. ... [-10.7865, 15.4305],
  1849. ... [8.6985, 10.8675],
  1850. ... [11.4975, -9.5715],
  1851. ... [7.8435, 7.4835],
  1852. ... [-5.3325, 6.5025],
  1853. ... [6.7905, -6.3765],
  1854. ... [-6.1695, -0.8235]]
  1855. >>> dst = [[0, 0],
  1856. ... [0, 5800],
  1857. ... [4900, 5800],
  1858. ... [4900, 0],
  1859. ... [4479, 4580],
  1860. ... [1176, 3660],
  1861. ... [3754, 790],
  1862. ... [1024, 1931]]
  1863. >>> tform = ski.transform.PolynomialTransform.from_estimate(src, dst)
  1864. Calling the transform applies the transformation to the points:
  1865. >>> pts = tform(src)
  1866. >>> np.allclose(pts, [[ 7.54, 12.27],
  1867. ... [ 2.98, 5796.95],
  1868. ... [4870.44, 5766.59],
  1869. ... [4889.72, -6.72],
  1870. ... [4515.62, 4617.5 ],
  1871. ... [1183.25, 3694. ],
  1872. ... [3767.57, 800.53],
  1873. ... [ 998.02, 1881.97]], atol=0.01)
  1874. True
  1875. """
  1876. def __init__(self, params=None, *, dimensionality=None):
  1877. if dimensionality is None:
  1878. dimensionality = 2
  1879. elif dimensionality != 2:
  1880. raise NotImplementedError(
  1881. 'Polynomial transforms are only implemented for 2D.'
  1882. )
  1883. self.params = np.array([[0, 1, 0], [0, 0, 1]] if params is None else params)
  1884. if self.params.shape == () or self.params.shape[0] != 2:
  1885. raise ValueError("Transformation parameters must be shape (2, N)")
  1886. @classmethod
  1887. def from_estimate(cls, src, dst, order=2, weights=None):
  1888. """Estimate the transformation from a set of corresponding points.
  1889. You can determine the over-, well- and under-determined parameters
  1890. with the total least-squares method.
  1891. Number of source and destination coordinates must match.
  1892. The transformation is defined as::
  1893. X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i ))
  1894. Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i ))
  1895. These equations can be transformed to the following form::
  1896. 0 = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - X
  1897. 0 = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - Y
  1898. which exist for each set of corresponding points, so we have a set of
  1899. N * 2 equations. The coefficients appear linearly so we can write
  1900. A x = 0, where::
  1901. A = [[1 x y x**2 x*y y**2 ... 0 ... 0 -X]
  1902. [0 ... 0 1 x y x**2 x*y y**2 -Y]
  1903. ...
  1904. ...
  1905. ]
  1906. x.T = [a00 a10 a11 a20 a21 a22 ... ann
  1907. b00 b10 b11 b20 b21 b22 ... bnn c3]
  1908. In case of total least-squares the solution of this homogeneous system
  1909. of equations is the right singular vector of A which corresponds to the
  1910. smallest singular value normed by the coefficient c3.
  1911. Weights can be applied to each pair of corresponding points to
  1912. indicate, particularly in an overdetermined system, if point pairs have
  1913. higher or lower confidence or uncertainties associated with them. From
  1914. the matrix treatment of least squares problems, these weight values are
  1915. normalized, square-rooted, then built into a diagonal matrix, by which
  1916. A is multiplied.
  1917. Parameters
  1918. ----------
  1919. src : (N, 2) array_like
  1920. Source coordinates.
  1921. dst : (N, 2) array_like
  1922. Destination coordinates.
  1923. order : int, optional
  1924. Polynomial order (number of coefficients is order + 1).
  1925. weights : (N,) array_like, optional
  1926. Relative weight values for each pair of points.
  1927. Returns
  1928. -------
  1929. tf : Self or ``FailedEstimation``
  1930. An instance of the transformation if the estimation succeeded.
  1931. Otherwise, we return a special ``FailedEstimation`` object to
  1932. signal a failed estimation. Testing the truth value of the failed
  1933. estimation object will return ``False``. E.g.
  1934. .. code-block:: python
  1935. tf = PolynomialTransform.from_estimate(...)
  1936. if not tf:
  1937. raise RuntimeError(f"Failed estimation: {tf}")
  1938. """
  1939. return super().from_estimate(src, dst, order, weights)
  1940. def _estimate(self, src, dst, order=2, weights=None):
  1941. src = np.asarray(src)
  1942. dst = np.asarray(dst)
  1943. xs = src[:, 0]
  1944. ys = src[:, 1]
  1945. xd = dst[:, 0]
  1946. yd = dst[:, 1]
  1947. rows = src.shape[0]
  1948. # number of unknown polynomial coefficients
  1949. order = safe_as_int(order)
  1950. u = (order + 1) * (order + 2)
  1951. A = np.zeros((rows * 2, u + 1))
  1952. pidx = 0
  1953. for j in range(order + 1):
  1954. for i in range(j + 1):
  1955. A[:rows, pidx] = xs ** (j - i) * ys**i
  1956. A[rows:, pidx + u // 2] = xs ** (j - i) * ys**i
  1957. pidx += 1
  1958. A[:rows, -1] = xd
  1959. A[rows:, -1] = yd
  1960. # Get the vectors that correspond to singular values, also applying
  1961. # the weighting if provided
  1962. if weights is None:
  1963. _, _, V = np.linalg.svd(A)
  1964. else:
  1965. weights = np.asarray(weights)
  1966. W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), 2))
  1967. _, _, V = np.linalg.svd(W @ A)
  1968. # solution is right singular vector that corresponds to smallest
  1969. # singular value
  1970. params = -V[-1, :-1] / V[-1, -1]
  1971. self.params = params.reshape((2, u // 2))
  1972. return None
  1973. def __call__(self, coords):
  1974. """Apply forward transformation.
  1975. Parameters
  1976. ----------
  1977. coords : (N, 2) array_like
  1978. source coordinates
  1979. Returns
  1980. -------
  1981. coords : (N, 2) array
  1982. Transformed coordinates.
  1983. """
  1984. coords = np.asarray(coords)
  1985. x = coords[:, 0]
  1986. y = coords[:, 1]
  1987. u = len(self.params.ravel())
  1988. # number of coefficients -> u = (order + 1) * (order + 2)
  1989. order = int((-3 + math.sqrt(9 - 4 * (2 - u))) / 2)
  1990. dst = np.zeros(coords.shape)
  1991. pidx = 0
  1992. for j in range(order + 1):
  1993. for i in range(j + 1):
  1994. dst[:, 0] += self.params[0, pidx] * x ** (j - i) * y**i
  1995. dst[:, 1] += self.params[1, pidx] * x ** (j - i) * y**i
  1996. pidx += 1
  1997. return dst
  1998. @classmethod
  1999. def identity(cls, dimensionality=None):
  2000. """Identity transform
  2001. Parameters
  2002. ----------
  2003. dimensionality : {None, 2}, optional
  2004. This transform only allows dimensionality of 2, where None
  2005. corresponds to 2. The parameter exists for compatibility with other
  2006. transforms.
  2007. Returns
  2008. -------
  2009. tform : transform
  2010. Transform such that ``np.all(tform(pts) == pts)``.
  2011. """
  2012. return cls(params=None, dimensionality=dimensionality)
  2013. @property
  2014. def inverse(self):
  2015. raise NotImplementedError(
  2016. 'There is no explicit way to do the inverse polynomial '
  2017. 'transformation. Instead, estimate the inverse transformation '
  2018. 'parameters by exchanging source and destination coordinates,'
  2019. 'then apply the forward transformation.'
  2020. )
  2021. @_deprecate_estimate
  2022. def estimate(self, src, dst, order=2, weights=None):
  2023. """Estimate the transformation from a set of corresponding points.
  2024. You can determine the over-, well- and under-determined parameters
  2025. with the total least-squares method.
  2026. Number of source and destination coordinates must match.
  2027. The transformation is defined as::
  2028. X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i ))
  2029. Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i ))
  2030. These equations can be transformed to the following form::
  2031. 0 = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - X
  2032. 0 = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - Y
  2033. which exist for each set of corresponding points, so we have a set of
  2034. N * 2 equations. The coefficients appear linearly so we can write
  2035. A x = 0, where::
  2036. A = [[1 x y x**2 x*y y**2 ... 0 ... 0 -X]
  2037. [0 ... 0 1 x y x**2 x*y y**2 -Y]
  2038. ...
  2039. ...
  2040. ]
  2041. x.T = [a00 a10 a11 a20 a21 a22 ... ann
  2042. b00 b10 b11 b20 b21 b22 ... bnn c3]
  2043. In case of total least-squares the solution of this homogeneous system
  2044. of equations is the right singular vector of A which corresponds to the
  2045. smallest singular value normed by the coefficient c3.
  2046. Weights can be applied to each pair of corresponding points to
  2047. indicate, particularly in an overdetermined system, if point pairs have
  2048. higher or lower confidence or uncertainties associated with them. From
  2049. the matrix treatment of least squares problems, these weight values are
  2050. normalized, square-rooted, then built into a diagonal matrix, by which
  2051. A is multiplied.
  2052. Parameters
  2053. ----------
  2054. src : (N, 2) array_like
  2055. Source coordinates.
  2056. dst : (N, 2) array_like
  2057. Destination coordinates.
  2058. order : int, optional
  2059. Polynomial order (number of coefficients is order + 1).
  2060. weights : (N,) array_like, optional
  2061. Relative weight values for each pair of points.
  2062. Returns
  2063. -------
  2064. success : bool
  2065. True, if model estimation succeeds.
  2066. """
  2067. return self._estimate(src, dst, order, weights) is None
  2068. TRANSFORMS = {
  2069. 'euclidean': EuclideanTransform,
  2070. 'similarity': SimilarityTransform,
  2071. 'affine': AffineTransform,
  2072. 'piecewise-affine': PiecewiseAffineTransform,
  2073. 'projective': ProjectiveTransform,
  2074. 'fundamental': FundamentalMatrixTransform,
  2075. 'essential': EssentialMatrixTransform,
  2076. 'polynomial': PolynomialTransform,
  2077. }
  2078. def estimate_transform(ttype, src, dst, *args, **kwargs):
  2079. """Estimate 2D geometric transformation parameters.
  2080. You can determine the over-, well- and under-determined parameters
  2081. with the total least-squares method.
  2082. Number of source and destination coordinates must match.
  2083. Parameters
  2084. ----------
  2085. ttype : {'euclidean', similarity', 'affine', 'piecewise-affine', \
  2086. 'projective', 'polynomial'}
  2087. Type of transform.
  2088. kwargs : array_like or int
  2089. Function parameters (src, dst, n, angle)::
  2090. NAME / TTYPE FUNCTION PARAMETERS
  2091. 'euclidean' `src, `dst`
  2092. 'similarity' `src, `dst`
  2093. 'affine' `src, `dst`
  2094. 'piecewise-affine' `src, `dst`
  2095. 'projective' `src, `dst`
  2096. 'polynomial' `src, `dst`, `order` (polynomial order,
  2097. default order is 2)
  2098. Also see examples below.
  2099. Returns
  2100. -------
  2101. tf : :class:`_GeometricTransform` or ``FailedEstimation``
  2102. An instance of the requested transformation if the estimation
  2103. Otherwise, we return a special ``FailedEstimation`` object to signal a
  2104. failed estimation. Testing the truth value of the failed estimation
  2105. object will return ``False``. E.g.
  2106. .. code-block:: python
  2107. tf = estimate_transform(...)
  2108. if not tf:
  2109. raise RuntimeError(f"Failed estimation: {tf}")
  2110. Examples
  2111. --------
  2112. >>> import numpy as np
  2113. >>> import skimage as ski
  2114. >>> # estimate transformation parameters
  2115. >>> src = np.array([0, 0, 10, 10]).reshape((2, 2))
  2116. >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2))
  2117. >>> tform = ski.transform.estimate_transform('similarity', src, dst)
  2118. >>> np.allclose(tform.inverse(tform(src)), src)
  2119. True
  2120. >>> # warp image using the estimated transformation
  2121. >>> image = ski.data.camera()
  2122. >>> ski.transform.warp(image, inverse_map=tform.inverse) # doctest: +SKIP
  2123. >>> # create transformation with explicit parameters
  2124. >>> tform2 = ski.transform.SimilarityTransform(scale=1.1, rotation=1,
  2125. ... translation=(10, 20))
  2126. >>> # unite transformations, applied in order from left to right
  2127. >>> tform3 = tform + tform2
  2128. >>> np.allclose(tform3(src), tform2(tform(src)))
  2129. True
  2130. The estimation can fail - for example, if all the input or output points
  2131. are the same. If this happens, you will get a transform that is not
  2132. "truthy" - meaning that ``bool(tform)`` is ``False``:
  2133. >>> # A successfully estimated model is truthy (applying ``bool()``
  2134. >>> # gives ``True``):
  2135. >>> if tform:
  2136. ... print("Estimation succeeded.")
  2137. Estimation succeeded.
  2138. >>> # Not so for a degenerate transform with identical points.
  2139. >>> bad_src = np.ones((2, 2))
  2140. >>> bad_tform = ski.transform.estimate_transform('similarity',
  2141. ... bad_src, dst)
  2142. >>> if not bad_tform:
  2143. ... print("Estimation failed.")
  2144. Estimation failed.
  2145. Trying to use this failed estimation transform result will give a suitable
  2146. error:
  2147. >>> bad_tform.params # doctest: +IGNORE_EXCEPTION_DETAIL
  2148. Traceback (most recent call last):
  2149. ...
  2150. FailedEstimationAccessError: No attribute "params" for failed estimation ...
  2151. """
  2152. ttype = ttype.lower()
  2153. if ttype not in TRANSFORMS:
  2154. raise ValueError(f'the transformation type \'{ttype}\' is not implemented')
  2155. return TRANSFORMS[ttype].from_estimate(src, dst, *args, **kwargs)
  2156. def matrix_transform(coords, matrix):
  2157. """Apply 2D matrix transform.
  2158. Parameters
  2159. ----------
  2160. coords : (N, 2) array_like
  2161. x, y coordinates to transform
  2162. matrix : (3, 3) array_like
  2163. Homogeneous transformation matrix.
  2164. Returns
  2165. -------
  2166. coords : (N, 2) array
  2167. Transformed coordinates.
  2168. """
  2169. return ProjectiveTransform(matrix)(coords)