Skip to content

Commit 46ac041

Browse files
committed
Add example
1 parent 1a8bcda commit 46ac041

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ test/*.txt
1919
# BLAS testing
2020
test/blas/*.SNAP
2121
test/blas/*.out
22+
23+
# example output
24+
example/output.svg

example/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Guilherme Lepsch
4+
Copyright (c) 2018 Ben Hammel
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

example/example.dart

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)