|
| 1 | +// Copyright (c) 2024 Guilherme Lepsch. All rights reserved. Use of this |
| 2 | +// source code is governed by a MIT license that can be found in the |
| 3 | +// [LICENSE file](https://github.com/lepsch/dart_lapack/blob/main/example/LICENSE). |
| 4 | + |
| 5 | +import 'dart:io'; |
| 6 | +import 'dart:math' as math; |
| 7 | +import 'package:dart_lapack/lapack.dart' as lapack; |
| 8 | + |
| 9 | +/// Least Squares fitting of Ellipses |
| 10 | +/// Based from Python version: https://github.com/bdhammel/least-squares-ellipse-fitting/ |
| 11 | +/// |
| 12 | +/// Fit the given [samples] to an ellipse. |
| 13 | +/// |
| 14 | +/// Return the estimated coefficients for the Least squares fit to the elliptical |
| 15 | +/// data containing the values [a,b,c,d,f,g] corresponding to Eqn 1 (*) |
| 16 | +/// ax**2 + bxy + cy**2 + dx + ey + f |
| 17 | +/// |
| 18 | +/// References |
| 19 | +/// ---------- |
| 20 | +/// (*) Halir R., Flusser J. 'Numerically Stable Direct Least Squares Fitting of Ellipses' |
| 21 | +/// (**) [Weisstein, Eric W. "Ellipse." From MathWorld--A Wolfram Web Resource](http://mathworld.wolfram.com/Ellipse.html) |
| 22 | +/// (***) https://mathworld.wolfram.com/InverseCotangent.html |
| 23 | +List<double> fit(List<(double, double)> samples) { |
| 24 | + assert(samples.length >= 5, |
| 25 | + 'Got ${samples.length} samples, 5 or more required.'); |
| 26 | + |
| 27 | + // extract x-y pairs |
| 28 | + final x = lapack.Array<double>.fromData( |
| 29 | + samples.map((sample) => sample.$1).toList()); |
| 30 | + final y = lapack.Array<double>.fromData( |
| 31 | + samples.map((sample) => sample.$2).toList()); |
| 32 | + |
| 33 | + final D1 = lapack.Matrix<double>(samples.length, 3); |
| 34 | + final D2 = lapack.Matrix<double>(samples.length, 3); |
| 35 | + for (var i = 1; i <= samples.length; i++) { |
| 36 | + // Quadratic part of design matrix [eqn. 15] from (*) |
| 37 | + D1[i][1] = x[i] * x[i]; |
| 38 | + D1[i][2] = x[i] * y[i]; |
| 39 | + D1[i][3] = y[i] * y[i]; |
| 40 | + // Linear part of design matrix [eqn. 16] from (*) |
| 41 | + D2[i][1] = x[i]; |
| 42 | + D2[i][2] = y[i]; |
| 43 | + D2[i][3] = 1; |
| 44 | + } |
| 45 | + |
| 46 | + // Forming scatter matrix [eqn. 17] from (*) |
| 47 | + final S1 = lapack.Matrix<double>(3, 3); |
| 48 | + final S2 = lapack.Matrix<double>(3, 3); |
| 49 | + final S3 = lapack.Matrix<double>(3, 3); |
| 50 | + // S1 = D1^(T) * D1 |
| 51 | + lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D1, D1.ld, D1, D1.ld, |
| 52 | + 0, S1, S1.ld); |
| 53 | + // S2 = D1^(T) * D2 |
| 54 | + lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D1, D1.ld, D2, D2.ld, |
| 55 | + 0, S2, S2.ld); |
| 56 | + // S3 = D2^(T) * D2 |
| 57 | + lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D2, D2.ld, D2, D2.ld, |
| 58 | + 0, S3, S3.ld); |
| 59 | + |
| 60 | + // Constraint matrix [eqn. 18] |
| 61 | + final C1 = lapack.Matrix.fromList([ |
| 62 | + [0.0, 0.0, 2.0], |
| 63 | + [0.0, -1.0, 0.0], |
| 64 | + [2.0, 0.0, 0.0] |
| 65 | + ]); |
| 66 | + |
| 67 | + // Reduced scatter matrix [eqn. 29] |
| 68 | + // M = C1^(-1) * (S1 - S2 * S3^(-1) * S2^(T)) |
| 69 | + final S11 = S1.copy(); |
| 70 | + lapack.dgemm( |
| 71 | + 'N', 'Transpose', 3, 3, 3, -1, dot(S2, inv(S3)), 3, S2, 3, 1, S11, 3); |
| 72 | + final M = dot(inv(C1), S11); |
| 73 | + |
| 74 | + // M*|a b c >=l|a b c >. Find eigenvalues and eigenvectors from this |
| 75 | + // equation [eqn. 28] |
| 76 | + const N = 3; |
| 77 | + final WR = lapack.Array<double>(N); |
| 78 | + final WI = lapack.Array<double>(N); |
| 79 | + final VL = lapack.Matrix<double>(N, N); |
| 80 | + final eigvec = lapack.Matrix<double>(N, N); |
| 81 | + final WORK = lapack.Array<double>(4 * N); |
| 82 | + final INFO = lapack.Box(0); |
| 83 | + lapack.dgeev('N', 'V', N, M, M.ld, WR, WI, VL, VL.ld, eigvec, eigvec.ld, WORK, |
| 84 | + 4 * N, INFO); |
| 85 | + |
| 86 | + // Eigenvector must meet constraint 4ac - b^2 to be valid. |
| 87 | + final cond = |
| 88 | + ScalarMultiply(4) * eigvec.row(1) * eigvec.row(3) - pow(eigvec.row(2), 2); |
| 89 | + final col = cond.indexWhere((x) => x > 0); |
| 90 | + final a1 = eigvec.col(col); |
| 91 | + |
| 92 | + // |d f g> = -S3^(-1) * S2^(T)*|a b c> [eqn. 24] |
| 93 | + final tmp = lapack.Matrix<double>(N, N); |
| 94 | + lapack.dgemm('N', 'Transpose', 3, 3, 3, 1, inv(-S3), 3, S2, 3, 1, tmp, 3); |
| 95 | + final a2 = lapack.Array<double>(N); |
| 96 | + lapack.dgemm( |
| 97 | + 'N', 'N', 3, 1, 3, 1, tmp, 3, a1.asMatrix(3), 3, 1, a2.asMatrix(3), 3); |
| 98 | + |
| 99 | + // Eigenvectors |a b c d f g> |
| 100 | + // list of the coefficients describing an ellipse [a,b,c,d,e,f] |
| 101 | + // corresponding to ax**2 + bxy + cy**2 + dx + ey + f from (*) |
| 102 | + return a1 + a2; |
| 103 | +} |
| 104 | + |
| 105 | +lapack.Matrix<double> inv(lapack.Matrix<double> A) { |
| 106 | + assert(A.dimensions.$1 == A.dimensions.$2); |
| 107 | + final ld = A.ld; |
| 108 | + final AInv = A.copy(); |
| 109 | + final IPIV = lapack.Array<int>(ld); |
| 110 | + final WORK = lapack.Array<double>(ld); |
| 111 | + final INFO = lapack.Box(0); |
| 112 | + lapack.dgetrf(ld, ld, AInv, ld, IPIV, INFO); |
| 113 | + assert(INFO.value == 0, 'Matrix is numerically singular'); |
| 114 | + lapack.dgetri(ld, AInv, ld, IPIV, WORK, ld, INFO); |
| 115 | + assert(INFO.value == 0, 'Matrix inversion failed'); |
| 116 | + return AInv; |
| 117 | +} |
| 118 | + |
| 119 | +lapack.Matrix<double> dot(lapack.Matrix<double> A, lapack.Matrix<double> B) { |
| 120 | + assert(A.dimensions.$2 == B.dimensions.$1); |
| 121 | + final K = A.dimensions.$2; |
| 122 | + final C = lapack.Matrix<double>(A.dimensions.$1, B.dimensions.$2); |
| 123 | + lapack.dgemm('N', 'N', A.dimensions.$1, B.dimensions.$2, K, 1, A, A.ld, B, |
| 124 | + B.ld, 0, C, C.ld); |
| 125 | + return C; |
| 126 | +} |
| 127 | + |
| 128 | +lapack.Array<double> pow(lapack.Array<double> a, num rhs) { |
| 129 | + final array = lapack.Array<double>(a.length); |
| 130 | + for (var i = 1; i <= a.length; i++) { |
| 131 | + array[i] = math.pow(a[i], rhs).toDouble(); |
| 132 | + } |
| 133 | + return array; |
| 134 | +} |
| 135 | + |
| 136 | +extension MatrixArray<T> on lapack.Matrix<T> { |
| 137 | + lapack.Array<T> row(int i) { |
| 138 | + final array = lapack.Array<T>(dimensions.$1); |
| 139 | + for (var j = 1; j <= dimensions.$2; j++) { |
| 140 | + array[j] = this[i][j]; |
| 141 | + } |
| 142 | + return array; |
| 143 | + } |
| 144 | + |
| 145 | + lapack.Array<T> col(int j) { |
| 146 | + final array = lapack.Array<T>(dimensions.$2); |
| 147 | + for (var i = 1; i <= dimensions.$2; i++) { |
| 148 | + array[i] = this[i][j]; |
| 149 | + } |
| 150 | + return array; |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +extension MatrixOp on lapack.Matrix<double> { |
| 155 | + lapack.Matrix<double> operator -() { |
| 156 | + final m = copy(); |
| 157 | + for (var i = 1; i <= dimensions.$2; i++) { |
| 158 | + for (var j = 1; j <= dimensions.$2; j++) { |
| 159 | + m[i][j] = -this[i][j]; |
| 160 | + } |
| 161 | + } |
| 162 | + return m; |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +extension VectorOperations on lapack.Array<double> { |
| 167 | + /// Vector product |
| 168 | + lapack.Array<double> operator *(lapack.Array<double> rhs) { |
| 169 | + assert(length == rhs.length); |
| 170 | + final array = lapack.Array<double>(length); |
| 171 | + for (var i = 1; i <= length; i++) { |
| 172 | + array[i] = this[i] * rhs[i]; |
| 173 | + } |
| 174 | + return array; |
| 175 | + } |
| 176 | + |
| 177 | + /// Vector subtract |
| 178 | + lapack.Array<double> operator -(lapack.Array<double> rhs) { |
| 179 | + assert(length == rhs.length); |
| 180 | + final array = copy(); |
| 181 | + for (var i = 1; i <= length; i++) { |
| 182 | + array[i] -= rhs[i]; |
| 183 | + } |
| 184 | + return array; |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +extension ScalarMultiply on num { |
| 189 | + lapack.Array<double> operator *(lapack.Array<double> rhs) { |
| 190 | + final array = lapack.Array<double>(rhs.length); |
| 191 | + for (var i = 1; i <= rhs.length; i++) { |
| 192 | + array[i] = this * rhs[i]; |
| 193 | + } |
| 194 | + return array; |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +/// Returns the definition of the fitted ellipse as localized parameters |
| 199 | +/// |
| 200 | +/// [center] is a tuple (x0, y0) |
| 201 | +/// [width] the total length (diameter) of horizontal axis. |
| 202 | +/// [height] the total length (diameter) of vertical axis. |
| 203 | +/// [phi] the counterclockwise angle (radians) of rotation from the x-axis to the semimajor axis |
| 204 | +((double, double) center, double width, double height, double phi) getParams( |
| 205 | + List<double> coefficients) { |
| 206 | + assert(coefficients.length == 6); |
| 207 | + // Eigenvectors are the coefficients of an ellipse in general form |
| 208 | + // the division by 2 is required to account for a slight difference in |
| 209 | + // the equations between (*) and (**) |
| 210 | + // a*x^2 + b*x*y + c*y^2 + d*x + e*y + f = 0 (*) Eqn 1 |
| 211 | + // a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0 (**) Eqn 15 |
| 212 | + // We'll use (**) to follow their documentation |
| 213 | + final a = coefficients[0], |
| 214 | + b = coefficients[1] / 2.0, |
| 215 | + c = coefficients[2], |
| 216 | + d = coefficients[3] / 2.0, |
| 217 | + f = coefficients[4] / 2.0, |
| 218 | + g = coefficients[5]; |
| 219 | + |
| 220 | + // Finding center of ellipse [eqn.19 and 20] from (**) |
| 221 | + final x0 = (c * d - b * f) / (math.pow(b, 2) - a * c), |
| 222 | + y0 = (a * f - b * d) / (math.pow(b, 2) - a * c); |
| 223 | + final center = (x0, y0); |
| 224 | + |
| 225 | + // Find the semi-axes lengths [eqn. 21 and 22] from (**) |
| 226 | + final numerator = 2 * |
| 227 | + (a * math.pow(f, 2) + |
| 228 | + c * math.pow(d, 2) + |
| 229 | + g * math.pow(b, 2) - |
| 230 | + 2 * b * d * f - |
| 231 | + a * c * g), |
| 232 | + denominator1 = (math.pow(b, 2) - a * c) * |
| 233 | + (math.sqrt(math.pow((a - c), 2) + 4 * math.pow(b, 2)) - (c + a)), |
| 234 | + denominator2 = (math.pow(b, 2) - a * c) * |
| 235 | + (-math.sqrt(math.pow((a - c), 2) + 4 * math.pow(b, 2)) - (c + a)); |
| 236 | + final height = math.sqrt(numerator / denominator1), |
| 237 | + width = math.sqrt(numerator / denominator2); |
| 238 | + |
| 239 | + // Angle of counterclockwise rotation of major-axis of ellipse to x-axis |
| 240 | + // [eqn. 23] from (**) |
| 241 | + // w/ trig identity eqn 9 form (***) |
| 242 | + final phi = switch (b) { |
| 243 | + 0 when a > c => 0.0, |
| 244 | + 0 when a < c => math.pi / 2, |
| 245 | + _ when a > c => 0.5 * math.atan(2 * b / (a - c)), |
| 246 | + _ when a < c => 0.5 * (math.pi + math.atan(2 * b / (a - c))), |
| 247 | + // Ellipse is a perfect circle, the answer is degenerate |
| 248 | + _ => 0.0, |
| 249 | + }; |
| 250 | + |
| 251 | + return (center, width, height, phi); |
| 252 | +} |
| 253 | + |
| 254 | +void main() { |
| 255 | + final samples = [ |
| 256 | + (19.59, 5.00), |
| 257 | + (17.71, 5.73), |
| 258 | + (16.16, 5.66), |
| 259 | + (14.15, 5.34), |
| 260 | + (12.14, 5.06), |
| 261 | + (10.04, 6.46), |
| 262 | + (8.19, 6.86), |
| 263 | + (5.55, 7.63), |
| 264 | + (4.07, 8.64), |
| 265 | + (4.19, 10.20), |
| 266 | + (4.66, 11.76), |
| 267 | + (5.55, 13.03), |
| 268 | + (7.63, 13.53), |
| 269 | + (9.41, 12.92), |
| 270 | + (10.56, 11.69), |
| 271 | + (11.23, 11.39), |
| 272 | + (13.26, 10.77), |
| 273 | + (15.93, 9.88), |
| 274 | + (18.30, 9.57), |
| 275 | + (20.67, 9.18), |
| 276 | + (22.67, 8.46), |
| 277 | + (23.93, 7.44), |
| 278 | + (23.55, 5.84), |
| 279 | + (21.86, 5.00), |
| 280 | + ]; |
| 281 | + final coefficients = fit(samples); |
| 282 | + final ( |
| 283 | + center, |
| 284 | + width, |
| 285 | + height, |
| 286 | + phi, |
| 287 | + ) = getParams(coefficients); |
| 288 | + |
| 289 | + final svg = File('output.svg'); |
| 290 | + svg.writeAsStringSync( |
| 291 | + '''<svg height="500" width="500" viewBox="0 0 30 15" xmlns="http://www.w3.org/2000/svg"> |
| 292 | +
|
| 293 | +<g fill="black"> |
| 294 | +${samples.map((sample) => '<circle cx="${sample.$1}" cy="${sample.$2}" r="0.1" />').join("\n")} |
| 295 | +</g> |
| 296 | +
|
| 297 | +<g stroke="blue" stroke-width="0.1" fill="transparent"> |
| 298 | +<ellipse cx="${center.$1}" cy="${center.$2}" rx="$width" ry="$height" transform="rotate(${phi * 180 / math.pi} ${center.$1} ${center.$2})"/> |
| 299 | +</g> |
| 300 | +
|
| 301 | +</svg> |
| 302 | + '''); |
| 303 | +} |
0 commit comments