pivtools 0.1.3__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. pivtools-0.1.3.dist-info/METADATA +222 -0
  2. pivtools-0.1.3.dist-info/RECORD +127 -0
  3. pivtools-0.1.3.dist-info/WHEEL +5 -0
  4. pivtools-0.1.3.dist-info/entry_points.txt +3 -0
  5. pivtools-0.1.3.dist-info/top_level.txt +3 -0
  6. pivtools_cli/__init__.py +5 -0
  7. pivtools_cli/_build_marker.c +25 -0
  8. pivtools_cli/_build_marker.cp311-win_amd64.pyd +0 -0
  9. pivtools_cli/cli.py +225 -0
  10. pivtools_cli/example.py +139 -0
  11. pivtools_cli/lib/PIV_2d_cross_correlate.c +334 -0
  12. pivtools_cli/lib/PIV_2d_cross_correlate.h +22 -0
  13. pivtools_cli/lib/common.h +36 -0
  14. pivtools_cli/lib/interp2custom.c +146 -0
  15. pivtools_cli/lib/interp2custom.h +48 -0
  16. pivtools_cli/lib/peak_locate_gsl.c +711 -0
  17. pivtools_cli/lib/peak_locate_gsl.h +40 -0
  18. pivtools_cli/lib/peak_locate_gsl_print.c +736 -0
  19. pivtools_cli/lib/peak_locate_lm.c +751 -0
  20. pivtools_cli/lib/peak_locate_lm.h +27 -0
  21. pivtools_cli/lib/xcorr.c +342 -0
  22. pivtools_cli/lib/xcorr.h +31 -0
  23. pivtools_cli/lib/xcorr_cache.c +78 -0
  24. pivtools_cli/lib/xcorr_cache.h +26 -0
  25. pivtools_cli/piv/interp2custom/interp2custom.py +69 -0
  26. pivtools_cli/piv/piv.py +240 -0
  27. pivtools_cli/piv/piv_backend/base.py +825 -0
  28. pivtools_cli/piv/piv_backend/cpu_instantaneous.py +1005 -0
  29. pivtools_cli/piv/piv_backend/factory.py +28 -0
  30. pivtools_cli/piv/piv_backend/gpu_instantaneous.py +15 -0
  31. pivtools_cli/piv/piv_backend/infilling.py +445 -0
  32. pivtools_cli/piv/piv_backend/outlier_detection.py +306 -0
  33. pivtools_cli/piv/piv_backend/profile_cpu_instantaneous.py +230 -0
  34. pivtools_cli/piv/piv_result.py +40 -0
  35. pivtools_cli/piv/save_results.py +342 -0
  36. pivtools_cli/piv_cluster/cluster.py +108 -0
  37. pivtools_cli/preprocessing/filters.py +399 -0
  38. pivtools_cli/preprocessing/preprocess.py +79 -0
  39. pivtools_cli/tests/helpers.py +107 -0
  40. pivtools_cli/tests/instantaneous_piv/test_piv_integration.py +167 -0
  41. pivtools_cli/tests/instantaneous_piv/test_piv_integration_multi.py +553 -0
  42. pivtools_cli/tests/preprocessing/test_filters.py +41 -0
  43. pivtools_core/__init__.py +5 -0
  44. pivtools_core/config.py +703 -0
  45. pivtools_core/config.yaml +135 -0
  46. pivtools_core/image_handling/__init__.py +0 -0
  47. pivtools_core/image_handling/load_images.py +464 -0
  48. pivtools_core/image_handling/readers/__init__.py +53 -0
  49. pivtools_core/image_handling/readers/generic_readers.py +50 -0
  50. pivtools_core/image_handling/readers/lavision_reader.py +190 -0
  51. pivtools_core/image_handling/readers/registry.py +24 -0
  52. pivtools_core/paths.py +49 -0
  53. pivtools_core/vector_loading.py +248 -0
  54. pivtools_gui/__init__.py +3 -0
  55. pivtools_gui/app.py +687 -0
  56. pivtools_gui/calibration/__init__.py +0 -0
  57. pivtools_gui/calibration/app/__init__.py +0 -0
  58. pivtools_gui/calibration/app/views.py +1186 -0
  59. pivtools_gui/calibration/calibration_planar/planar_calibration_production.py +570 -0
  60. pivtools_gui/calibration/vector_calibration_production.py +544 -0
  61. pivtools_gui/config.py +703 -0
  62. pivtools_gui/image_handling/__init__.py +0 -0
  63. pivtools_gui/image_handling/load_images.py +464 -0
  64. pivtools_gui/image_handling/readers/__init__.py +53 -0
  65. pivtools_gui/image_handling/readers/generic_readers.py +50 -0
  66. pivtools_gui/image_handling/readers/lavision_reader.py +190 -0
  67. pivtools_gui/image_handling/readers/registry.py +24 -0
  68. pivtools_gui/masking/__init__.py +0 -0
  69. pivtools_gui/masking/app/__init__.py +0 -0
  70. pivtools_gui/masking/app/views.py +123 -0
  71. pivtools_gui/paths.py +49 -0
  72. pivtools_gui/piv_runner.py +261 -0
  73. pivtools_gui/pivtools.py +58 -0
  74. pivtools_gui/plotting/__init__.py +0 -0
  75. pivtools_gui/plotting/app/__init__.py +0 -0
  76. pivtools_gui/plotting/app/views.py +1671 -0
  77. pivtools_gui/plotting/plot_maker.py +220 -0
  78. pivtools_gui/post_processing/POD/__init__.py +0 -0
  79. pivtools_gui/post_processing/POD/app/__init__.py +0 -0
  80. pivtools_gui/post_processing/POD/app/views.py +647 -0
  81. pivtools_gui/post_processing/POD/pod_decompose.py +979 -0
  82. pivtools_gui/post_processing/POD/views.py +1096 -0
  83. pivtools_gui/post_processing/__init__.py +0 -0
  84. pivtools_gui/static/404.html +1 -0
  85. pivtools_gui/static/_next/static/chunks/117-d5793c8e79de5511.js +2 -0
  86. pivtools_gui/static/_next/static/chunks/484-cfa8b9348ce4f00e.js +1 -0
  87. pivtools_gui/static/_next/static/chunks/869-320a6b9bdafbb6d3.js +1 -0
  88. pivtools_gui/static/_next/static/chunks/app/_not-found/page-12f067ceb7415e55.js +1 -0
  89. pivtools_gui/static/_next/static/chunks/app/layout-b907d5f31ac82e9d.js +1 -0
  90. pivtools_gui/static/_next/static/chunks/app/page-334cc4e8444cde2f.js +1 -0
  91. pivtools_gui/static/_next/static/chunks/fd9d1056-ad15f396ddf9b7e5.js +1 -0
  92. pivtools_gui/static/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  93. pivtools_gui/static/_next/static/chunks/main-a1b3ced4d5f6d998.js +1 -0
  94. pivtools_gui/static/_next/static/chunks/main-app-8a63c6f5e7baee11.js +1 -0
  95. pivtools_gui/static/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  96. pivtools_gui/static/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  97. pivtools_gui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  98. pivtools_gui/static/_next/static/chunks/webpack-4a8ca7c99e9bb3d8.js +1 -0
  99. pivtools_gui/static/_next/static/css/7d3f2337d7ea12a5.css +3 -0
  100. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_buildManifest.js +1 -0
  101. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_ssgManifest.js +1 -0
  102. pivtools_gui/static/file.svg +1 -0
  103. pivtools_gui/static/globe.svg +1 -0
  104. pivtools_gui/static/grid.svg +8 -0
  105. pivtools_gui/static/index.html +1 -0
  106. pivtools_gui/static/index.txt +8 -0
  107. pivtools_gui/static/next.svg +1 -0
  108. pivtools_gui/static/vercel.svg +1 -0
  109. pivtools_gui/static/window.svg +1 -0
  110. pivtools_gui/stereo_reconstruction/__init__.py +0 -0
  111. pivtools_gui/stereo_reconstruction/app/__init__.py +0 -0
  112. pivtools_gui/stereo_reconstruction/app/views.py +1985 -0
  113. pivtools_gui/stereo_reconstruction/stereo_calibration_production.py +606 -0
  114. pivtools_gui/stereo_reconstruction/stereo_reconstruction_production.py +544 -0
  115. pivtools_gui/utils.py +63 -0
  116. pivtools_gui/vector_loading.py +248 -0
  117. pivtools_gui/vector_merging/__init__.py +1 -0
  118. pivtools_gui/vector_merging/app/__init__.py +1 -0
  119. pivtools_gui/vector_merging/app/views.py +759 -0
  120. pivtools_gui/vector_statistics/app/__init__.py +1 -0
  121. pivtools_gui/vector_statistics/app/views.py +710 -0
  122. pivtools_gui/vector_statistics/ensemble_statistics.py +49 -0
  123. pivtools_gui/vector_statistics/instantaneous_statistics.py +311 -0
  124. pivtools_gui/video_maker/__init__.py +0 -0
  125. pivtools_gui/video_maker/app/__init__.py +0 -0
  126. pivtools_gui/video_maker/app/views.py +436 -0
  127. pivtools_gui/video_maker/video_maker.py +662 -0
@@ -0,0 +1,751 @@
1
+ #include "peak_locate_lm.h"
2
+ #include "common.h"
3
+ #include <stdio.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+ #include <math.h>
7
+ #include <float.h>
8
+
9
+ /******************************************************************************
10
+ * Lightweight Levenberg-Marquardt Implementation for Gaussian Peak Fitting
11
+ *
12
+ * OVERVIEW:
13
+ * This module provides a fast, numerically stable implementation of Gaussian
14
+ * peak fitting for Particle Image Velocimetry (PIV) correlation analysis.
15
+ * It uses the Levenberg-Marquardt algorithm to fit 2D Gaussian models to
16
+ * correlation peaks, enabling sub-pixel displacement accuracy.
17
+ *
18
+ * SUPPORTED GAUSSIAN MODELS:
19
+ * - 3-point parabolic estimator (fast fallback/initial guess)
20
+ * - 4-DOF: Circular Gaussian (A, i0, j0, s)
21
+ * - 5-DOF: Elliptical Gaussian (A, i0, j0, sx, sy)
22
+ * - 6-DOF: Rotated elliptical Gaussian with correlation (A, i0, j0, sx, sy, sxy)
23
+ *
24
+ * ALGORITHM OVERVIEW:
25
+ * 1. Initial peak detection using brute-force search in central region
26
+ * 2. Sub-window extraction around detected peak
27
+ * 3. Initial parameter estimation using 3-point parabolic fit
28
+ * 4. Levenberg-Marquardt nonlinear least squares optimization
29
+ * 5. Peak subtraction for multi-peak detection
30
+ *
31
+ * KEY FEATURES:
32
+ * - Fast Cholesky-based linear system solving
33
+ * - Robust parameter bounds and damping
34
+ * - Automatic model selection based on fit type
35
+ * - Multi-peak capability with sequential fitting
36
+ *
37
+ * TECHNICAL NOTES:
38
+ * - Uses inverse covariance parameterization for 6-DOF model
39
+ * - Implements trust-region Levenberg-Marquardt with adaptive damping
40
+ * - Memory efficient with fixed-size matrices (max 6x6)
41
+ * - Thread-safe (no global state)
42
+ *
43
+ ******************************************************************************/
44
+
45
+ /* Fast 3-point parabolic estimator - used as initial guess and fallback */
46
+ static void threept_estimate(const float *xcorr, const int *N, float *peak_loc, float *A_out, float *sx_out, float *sy_out)
47
+ {
48
+ float x_fit[3], y_fit[3];
49
+ int i;
50
+
51
+ /* Extract 3 points along each axis centered on the correlation peak */
52
+ for(i = 0; i < 3; ++i)
53
+ {
54
+ x_fit[i] = xcorr[(i - 1 + (N[0]-1)/2) * N[1] + (N[1]-1)/2];
55
+ y_fit[i] = xcorr[(N[0]-1)/2 * N[1] + (i - 1 + (N[1]-1)/2)];
56
+ /* Convert to log space for parabolic fitting (handles exponential decay) */
57
+ x_fit[i] = (float)log((x_fit[i] < FLT_EPSILON) ? FLT_EPSILON : x_fit[i]);
58
+ y_fit[i] = (float)log((y_fit[i] < FLT_EPSILON) ? FLT_EPSILON : y_fit[i]);
59
+ }
60
+
61
+ /* Parabolic fit: peak location is at i0 = numer/denom */
62
+ float denom_x = 2*x_fit[0] - 4*x_fit[1] + 2*x_fit[2];
63
+ float denom_y = 2*y_fit[0] - 4*y_fit[1] + 2*y_fit[2];
64
+
65
+ if(fabs(denom_x) > FLT_EPSILON)
66
+ peak_loc[0] = (x_fit[0] - x_fit[2]) / denom_x;
67
+ else
68
+ peak_loc[0] = 0.0f;
69
+
70
+ if(fabs(denom_y) > FLT_EPSILON)
71
+ peak_loc[1] = (y_fit[0] - y_fit[2]) / denom_y;
72
+ else
73
+ peak_loc[1] = 0.0f;
74
+
75
+ *A_out = xcorr[(N[0]-1)/2 * N[1] + (N[1]-1)/2];
76
+ *sx_out = (float)sqrt(-4.0f / (denom_x + FLT_EPSILON));
77
+ *sy_out = (float)sqrt(-4.0f / (denom_y + FLT_EPSILON));
78
+ }
79
+
80
+ /* Evaluate 4-DOF Gaussian: A * exp(-((i-i0)^2 + (j-j0)^2)/s^2) - circular Gaussian */
81
+ static inline float eval_gauss4(float i, float j, float A, float i0, float j0, float s)
82
+ {
83
+ float di = (i - i0) / s;
84
+ float dj = (j - j0) / s;
85
+ return A * expf(-(di*di + dj*dj));
86
+ }
87
+
88
+ /* Evaluate 5-DOF Gaussian: A * exp(-((i-i0)^2/sx^2 + (j-j0)^2/sy^2)) - elliptical */
89
+ static inline float eval_gauss5(float i, float j, float A, float i0, float j0, float sx, float sy)
90
+ {
91
+ float di = (i - i0) / sx;
92
+ float dj = (j - j0) / sy;
93
+ return A * expf(-(di*di + dj*dj));
94
+ }
95
+
96
+ /* Evaluate 6-DOF Gaussian with correlation term - rotated elliptical
97
+ * NOTE: sx, sy, sxy are elements of the INVERSE covariance matrix, not standard deviations
98
+ * Model: A * exp(-0.5 * (di^2/sx + dj^2/sy + 2*di*dj*sxy))
99
+ * where sx = sigma_x^2, sy = sigma_y^2 in the inverse covariance representation
100
+ */
101
+ static inline float eval_gauss6(float i, float j, float A, float i0, float j0, float sx, float sy, float sxy)
102
+ {
103
+ float di = i - i0;
104
+ float dj = j - j0;
105
+ return A * expf(-0.5f * (di*di/sx + dj*dj/sy + 2.0f*di*dj*sxy));
106
+ }
107
+
108
+ /* Compute residual and Jacobian for 4-DOF Gaussian fit */
109
+ static float compute_residual_jacobian_4dof(
110
+ const float *xcorr, const int *N,
111
+ float A, float i0, float j0, float s,
112
+ float *JtJ, float *Jtr, int compute_jacobian)
113
+ {
114
+ int ii, jj, idx;
115
+ float residual_sum = 0.0f;
116
+ const int n_params = 4;
117
+
118
+ if(compute_jacobian) {
119
+ memset(JtJ, 0, n_params * n_params * sizeof(float));
120
+ memset(Jtr, 0, n_params * sizeof(float));
121
+ }
122
+
123
+ for(ii = 0; ii < N[0]; ++ii) {
124
+ float i = (float)(ii - (N[0]-1)/2);
125
+
126
+ for(jj = 0; jj < N[1]; ++jj) {
127
+ float j = (float)(jj - (N[1]-1)/2);
128
+ idx = ii * N[1] + jj;
129
+
130
+ float pred = eval_gauss4(i, j, A, i0, j0, s);
131
+ float r = pred - xcorr[idx];
132
+ residual_sum += r * r;
133
+
134
+ if(compute_jacobian) {
135
+ float di = (i - i0) / s;
136
+ float dj = (j - j0) / s;
137
+ float r2 = di*di + dj*dj;
138
+
139
+ /* Compute Jacobian matrix elements (partial derivatives) */
140
+ float J[4];
141
+ J[0] = pred / A; /* dF/dA */
142
+ J[1] = 2.0f * pred * di / s; /* dF/di0 */
143
+ J[2] = 2.0f * pred * dj / s; /* dF/dj0 */
144
+ J[3] = 2.0f * pred * r2 / s; /* dF/ds */
145
+
146
+ /* Accumulate J^T * J and J^T * r for normal equations */
147
+ for(int p1 = 0; p1 < n_params; ++p1) {
148
+ Jtr[p1] += J[p1] * r;
149
+ for(int p2 = 0; p2 <= p1; ++p2) {
150
+ JtJ[p1 * n_params + p2] += J[p1] * J[p2];
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ if(compute_jacobian) {
158
+ for(int p1 = 0; p1 < n_params; ++p1) {
159
+ for(int p2 = p1 + 1; p2 < n_params; ++p2) {
160
+ JtJ[p1 * n_params + p2] = JtJ[p2 * n_params + p1];
161
+ }
162
+ }
163
+ }
164
+
165
+ return residual_sum;
166
+ }
167
+
168
+ /* Compute residual and Jacobian for 5-DOF Gaussian fit */
169
+ static float compute_residual_jacobian_5dof(
170
+ const float *xcorr, const int *N,
171
+ float A, float i0, float j0, float sx, float sy,
172
+ float *JtJ, float *Jtr, int compute_jacobian)
173
+ {
174
+ int ii, jj, idx;
175
+ float residual_sum = 0.0f;
176
+ const int n_params = 5;
177
+
178
+ if(compute_jacobian) {
179
+ memset(JtJ, 0, n_params * n_params * sizeof(float));
180
+ memset(Jtr, 0, n_params * sizeof(float));
181
+ }
182
+
183
+ for(ii = 0; ii < N[0]; ++ii) {
184
+ float i = (float)(ii - (N[0]-1)/2);
185
+
186
+ for(jj = 0; jj < N[1]; ++jj) {
187
+ float j = (float)(jj - (N[1]-1)/2);
188
+ idx = ii * N[1] + jj;
189
+
190
+ float pred = eval_gauss5(i, j, A, i0, j0, sx, sy);
191
+ float r = pred - xcorr[idx];
192
+ residual_sum += r * r;
193
+
194
+ if(compute_jacobian) {
195
+ float di = (i - i0) / sx;
196
+ float dj = (j - j0) / sy;
197
+
198
+ /* Compute Jacobian matrix elements (partial derivatives) */
199
+ float J[5];
200
+ J[0] = pred / A; /* dF/dA */
201
+ J[1] = 2.0f * pred * di / sx; /* dF/di0 */
202
+ J[2] = 2.0f * pred * dj / sy; /* dF/dj0 */
203
+ J[3] = 2.0f * pred * di * di / sx; /* dF/dsx */
204
+ J[4] = 2.0f * pred * dj * dj / sy; /* dF/dsy */
205
+
206
+ /* Accumulate J^T * J and J^T * r for normal equations */
207
+ for(int p1 = 0; p1 < n_params; ++p1) {
208
+ Jtr[p1] += J[p1] * r;
209
+ for(int p2 = 0; p2 <= p1; ++p2) {
210
+ JtJ[p1 * n_params + p2] += J[p1] * J[p2];
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ if(compute_jacobian) {
218
+ for(int p1 = 0; p1 < n_params; ++p1) {
219
+ for(int p2 = p1 + 1; p2 < n_params; ++p2) {
220
+ JtJ[p1 * n_params + p2] = JtJ[p2 * n_params + p1];
221
+ }
222
+ }
223
+ }
224
+
225
+ return residual_sum;
226
+ }
227
+
228
+ /* Compute residual and Jacobian for 6-DOF Gaussian fit */
229
+ static float compute_residual_jacobian_6dof(
230
+ const float *xcorr, const int *N,
231
+ float A, float i0, float j0, float sx, float sy, float sxy,
232
+ float *JtJ, float *Jtr, int compute_jacobian)
233
+ {
234
+ int ii, jj, idx;
235
+ float residual_sum = 0.0f;
236
+ const int n_params = 6;
237
+
238
+ if(compute_jacobian) {
239
+ memset(JtJ, 0, n_params * n_params * sizeof(float));
240
+ memset(Jtr, 0, n_params * sizeof(float));
241
+ }
242
+
243
+ for(ii = 0; ii < N[0]; ++ii) {
244
+ float i = (float)(ii - (N[0]-1)/2);
245
+
246
+ for(jj = 0; jj < N[1]; ++jj) {
247
+ float j = (float)(jj - (N[1]-1)/2);
248
+ idx = ii * N[1] + jj;
249
+
250
+ float pred = eval_gauss6(i, j, A, i0, j0, sx, sy, sxy);
251
+ float r = pred - xcorr[idx];
252
+ residual_sum += r * r;
253
+
254
+ if(compute_jacobian) {
255
+ float di = i - i0;
256
+ float dj = j - j0;
257
+
258
+ /* Compute Jacobian matrix elements (partial derivatives) */
259
+ float J[6];
260
+ J[0] = pred / A; /* dF/dA */
261
+ J[1] = pred * (di/sx + dj*sxy); /* dF/di0 */
262
+ J[2] = pred * (dj/sy + di*sxy); /* dF/dj0 */
263
+ J[3] = 0.5f * pred * di * di / (sx * sx); /* dF/dsx - */
264
+ J[4] = 0.5f * pred * dj * dj / (sy * sy); /* dF/dsy - */
265
+ J[5] = -pred * di * dj; /* dF/dsxy */
266
+
267
+ /* Accumulate J^T * J and J^T * r for normal equations */
268
+ for(int p1 = 0; p1 < n_params; ++p1) {
269
+ Jtr[p1] += J[p1] * r;
270
+ for(int p2 = 0; p2 <= p1; ++p2) {
271
+ JtJ[p1 * n_params + p2] += J[p1] * J[p2];
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ if(compute_jacobian) {
279
+ for(int p1 = 0; p1 < n_params; ++p1) {
280
+ for(int p2 = p1 + 1; p2 < n_params; ++p2) {
281
+ JtJ[p1 * n_params + p2] = JtJ[p2 * n_params + p1];
282
+ }
283
+ }
284
+ }
285
+
286
+ return residual_sum;
287
+ }
288
+
289
+ /* Solve (JtJ + lambda*diag(JtJ)) * delta = -Jtr using Cholesky decomposition */
290
+ static int solve_lm_step(const float *JtJ, const float *Jtr, float lambda, float *delta, int n)
291
+ {
292
+ float A[36]; /* Max 6x6 matrix */
293
+ float L[36];
294
+ float y[6];
295
+ int i, j, k;
296
+
297
+ /* Form damped normal matrix: A = JtJ + lambda * diag(JtJ) */
298
+ memcpy(A, JtJ, n * n * sizeof(float));
299
+ for(i = 0; i < n; ++i) {
300
+ A[i * n + i] *= (1.0f + lambda);
301
+ }
302
+
303
+ /* Cholesky decomposition: A = L * L^T */
304
+ memset(L, 0, n * n * sizeof(float));
305
+ for(i = 0; i < n; ++i) {
306
+ for(j = 0; j <= i; ++j) {
307
+ float sum = A[i * n + j];
308
+ for(k = 0; k < j; ++k) {
309
+ sum -= L[i * n + k] * L[j * n + k];
310
+ }
311
+ if(i == j) {
312
+ if(sum <= 0.0f) return -1; /* Matrix not positive definite */
313
+ L[i * n + j] = sqrtf(sum);
314
+ } else {
315
+ L[i * n + j] = sum / L[j * n + j];
316
+ }
317
+ }
318
+ }
319
+
320
+ /* Forward substitution: L * y = -Jtr */
321
+ for(i = 0; i < n; ++i) {
322
+ float sum = -Jtr[i];
323
+ for(j = 0; j < i; ++j) {
324
+ sum -= L[i * n + j] * y[j];
325
+ }
326
+ y[i] = sum / L[i * n + i];
327
+ }
328
+
329
+ /* Back substitution: L^T * delta = y */
330
+ for(i = n - 1; i >= 0; --i) {
331
+ float sum = y[i];
332
+ for(j = i + 1; j < n; ++j) {
333
+ sum -= L[j * n + i] * delta[j];
334
+ }
335
+ delta[i] = sum / L[i * n + i];
336
+ }
337
+
338
+ return 0;
339
+ }
340
+
341
+ /* Fast Levenberg-Marquardt for 4-DOF Gaussian fitting (circular) */
342
+ static void lm_gauss4_fit(const float *xcorr, const int *N, float *peak_loc, float *fitval, float *sig)
343
+ {
344
+ float A, i0, j0, s;
345
+ float JtJ[16], Jtr[4], delta[4];
346
+ float lambda = 0.01f;
347
+ float residual, new_residual;
348
+ int iter, ii, jj, idx;
349
+ const int max_iter = 20;
350
+ const float tol = 1e-6f;
351
+
352
+ /* Get initial guess using 3-point parabolic estimator */
353
+ float sx, sy;
354
+ threept_estimate(xcorr, N, peak_loc, &A, &sx, &sy);
355
+ i0 = peak_loc[0];
356
+ j0 = peak_loc[1];
357
+ s = sqrtf(sx * sx + sy * sy); /* Combined width */
358
+
359
+ /* Clamp parameters to reasonable bounds */
360
+ i0 = fminf(fmaxf(i0, -2.0f), 2.0f);
361
+ j0 = fminf(fmaxf(j0, -2.0f), 2.0f);
362
+ s = fminf(fmaxf(s, 0.5f), 3.0f);
363
+
364
+ /* Compute initial residual and Jacobian */
365
+ residual = compute_residual_jacobian_4dof(xcorr, N, A, i0, j0, s, JtJ, Jtr, 1);
366
+
367
+ /* Levenberg-Marquardt optimization loop */
368
+ for(iter = 0; iter < max_iter; ++iter) {
369
+ /* Solve for parameter update */
370
+ if(solve_lm_step(JtJ, Jtr, lambda, delta, 4) != 0) break;
371
+
372
+ /* Apply parameter update with bounds checking */
373
+ float A_new = A + delta[0];
374
+ float i0_new = i0 + delta[1];
375
+ float j0_new = j0 + delta[2];
376
+ float s_new = s + delta[3];
377
+
378
+ A_new = fmaxf(A_new, A * 0.5f);
379
+ i0_new = fminf(fmaxf(i0_new, -2.5f), 2.5f);
380
+ j0_new = fminf(fmaxf(j0_new, -2.5f), 2.5f);
381
+ s_new = fminf(fmaxf(s_new, 0.25f), 4.0f);
382
+
383
+ /* Evaluate new residual */
384
+ new_residual = compute_residual_jacobian_4dof(xcorr, N, A_new, i0_new, j0_new, s_new, NULL, NULL, 0);
385
+
386
+ if(new_residual < residual) {
387
+ /* Accept step: update parameters */
388
+ A = A_new; i0 = i0_new; j0 = j0_new; s = s_new;
389
+ float improvement = (residual - new_residual) / (residual + FLT_EPSILON);
390
+ residual = new_residual;
391
+ lambda *= 0.5f; /* Decrease damping */
392
+ compute_residual_jacobian_4dof(xcorr, N, A, i0, j0, s, JtJ, Jtr, 1);
393
+ if(improvement < tol) break; /* Converged */
394
+ } else {
395
+ lambda *= 2.0f; /* Increase damping */
396
+ if(lambda > 1e6f) break; /* Diverged */
397
+ }
398
+ }
399
+
400
+ /* Store final results */
401
+ peak_loc[0] = i0;
402
+ peak_loc[1] = j0;
403
+ sig[0] = s;
404
+ sig[1] = s;
405
+ sig[2] = 0.0f;
406
+
407
+ /* Generate fitted values if requested */
408
+ if(fitval) {
409
+ for(ii = 0; ii < N[0]; ++ii) {
410
+ float i = (float)(ii - (N[0]-1)/2);
411
+ for(jj = 0; jj < N[1]; ++jj) {
412
+ float j = (float)(jj - (N[1]-1)/2);
413
+ idx = ii * N[1] + jj;
414
+ fitval[idx] = eval_gauss4(i, j, A, i0, j0, s);
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ /* Fast Levenberg-Marquardt for 5-DOF Gaussian fitting */
421
+ static void lm_gauss5_fit(const float *xcorr, const int *N, float *peak_loc, float *fitval, float *sig)
422
+ {
423
+ float A, i0, j0, sx, sy;
424
+ float JtJ[25], Jtr[5], delta[5];
425
+ float lambda = 0.01f;
426
+ float residual, new_residual;
427
+ int iter, ii, jj, idx;
428
+ const int max_iter = 20;
429
+ const float tol = 1e-6f;
430
+
431
+ /* Get initial guess using 3-point parabolic estimator */
432
+ threept_estimate(xcorr, N, peak_loc, &A, &sx, &sy);
433
+ i0 = peak_loc[0];
434
+ j0 = peak_loc[1];
435
+
436
+ /* Clamp parameters to reasonable bounds */
437
+ i0 = fminf(fmaxf(i0, -2.0f), 2.0f);
438
+ j0 = fminf(fmaxf(j0, -2.0f), 2.0f);
439
+ sx = fminf(fmaxf(sx, 0.5f), 3.0f);
440
+ sy = fminf(fmaxf(sy, 0.5f), 3.0f);
441
+
442
+ /* Compute initial residual and Jacobian */
443
+ residual = compute_residual_jacobian_5dof(xcorr, N, A, i0, j0, sx, sy, JtJ, Jtr, 1);
444
+
445
+ /* Levenberg-Marquardt optimization loop */
446
+ for(iter = 0; iter < max_iter; ++iter) {
447
+ /* Solve for parameter update */
448
+ if(solve_lm_step(JtJ, Jtr, lambda, delta, 5) != 0) break;
449
+
450
+ /* Apply parameter update with bounds checking */
451
+ float A_new = A + delta[0];
452
+ float i0_new = i0 + delta[1];
453
+ float j0_new = j0 + delta[2];
454
+ float sx_new = sx + delta[3];
455
+ float sy_new = sy + delta[4];
456
+
457
+ A_new = fmaxf(A_new, A * 0.5f);
458
+ i0_new = fminf(fmaxf(i0_new, -2.5f), 2.5f);
459
+ j0_new = fminf(fmaxf(j0_new, -2.5f), 2.5f);
460
+ sx_new = fminf(fmaxf(sx_new, 0.25f), 4.0f);
461
+ sy_new = fminf(fmaxf(sy_new, 0.25f), 4.0f);
462
+
463
+ /* Evaluate new residual */
464
+ new_residual = compute_residual_jacobian_5dof(xcorr, N, A_new, i0_new, j0_new, sx_new, sy_new, NULL, NULL, 0);
465
+
466
+ if(new_residual < residual) {
467
+ /* Accept step: update parameters */
468
+ A = A_new; i0 = i0_new; j0 = j0_new; sx = sx_new; sy = sy_new;
469
+ float improvement = (residual - new_residual) / (residual + FLT_EPSILON);
470
+ residual = new_residual;
471
+ lambda *= 0.5f; /* Decrease damping */
472
+ compute_residual_jacobian_5dof(xcorr, N, A, i0, j0, sx, sy, JtJ, Jtr, 1);
473
+ if(improvement < tol) break; /* Converged */
474
+ } else {
475
+ lambda *= 2.0f; /* Increase damping */
476
+ if(lambda > 1e6f) break; /* Diverged */
477
+ }
478
+ }
479
+
480
+ /* Store final results */
481
+ peak_loc[0] = i0;
482
+ peak_loc[1] = j0;
483
+ sig[0] = sx;
484
+ sig[1] = sy;
485
+ sig[2] = 0.0f;
486
+
487
+ /* Generate fitted values if requested */
488
+ if(fitval) {
489
+ for(ii = 0; ii < N[0]; ++ii) {
490
+ float i = (float)(ii - (N[0]-1)/2);
491
+ for(jj = 0; jj < N[1]; ++jj) {
492
+ float j = (float)(jj - (N[1]-1)/2);
493
+ idx = ii * N[1] + jj;
494
+ fitval[idx] = eval_gauss5(i, j, A, i0, j0, sx, sy);
495
+ }
496
+ }
497
+ }
498
+ }
499
+
500
+ /* Fast Levenberg-Marquardt for 6-DOF Gaussian fitting
501
+ */
502
+ static void lm_gauss6_fit(const float *xcorr, const int *N, float *peak_loc, float *fitval, float *sig)
503
+ {
504
+ float A, i0, j0, sx, sy, sxy;
505
+ float JtJ[36], Jtr[6], delta[6];
506
+ float lambda = 0.01f;
507
+ float residual, new_residual;
508
+ int iter, ii, jj, idx;
509
+ const int max_iter = 20;
510
+ const float tol = 1e-6f;
511
+
512
+ /* Get initial guess using 3-point parabolic estimator */
513
+ threept_estimate(xcorr, N, peak_loc, &A, &sx, &sy);
514
+ i0 = peak_loc[0];
515
+ j0 = peak_loc[1];
516
+ sxy = 0.0f; /* Initial correlation coefficient */
517
+
518
+ /* Clamp parameters to reasonable bounds */
519
+ i0 = fminf(fmaxf(i0, -2.0f), 2.0f);
520
+ j0 = fminf(fmaxf(j0, -2.0f), 2.0f);
521
+ sx = fminf(fmaxf(sx * sx, 0.25f), 9.0f); /* Convert to variance */
522
+ sy = fminf(fmaxf(sy * sy, 0.25f), 9.0f);
523
+
524
+ /* Compute initial residual and Jacobian */
525
+ residual = compute_residual_jacobian_6dof(xcorr, N, A, i0, j0, sx, sy, sxy, JtJ, Jtr, 1);
526
+
527
+ /* Levenberg-Marquardt optimization loop */
528
+ for(iter = 0; iter < max_iter; ++iter) {
529
+ /* Solve for parameter update */
530
+ if(solve_lm_step(JtJ, Jtr, lambda, delta, 6) != 0) break;
531
+
532
+ /* Apply parameter update with bounds checking */
533
+ float A_new = A + delta[0];
534
+ float i0_new = i0 + delta[1];
535
+ float j0_new = j0 + delta[2];
536
+ float sx_new = sx + delta[3];
537
+ float sy_new = sy + delta[4];
538
+ float sxy_new = sxy + delta[5];
539
+
540
+ A_new = fmaxf(A_new, A * 0.5f);
541
+ i0_new = fminf(fmaxf(i0_new, -2.5f), 2.5f);
542
+ j0_new = fminf(fmaxf(j0_new, -2.5f), 2.5f);
543
+ sx_new = fminf(fmaxf(sx_new, 0.1f), 16.0f);
544
+ sy_new = fminf(fmaxf(sy_new, 0.1f), 16.0f);
545
+ float sxy_max = 0.95f / sqrtf(sx_new * sy_new); /* Prevent singular covariance */
546
+ sxy_new = fminf(fmaxf(sxy_new, -sxy_max), sxy_max);
547
+
548
+ /* Evaluate new residual */
549
+ new_residual = compute_residual_jacobian_6dof(xcorr, N, A_new, i0_new, j0_new, sx_new, sy_new, sxy_new, NULL, NULL, 0);
550
+
551
+ if(new_residual < residual) {
552
+ /* Accept step: update parameters */
553
+ A = A_new; i0 = i0_new; j0 = j0_new;
554
+ sx = sx_new; sy = sy_new; sxy = sxy_new;
555
+ float improvement = (residual - new_residual) / (residual + FLT_EPSILON);
556
+ residual = new_residual;
557
+ lambda *= 0.5f; /* Decrease damping */
558
+ compute_residual_jacobian_6dof(xcorr, N, A, i0, j0, sx, sy, sxy, JtJ, Jtr, 1);
559
+ if(improvement < tol) break; /* Converged */
560
+ } else {
561
+ lambda *= 2.0f; /* Increase damping */
562
+ if(lambda > 1e6f) break; /* Diverged */
563
+ }
564
+ }
565
+
566
+ /* Store final results */
567
+ peak_loc[0] = i0;
568
+ peak_loc[1] = j0;
569
+ /* WARNING: Output parameters are swapped! sig[0]=sy, sig[1]=sx
570
+ * This is confusing and should be refactored. The 6-DOF model uses
571
+ * inverse covariance parameterization which differs from standard
572
+ * Gaussian parameterization used in 4-DOF and 5-DOF models. */
573
+ sig[0] = sy;
574
+ sig[1] = sx;
575
+ sig[2] = -(sxy * (sx * sy - sxy * sxy)); /* Covariance determinant */
576
+
577
+ /* Generate fitted values if requested */
578
+ if(fitval) {
579
+ for(ii = 0; ii < N[0]; ++ii) {
580
+ float i = (float)(ii - (N[0]-1)/2);
581
+ for(jj = 0; jj < N[1]; ++jj) {
582
+ float j = (float)(jj - (N[1]-1)/2);
583
+ idx = ii * N[1] + jj;
584
+ fitval[idx] = eval_gauss6(i, j, A, i0, j0, sx, sy, sxy);
585
+ }
586
+ }
587
+ }
588
+ }
589
+
590
+ /******************************************************************************
591
+ * Main peak localization function
592
+ *
593
+ * Performs multi-peak detection and fitting on correlation data.
594
+ *
595
+ * Parameters:
596
+ * xcorr: Input correlation array (N[0] x N[1])
597
+ * N: Dimensions of correlation array [rows, cols]
598
+ * peak_loc: Output peak locations (3 x nPeaks array)
599
+ * peak_loc[0,i] = x-coordinate, peak_loc[1,i] = y-coordinate, peak_loc[2,i] = peak height
600
+ * nPeaks: Maximum number of peaks to find
601
+ * iFitType: Fitting model (3=3pt, 4=circular, 5=elliptical, 6=rotated elliptical)
602
+ * std_dev: Output standard deviations (3 x nPeaks array)
603
+ * std_dev[0,i] = sigma_x, std_dev[1,i] = sigma_y, std_dev[2,i] = correlation/covariance
604
+ *****************************************************************************/
605
+ void lsqpeaklocate_lm(const float *xcorr, const int *N, float *peak_loc, int nPeaks, int iFitType, float *std_dev)
606
+ {
607
+ int i, j, iPeak, idx;
608
+ int i0, j0;
609
+ float *xcorr_copy;
610
+ float fPeakHeight;
611
+ float subxcorr[PKSIZE_X * PKSIZE_Y];
612
+ float fitval[PKSIZE_X * PKSIZE_Y];
613
+ int Nsub[2];
614
+ float peak[2];
615
+ float sig[3];
616
+
617
+ /* Create working copy of correlation data for peak subtraction */
618
+ xcorr_copy = (float*)malloc(sizeof(float) * N[0] * N[1]);
619
+ memcpy(xcorr_copy, xcorr, N[0] * N[1] * sizeof(float));
620
+ Nsub[0] = PKSIZE_X;
621
+ Nsub[1] = PKSIZE_Y;
622
+
623
+ /* Process each peak sequentially */
624
+ for(iPeak = 0; iPeak < nPeaks; ++iPeak)
625
+ {
626
+ /* Find highest remaining peak in central region
627
+ * N[0] = number of rows, N[1] = number of columns
628
+ * i indexes rows, j indexes columns
629
+ * Row-major indexing: SUB2IND_2D(row, col, num_cols)
630
+ */
631
+ i0 = j0 = 0;
632
+ fPeakHeight = 0;
633
+ for(i = N[0]/8; i < N[0]*7/8; ++i) {
634
+ for(j = N[1]/8; j < N[1]*7/8; ++j) {
635
+ if(xcorr_copy[SUB2IND_2D(i, j, N[1])] > fPeakHeight) {
636
+ fPeakHeight = xcorr_copy[SUB2IND_2D(i, j, N[1])];
637
+ i0 = i;
638
+ j0 = j;
639
+ }
640
+ }
641
+ }
642
+
643
+ /* Validate peak: check height and local maximum condition
644
+ * Row-major indexing: SUB2IND_2D(row, col, num_cols)
645
+ */
646
+ if(fPeakHeight <= 0 ||
647
+ i0 < (PKSIZE_X-1)/2 || i0 >= N[0]-(PKSIZE_X-1)/2 ||
648
+ j0 < (PKSIZE_Y-1)/2 || j0 >= N[1]-(PKSIZE_Y-1)/2 ||
649
+ fPeakHeight <= xcorr_copy[SUB2IND_2D(i0-1, j0, N[1])] ||
650
+ fPeakHeight <= xcorr_copy[SUB2IND_2D(i0+1, j0, N[1])] ||
651
+ fPeakHeight <= xcorr_copy[SUB2IND_2D(i0, j0-1, N[1])] ||
652
+ fPeakHeight <= xcorr_copy[SUB2IND_2D(i0, j0+1, N[1])])
653
+ {
654
+ /* Invalid peak: mark as NaN
655
+ * Row-major: array[3, nPeaks] -> stride is nPeaks
656
+ */
657
+ peak_loc[SUB2IND_2D(0, iPeak, nPeaks)] = NAN;
658
+ peak_loc[SUB2IND_2D(1, iPeak, nPeaks)] = NAN;
659
+ peak_loc[SUB2IND_2D(2, iPeak, nPeaks)] = 0;
660
+ std_dev[SUB2IND_2D(0, iPeak, nPeaks)] = 0;
661
+ std_dev[SUB2IND_2D(1, iPeak, nPeaks)] = 0;
662
+ std_dev[SUB2IND_2D(2, iPeak, nPeaks)] = 0;
663
+ continue;
664
+ }
665
+
666
+ /* Extract sub-window around peak for fitting
667
+ * Row-major indexing for both subxcorr and xcorr_copy
668
+ */
669
+ for(i = 0; i < PKSIZE_X; ++i) {
670
+ for(j = 0; j < PKSIZE_Y; ++j) {
671
+ subxcorr[i * PKSIZE_Y + j] = xcorr_copy[SUB2IND_2D(i0 + i - (PKSIZE_X-1)/2, j0 + j - (PKSIZE_Y-1)/2, N[1])];
672
+ }
673
+ }
674
+
675
+ /* Perform fit based on type - only use higher order fits for first peak */
676
+ if(iPeak == 0) {
677
+ switch(iFitType) {
678
+ case 6:
679
+ lm_gauss6_fit(subxcorr, Nsub, peak, fitval, sig);
680
+ break;
681
+ case 5:
682
+ lm_gauss5_fit(subxcorr, Nsub, peak, fitval, sig);
683
+ break;
684
+ case 4:
685
+ lm_gauss4_fit(subxcorr, Nsub, peak, fitval, sig);
686
+ break;
687
+ case 3:
688
+ default:
689
+ /* 3-point estimator fallback */
690
+ {
691
+ float A, sx, sy;
692
+ threept_estimate(subxcorr, Nsub, peak, &A, &sx, &sy);
693
+ sig[0] = sx;
694
+ sig[1] = sy;
695
+ sig[2] = 0.0f;
696
+ for(i = 0; i < PKSIZE_X; ++i) {
697
+ float fi = (float)(i - (PKSIZE_X-1)/2);
698
+ for(j = 0; j < PKSIZE_Y; ++j) {
699
+ float fj = (float)(j - (PKSIZE_Y-1)/2);
700
+ fitval[i * PKSIZE_Y + j] = eval_gauss5(fi, fj, A, peak[0], peak[1], sx, sy);
701
+ }
702
+ }
703
+ }
704
+ break;
705
+ }
706
+ } else {
707
+ /* Subsequent peaks: use fast 3-point for speed */
708
+ float A, sx, sy;
709
+ threept_estimate(subxcorr, Nsub, peak, &A, &sx, &sy);
710
+ sig[0] = sx;
711
+ sig[1] = sy;
712
+ sig[2] = 0.0f;
713
+ for(i = 0; i < PKSIZE_X; ++i) {
714
+ float fi = (float)(i - (PKSIZE_X-1)/2);
715
+ for(j = 0; j < PKSIZE_Y; ++j) {
716
+ float fj = (float)(j - (PKSIZE_Y-1)/2);
717
+ fitval[i * PKSIZE_Y + j] = eval_gauss5(fi, fj, A, peak[0], peak[1], sx, sy);
718
+ }
719
+ }
720
+ }
721
+
722
+ /* Save results: convert sub-window coordinates back to full image
723
+ * peak[0] = row offset, peak[1] = col offset (both relative to sub-window center)
724
+ * i0 = row index of peak in full correlation plane
725
+ * j0 = col index of peak in full correlation plane
726
+ * Output: peak_loc is [3, nPeaks] array in row-major
727
+ * peak_loc[0, iPeak] = row (Y) position
728
+ * peak_loc[1, iPeak] = col (X) position
729
+ * peak_loc[2, iPeak] = peak height
730
+ */
731
+ peak_loc[SUB2IND_2D(0, iPeak, nPeaks)] = peak[0] + i0; // Row (Y)
732
+ peak_loc[SUB2IND_2D(1, iPeak, nPeaks)] = peak[1] + j0; // Col (X)
733
+ peak_loc[SUB2IND_2D(2, iPeak, nPeaks)] = fPeakHeight;
734
+ std_dev[SUB2IND_2D(0, iPeak, nPeaks)] = sig[0];
735
+ std_dev[SUB2IND_2D(1, iPeak, nPeaks)] = sig[1];
736
+ std_dev[SUB2IND_2D(2, iPeak, nPeaks)] = sig[2];
737
+
738
+ /* Subtract fitted peak from correlation plane to find remaining peaks
739
+ * Row-major indexing
740
+ */
741
+ for(i = 0; i < PKSIZE_X; ++i) {
742
+ for(j = 0; j < PKSIZE_Y; ++j) {
743
+ idx = SUB2IND_2D(i0 + i - (PKSIZE_X-1)/2, j0 + j - (PKSIZE_Y-1)/2, N[1]);
744
+ xcorr_copy[idx] = MAX(0, xcorr_copy[idx] - fitval[i * PKSIZE_Y + j]);
745
+ }
746
+ }
747
+ }
748
+
749
+ free(xcorr_copy);
750
+ }
751
+