Coverage for geometry/ellipse.py: 74%
72 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Ellipse
3=======
5Define objects for ellipse computations and fitting operations.
7- :func:`colour.algebra.ellipse_coefficients_general_form`
8- :func:`colour.algebra.ellipse_coefficients_canonical_form`
9- :func:`colour.algebra.point_at_angle_on_ellipse`
10- :func:`colour.algebra.ellipse_fitting_Halir1998`
12References
13----------
14- :cite:`Halir1998` : Halir, R., & Flusser, J. (1998). Numerically Stable
15 Direct Least Squares Fitting Of Ellipses (pp. 1-8).
16 http://citeseerx.ist.psu.edu/viewdoc/download;\
17jsessionid=BEEAFC85DE53308286D626302F4A3E3C?doi=10.1.1.1.7559&rep=rep1&type=pdf
18- :cite:`Wikipedia` : Wikipedia. (n.d.). Ellipse. Retrieved November 24,
19 2018, from https://en.wikipedia.org/wiki/Ellipse
20"""
22from __future__ import annotations
24import typing
26import numpy as np
28if typing.TYPE_CHECKING:
29 from colour.hints import ArrayLike, Literal
31from colour.hints import NDArrayFloat, cast
32from colour.utilities import (
33 CanonicalMapping,
34 ones,
35 tsplit,
36 tstack,
37 validate_method,
38)
40__author__ = "Colour Developers"
41__copyright__ = "Copyright 2013 Colour Developers"
42__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
43__maintainer__ = "Colour Developers"
44__email__ = "colour-developers@colour-science.org"
45__status__ = "Production"
47__all__ = [
48 "ellipse_coefficients_general_form",
49 "ellipse_coefficients_canonical_form",
50 "point_at_angle_on_ellipse",
51 "ellipse_fitting_Halir1998",
52 "ELLIPSE_FITTING_METHODS",
53 "ellipse_fitting",
54]
57def ellipse_coefficients_general_form(coefficients: ArrayLike) -> NDArrayFloat:
58 """
59 Compute general form ellipse coefficients from the specified canonical
60 form ellipse coefficients.
62 Transform ellipse coefficients from canonical representation (center,
63 semi-axes, rotation) to general quadratic form
64 :math:`Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0`.
66 The canonical form ellipse coefficients are: center coordinates
67 :math:`(x_c, y_c)`, semi-major axis length :math:`a`, semi-minor axis
68 length :math:`b`, and rotation angle :math:`\\theta` (degrees) of the
69 semi-major axis from the positive x-axis.
71 Parameters
72 ----------
73 coefficients
74 Canonical form ellipse coefficients as
75 :math:`[x_c, y_c, a, b, \\theta]`.
77 Returns
78 -------
79 :class:`numpy.ndarray`
80 General form coefficients :math:`[A, B, C, D, E, F]`.
82 References
83 ----------
84 :cite:`Wikipedia`
86 Examples
87 --------
88 >>> coefficients = np.array([0.5, 0.5, 2, 1, 45])
89 >>> ellipse_coefficients_general_form(coefficients)
90 array([ 2.5, -3. , 2.5, -1. , -1. , -3.5])
91 """
93 x_c, y_c, a_a, a_b, theta = tsplit(coefficients)
95 theta = np.radians(theta)
96 cos_theta = np.cos(theta)
97 sin_theta = np.sin(theta)
98 cos_theta_2 = cos_theta**2
99 sin_theta_2 = sin_theta**2
100 a_a_2 = a_a**2
101 a_b_2 = a_b**2
103 a = a_a_2 * sin_theta_2 + a_b_2 * cos_theta_2
104 b = 2 * (a_b_2 - a_a_2) * sin_theta * cos_theta
105 c = a_a_2 * cos_theta_2 + a_b_2 * sin_theta_2
106 d = -2 * a * x_c - b * y_c
107 e = -b * x_c - 2 * c * y_c
108 f = a * x_c**2 + b * x_c * y_c + c * y_c**2 - a_a_2 * a_b_2
110 return np.array([a, b, c, d, e, f])
113def ellipse_coefficients_canonical_form(
114 coefficients: ArrayLike,
115) -> NDArrayFloat:
116 """
117 Compute canonical form ellipse coefficients from the specified general
118 form ellipse coefficients.
120 The general form ellipse coefficients are the coefficients of the
121 implicit second-order polynomial/quadratic curve expressed as follows:
123 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0`
125 with an ellipse-specific constraint such as :math:`b^2 - 4ac < 0` and
126 where :math:`a, b, c, d, e, f` are the ellipse coefficients and
127 :math:`F\\left(x, y\\right)` are coordinates of points lying on the
128 ellipse.
130 Parameters
131 ----------
132 coefficients
133 General form ellipse coefficients.
135 Returns
136 -------
137 :class:`numpy.ndarray`
138 Canonical form ellipse coefficients.
140 References
141 ----------
142 :cite:`Wikipedia`
144 Examples
145 --------
146 >>> coefficients = np.array([2.5, -3.0, 2.5, -1.0, -1.0, -3.5])
147 >>> ellipse_coefficients_canonical_form(coefficients)
148 array([ 0.5, 0.5, 2. , 1. , 45. ])
149 """
151 a, b, c, d, e, f = tsplit(coefficients)
153 d_1 = b**2 - 4 * a * c
154 n_p_1 = 2 * (a * e**2 + c * d**2 - b * d * e + d_1 * f)
155 n_p_2 = np.sqrt((a - c) ** 2 + b**2)
157 a_a = (-np.sqrt(n_p_1 * (a + c + n_p_2))) / d_1
158 a_b = (-np.sqrt(n_p_1 * (a + c - n_p_2))) / d_1
160 x_c = (2 * c * d - b * e) / d_1
161 y_c = (2 * a * e - b * d) / d_1
163 theta = np.select(
164 [
165 np.logical_and(b == 0, a < c),
166 np.logical_and(b == 0, a > c),
167 b != 0,
168 ],
169 [
170 0,
171 90,
172 np.degrees(np.arctan((c - a - n_p_2) / b)),
173 ],
174 )
176 return np.array([x_c, y_c, a_a, a_b, theta])
179def point_at_angle_on_ellipse(phi: ArrayLike, coefficients: ArrayLike) -> NDArrayFloat:
180 """
181 Compute the coordinates of the point at angle :math:`\\phi` in degrees on
182 the ellipse with the specified canonical form coefficients.
184 Parameters
185 ----------
186 phi
187 Point at angle :math:`\\phi` in degrees to retrieve the coordinates
188 of.
189 coefficients
190 Canonical form ellipse coefficients as follows: the center coordinates
191 :math:`x_c` and :math:`y_c`, semi-major axis length :math:`a_a`,
192 semi-minor axis length :math:`a_b` and rotation angle :math:`\\theta`
193 in degrees of its semi-major axis :math:`a_a`.
195 Returns
196 -------
197 :class:`numpy.ndarray`
198 Coordinates of the point at angle :math:`\\phi`.
200 Examples
201 --------
202 >>> coefficients = np.array([0.5, 0.5, 2, 1, 45])
203 >>> point_at_angle_on_ellipse(45, coefficients) # doctest: +ELLIPSIS
204 array([ 1., 2.])
205 """
207 phi = np.radians(phi)
208 x_c, y_c, a_a, a_b, theta = tsplit(coefficients)
209 theta = np.radians(theta)
211 cos_phi = np.cos(phi)
212 sin_phi = np.sin(phi)
213 cos_theta = np.cos(theta)
214 sin_theta = np.sin(theta)
216 x = x_c + a_a * cos_theta * cos_phi - a_b * sin_theta * sin_phi
217 y = y_c + a_a * sin_theta * cos_phi + a_b * cos_theta * sin_phi
219 return tstack([x, y])
222def ellipse_fitting_Halir1998(a: ArrayLike) -> NDArrayFloat:
223 """
224 Compute the coefficients of the implicit second-order
225 polynomial/quadratic curve that fits the specified point array
226 :math:`a` using the *Halir and Flusser (1998)* method.
228 The implicit second-order polynomial is expressed as follows:
230 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0`
232 with an ellipse-specific constraint such as :math:`b^2 - 4ac < 0` and
233 where :math:`a, b, c, d, e, f` are coefficients of the ellipse and
234 :math:`F\\left(x, y\\right)` are coordinates of points lying on it.
236 Parameters
237 ----------
238 a
239 Point array :math:`a` to be fitted.
241 Returns
242 -------
243 :class:`numpy.ndarray`
244 Coefficients of the implicit second-order polynomial/quadratic curve
245 that fits the specified point array :math:`a`.
247 References
248 ----------
249 :cite:`Halir1998`
251 Examples
252 --------
253 >>> a = np.array([[2, 0], [0, 1], [-2, 0], [0, -1]])
254 >>> ellipse_fitting_Halir1998(a) # doctest: +ELLIPSIS
255 array([ 0.2425356..., 0. , 0.9701425..., 0. , 0. ,
256 -0.9701425...])
257 >>> ellipse_coefficients_canonical_form(ellipse_fitting_Halir1998(a))
258 array([-0., -0., 2., 1., 0.])
259 """
261 x, y = tsplit(a)
263 # Quadratic part of the design matrix.
264 D1 = tstack([x**2, x * y, y**2])
265 # Linear part of the design matrix.
266 D2 = tstack([x, y, ones(x.shape)])
268 D1_T = np.transpose(D1)
269 D2_T = np.transpose(D2)
271 # Quadratic part of the scatter matrix.
272 S1 = np.dot(D1_T, D1)
273 # Combined part of the scatter matrix.
274 S2 = np.dot(D1_T, D2)
275 # Linear part of the scatter matrix.
276 S3 = np.dot(D2_T, D2)
278 T = -np.dot(np.linalg.inv(S3), np.transpose(S2))
280 # Reduced scatter matrix.
281 M = S1 + np.dot(S2, T)
282 M = np.array([M[2, :] / 2, -M[1, :], M[0, :] / 2])
284 _w, v = np.linalg.eig(M)
286 A1 = v[:, np.nonzero(4 * v[0, :] * v[2, :] - v[1, :] ** 2 > 0)[0]]
287 A2 = np.dot(T, A1)
289 return cast("NDArrayFloat", np.ravel([A1, A2]))
292ELLIPSE_FITTING_METHODS: CanonicalMapping = CanonicalMapping(
293 {"Halir 1998": ellipse_fitting_Halir1998}
294)
295ELLIPSE_FITTING_METHODS.__doc__ = """
296Supported ellipse fitting methods.
298References
299----------
300:cite:`Halir1998`
301"""
304def ellipse_fitting(
305 a: ArrayLike, method: Literal["Halir 1998"] | str = "Halir 1998"
306) -> NDArrayFloat:
307 """
308 Compute the coefficients of the implicit second-order
309 polynomial/quadratic curve that fits the specified point array :math:`a`.
311 The implicit second-order polynomial is expressed as follows:
313 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0`
315 with an ellipse-specific constraint such as :math:`b^2 - 4ac < 0` and
316 where :math:`a, b, c, d, e, f` are coefficients of the ellipse and
317 :math:`F\\left(x, y\\right)` are coordinates of points lying on it.
319 Parameters
320 ----------
321 a
322 Point array :math:`a` to be fitted.
323 method
324 Computation method.
326 Returns
327 -------
328 :class:`numpy.ndarray`
329 Coefficients of the implicit second-order polynomial/quadratic curve
330 that fits the specified point array :math:`a`.
332 References
333 ----------
334 :cite:`Halir1998`
336 Examples
337 --------
338 >>> a = np.array([[2, 0], [0, 1], [-2, 0], [0, -1]])
339 >>> ellipse_fitting(a) # doctest: +ELLIPSIS
340 array([ 0.2425356..., 0. , 0.9701425..., 0. , 0. ,
341 -0.9701425...])
342 >>> ellipse_coefficients_canonical_form(ellipse_fitting(a))
343 array([-0., -0., 2., 1., 0.])
344 """
346 method = validate_method(method, tuple(ELLIPSE_FITTING_METHODS))
348 function = ELLIPSE_FITTING_METHODS[method]
350 return function(a)