Coverage for geometry/ellipse.py: 74%

1"""

2 Ellipse

3=======

4

5 Define 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

12 References

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;\

17 jsessionid=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

22 from __future__ import annotations

23

24 import typing

25

26 import numpy as np

27

28 if typing.TYPE_CHECKING:

29 from colour.hints import ArrayLike, Literal

30

31 from colour.hints import NDArrayFloat, cast

32 from 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

57 def 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

113 def 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

179 def 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

222 def 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

292 ELLIPSE_FITTING_METHODS: CanonicalMapping = CanonicalMapping(

293 {"Halir 1998": ellipse_fitting_Halir1998}

294)

295 ELLIPSE_FITTING_METHODS.__doc__ = """

296 Supported ellipse fitting methods.

297

298 References

299----------

300:cite:`Halir1998`

301"""

302

303

304 def 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)