Coverage for colour/geometry/ellipse.py: 100%

72 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Ellipse 

3======= 

4 

5Define objects for ellipse computations and fitting operations. 

6 

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` 

11 

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""" 

21 

22from __future__ import annotations 

23 

24import typing 

25 

26import numpy as np 

27 

28if typing.TYPE_CHECKING: 

29 from colour.hints import ArrayLike, Literal 

30 

31from colour.hints import NDArrayFloat, cast 

32from colour.utilities import ( 

33 CanonicalMapping, 

34 ones, 

35 tsplit, 

36 tstack, 

37 validate_method, 

38) 

39 

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" 

46 

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] 

55 

56 

57def ellipse_coefficients_general_form(coefficients: ArrayLike) -> NDArrayFloat: 

58 """ 

59 Compute general form ellipse coefficients from the specified canonical 

60 form ellipse coefficients. 

61 

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`. 

65 

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. 

70 

71 Parameters 

72 ---------- 

73 coefficients 

74 Canonical form ellipse coefficients as 

75 :math:`[x_c, y_c, a, b, \\theta]`. 

76 

77 Returns 

78 ------- 

79 :class:`numpy.ndarray` 

80 General form coefficients :math:`[A, B, C, D, E, F]`. 

81 

82 References 

83 ---------- 

84 :cite:`Wikipedia` 

85 

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 """ 

92 

93 x_c, y_c, a_a, a_b, theta = tsplit(coefficients) 

94 

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 

102 

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 

109 

110 return np.array([a, b, c, d, e, f]) 

111 

112 

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. 

119 

120 The general form ellipse coefficients are the coefficients of the 

121 implicit second-order polynomial/quadratic curve expressed as follows: 

122 

123 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0` 

124 

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. 

129 

130 Parameters 

131 ---------- 

132 coefficients 

133 General form ellipse coefficients. 

134 

135 Returns 

136 ------- 

137 :class:`numpy.ndarray` 

138 Canonical form ellipse coefficients. 

139 

140 References 

141 ---------- 

142 :cite:`Wikipedia` 

143 

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 """ 

150 

151 a, b, c, d, e, f = tsplit(coefficients) 

152 

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) 

156 

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 

159 

160 x_c = (2 * c * d - b * e) / d_1 

161 y_c = (2 * a * e - b * d) / d_1 

162 

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 ) 

175 

176 return np.array([x_c, y_c, a_a, a_b, theta]) 

177 

178 

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. 

183 

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`. 

194 

195 Returns 

196 ------- 

197 :class:`numpy.ndarray` 

198 Coordinates of the point at angle :math:`\\phi`. 

199 

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 """ 

206 

207 phi = np.radians(phi) 

208 x_c, y_c, a_a, a_b, theta = tsplit(coefficients) 

209 theta = np.radians(theta) 

210 

211 cos_phi = np.cos(phi) 

212 sin_phi = np.sin(phi) 

213 cos_theta = np.cos(theta) 

214 sin_theta = np.sin(theta) 

215 

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 

218 

219 return tstack([x, y]) 

220 

221 

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. 

227 

228 The implicit second-order polynomial is expressed as follows: 

229 

230 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0` 

231 

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. 

235 

236 Parameters 

237 ---------- 

238 a 

239 Point array :math:`a` to be fitted. 

240 

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`. 

246 

247 References 

248 ---------- 

249 :cite:`Halir1998` 

250 

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 """ 

260 

261 x, y = tsplit(a) 

262 

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)]) 

267 

268 D1_T = np.transpose(D1) 

269 D2_T = np.transpose(D2) 

270 

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) 

277 

278 T = -np.dot(np.linalg.inv(S3), np.transpose(S2)) 

279 

280 # Reduced scatter matrix. 

281 M = S1 + np.dot(S2, T) 

282 M = np.array([M[2, :] / 2, -M[1, :], M[0, :] / 2]) 

283 

284 _w, v = np.linalg.eig(M) 

285 

286 A1 = v[:, np.nonzero(4 * v[0, :] * v[2, :] - v[1, :] ** 2 > 0)[0]] 

287 A2 = np.dot(T, A1) 

288 

289 return cast("NDArrayFloat", np.ravel([A1, A2])) 

290 

291 

292ELLIPSE_FITTING_METHODS: CanonicalMapping = CanonicalMapping( 

293 {"Halir 1998": ellipse_fitting_Halir1998} 

294) 

295ELLIPSE_FITTING_METHODS.__doc__ = """ 

296Supported ellipse fitting methods. 

297 

298References 

299---------- 

300:cite:`Halir1998` 

301""" 

302 

303 

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`. 

310 

311 The implicit second-order polynomial is expressed as follows: 

312 

313 :math:`F\\left(x, y\\right) = ax^2 + bxy + cy^2 + dx + ey + f = 0` 

314 

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. 

318 

319 Parameters 

320 ---------- 

321 a 

322 Point array :math:`a` to be fitted. 

323 method 

324 Computation method. 

325 

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`. 

331 

332 References 

333 ---------- 

334 :cite:`Halir1998` 

335 

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 """ 

345 

346 method = validate_method(method, tuple(ELLIPSE_FITTING_METHODS)) 

347 

348 function = ELLIPSE_FITTING_METHODS[method] 

349 

350 return function(a)