MayaFlux 0.1.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ConvolutionHelper.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <unsupported/Eigen/FFT>
6
7namespace MayaFlux::Yantra {
8
9/**
10 * @brief Common FFT convolution helper to eliminate code duplication
11 * @param data_span Input data
12 * @param kernel Convolution kernel
13 * @param operation Function to apply in frequency domain
14 * @return Convolved result
15 */
16template <typename OperationFunc>
17std::vector<double> fft_convolve_helper(
18 std::span<const double> data_span,
19 std::span<const double> kernel,
20 OperationFunc&& operation,
21 bool return_full_size = true)
22{
23 if (data_span.empty() || kernel.empty()) {
24 if (return_full_size && !data_span.empty() && !kernel.empty()) {
25 return std::vector<double>(data_span.size() + kernel.size() - 1, 0.0);
26 } else {
27 return std::vector<double>(data_span.size(), 0.0);
28 }
29 }
30
31 size_t conv_size = data_span.size() + kernel.size() - 1;
32
33 size_t fft_size = std::max(256UZ, conv_size);
34 fft_size = 1 << (32 - std::countl_zero(fft_size - 1));
35
36 Eigen::VectorXd padded_input = Eigen::VectorXd::Zero(fft_size);
37 Eigen::VectorXd padded_kernel = Eigen::VectorXd::Zero(fft_size);
38
39 std::ranges::copy(data_span, padded_input.begin());
40 std::ranges::copy(kernel, padded_kernel.begin());
41
42 Eigen::FFT<double> fft;
43 Eigen::VectorXcd input_fft, kernel_fft;
44 fft.fwd(input_fft, padded_input);
45 fft.fwd(kernel_fft, padded_kernel);
46
47 Eigen::VectorXcd result_fft(fft_size);
48 operation(input_fft, kernel_fft, result_fft);
49
50 double scale_factor = 1.0 / fft_size;
51 std::ranges::transform(result_fft, result_fft.begin(),
52 [scale_factor](std::complex<double>& c) {
53 return c * scale_factor;
54 });
55
56 Eigen::VectorXd time_result;
57 fft.inv(time_result, result_fft);
58
59 if (return_full_size) {
60 std::vector<double> result(conv_size);
61 std::ranges::copy(time_result | std::views::take(conv_size), result.begin());
62 return result;
63 } else {
64 std::vector<double> result(data_span.size());
65 std::ranges::copy(time_result | std::views::take(data_span.size()), result.begin());
66 return result;
67 }
68}
69
70/**
71 * @brief Direct convolution using existing FIR filter infrastructure (IN-PLACE)
72 * This leverages the existing, well-tested filter implementation
73 * @tparam DataType OperationReadyData type
74 * @param input Input data - WILL BE MODIFIED
75 * @param impulse_response Impulse response for convolution
76 * @return Convolved data
77 */
78template <OperationReadyData DataType>
79DataType transform_convolve_with_fir(DataType& input, const std::vector<double>& impulse_response)
80{
81 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
82
83 for (auto& span : target_data) {
84 auto fir_filter = std::make_shared<Nodes::Filters::FIR>(nullptr, impulse_response);
85 std::vector<double> output;
86 output.reserve(span.size());
87 for (double sample : span) {
88 output.push_back(fir_filter->process_sample(sample));
89 }
90 std::copy(output.begin(), output.end(), span.begin());
91 }
92
93 auto reconstructed_data = target_data
94 | std::views::transform([](const auto& span) {
95 return std::vector<double>(span.begin(), span.end());
96 })
97 | std::ranges::to<std::vector>();
98
99 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
100}
101
102/**
103 * @brief Direct convolution using existing FIR filter infrastructure (OUT-OF-PLACE)
104 * This leverages the existing, well-tested filter implementation
105 * @tparam DataType OperationReadyData type
106 * @param input Input data - will NOT be modified
107 * @param impulse_response Impulse response for convolution
108 * @param working_buffer Buffer for operations (will be resized if needed)
109 * @return Convolved data
110 */
111template <OperationReadyData DataType>
112DataType transform_convolve_with_fir(DataType& input, const std::vector<double>& impulse_response, std::vector<std::vector<double>>& working_buffer)
113{
114 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
115
116 working_buffer.clear();
117 working_buffer.resize(target_data.size());
118
119 for (size_t i = 0; i < target_data.size(); ++i) {
120 auto fir_filter = std::make_shared<Nodes::Filters::FIR>(nullptr, impulse_response);
121 working_buffer[i].reserve(target_data[i].size());
122 for (double sample : target_data[i]) {
123 working_buffer[i].push_back(fir_filter->process_sample(sample));
124 }
125 }
126
127 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
128}
129
130/**
131 * @brief Convolution transformation using existing infrastructure with C++20 ranges (IN-PLACE)
132 * @tparam DataType OperationReadyData type
133 * @param input Input data - WILL BE MODIFIED
134 * @param impulse_response Impulse response for convolution
135 * @return Convolved data
136 */
137template <OperationReadyData DataType>
138DataType transform_convolve(DataType& input, const std::vector<double>& impulse_response)
139{
140 if (impulse_response.size() == 1) {
141 if (std::abs(impulse_response[0] - 1.0) < 1e-15) {
142 return input;
143 } else {
144 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
145
146 std::vector<std::vector<double>> reconstructed_data(target_data.size());
147
148 for (size_t i = 0; i < target_data.size(); ++i) {
149 std::ranges::transform(target_data[i], reconstructed_data[i].begin(),
150 [gain = impulse_response[0]](double x) { return x * gain; });
151 }
152 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
153 }
154 }
155
156 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
157
158 auto convolution_op = [](const Eigen::VectorXcd& input_fft,
159 const Eigen::VectorXcd& kernel_fft,
160 Eigen::VectorXcd& result_fft) {
161 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
162 [](const std::complex<double>& a, const std::complex<double>& b) {
163 return a * b;
164 });
165 };
166
167 for (auto& span : target_data) {
168 auto result = fft_convolve_helper(span, std::span<const double>(impulse_response), convolution_op, false);
169 std::copy(result.begin(), result.end(), span.begin());
170 }
171
172 auto reconstructed_data = target_data
173 | std::views::transform([](const auto& span) {
174 return std::vector<double>(span.begin(), span.end());
175 })
176 | std::ranges::to<std::vector>();
177
178 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
179}
180
181/**
182 * @brief Convolution transformation using existing infrastructure with C++20 ranges (OUT-OF-PLACE)
183 * @tparam DataType OperationReadyData type
184 * @param input Input data - will NOT be modified
185 * @param impulse_response Impulse response for convolution
186 * @param working_buffer Buffer for operations (will be resized if needed)
187 * @return Convolved data
188 */
189template <OperationReadyData DataType>
190DataType transform_convolve(DataType& input, const std::vector<double>& impulse_response, std::vector<std::vector<double>>& working_buffer)
191{
192 if (impulse_response.size() == 1) {
193 if (std::abs(impulse_response[0] - 1.0) < 1e-15) {
194 return input;
195 } else {
196 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
197 for (size_t i = 0; i < target_data.size(); ++i) {
198 std::ranges::transform(target_data[i], working_buffer[i].begin(),
199 [gain = impulse_response[0]](double x) { return x * gain; });
200 }
201 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
202 }
203 }
204
205 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
206
207 working_buffer.clear();
208 working_buffer.resize(target_data.size());
209
210 auto convolution_op = [](const Eigen::VectorXcd& input_fft,
211 const Eigen::VectorXcd& kernel_fft,
212 Eigen::VectorXcd& result_fft) {
213 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
214 [](const std::complex<double>& a, const std::complex<double>& b) {
215 return a * b;
216 });
217 };
218
219 for (size_t i = 0; i < target_data.size(); ++i) {
220 working_buffer[i] = fft_convolve_helper(target_data[i], std::span<const double>(impulse_response), convolution_op, false);
221 }
222
223 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
224}
225
226/**
227 * @brief Cross-correlation using FFT (convolution with time-reversed impulse) (IN-PLACE)
228 * @tparam DataType OperationReadyData type
229 * @param input Input data - WILL BE MODIFIED
230 * @param template_signal Template signal for correlation
231 * @param normalize Whether to normalize the result
232 * @return Cross-correlated data
233 */
234template <OperationReadyData DataType>
235DataType transform_cross_correlate(DataType& input, const std::vector<double>& template_signal, bool normalize = true)
236{
237 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
238
239 std::vector<double> reversed_template(template_signal.rbegin(), template_signal.rend());
240
241 auto correlation_op = [](const Eigen::VectorXcd& input_fft,
242 const Eigen::VectorXcd& kernel_fft,
243 Eigen::VectorXcd& result_fft) {
244 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
245 [](const std::complex<double>& a, const std::complex<double>& b) {
246 return a * std::conj(b);
247 });
248 };
249
250 for (auto& span : target_data) {
251 auto result = fft_convolve_helper(span, reversed_template, correlation_op);
252
253 if (normalize && !result.empty()) {
254 auto [min_it, max_it] = std::ranges::minmax_element(result);
255 double max_abs = std::max(std::abs(*min_it), std::abs(*max_it));
256 if (max_abs > 0.0) {
257 std::ranges::transform(result, result.begin(),
258 [max_abs](double val) { return val / max_abs; });
259 }
260 }
261
262 std::copy(result.begin(), result.begin() + std::min(result.size(), span.size()), span.begin());
263 }
264
265 auto reconstructed_data = target_data
266 | std::views::transform([](const auto& span) {
267 return std::vector<double>(span.begin(), span.end());
268 })
269 | std::ranges::to<std::vector>();
270
271 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
272}
273
274/**
275 * @brief Cross-correlation using FFT (convolution with time-reversed impulse) (OUT-OF-PLACE)
276 * @tparam DataType OperationReadyData type
277 * @param input Input data - will NOT be modified
278 * @param template_signal Template signal for correlation
279 * @param normalize Whether to normalize the result
280 * @param working_buffer Buffer for operations (will be resized if needed)
281 * @return Cross-correlated data
282 */
283template <OperationReadyData DataType>
284DataType transform_cross_correlate(DataType& input, const std::vector<double>& template_signal, bool normalize, std::vector<std::vector<double>>& working_buffer)
285{
286 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
287
288 std::vector<double> reversed_template(template_signal.rbegin(), template_signal.rend());
289
290 auto correlation_op = [](const Eigen::VectorXcd& input_fft,
291 const Eigen::VectorXcd& kernel_fft,
292 Eigen::VectorXcd& result_fft) {
293 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
294 [](const std::complex<double>& a, const std::complex<double>& b) {
295 return a * std::conj(b);
296 });
297 };
298
299 for (size_t i = 0; i < target_data.size(); ++i) {
300 auto result = fft_convolve_helper(target_data[i], reversed_template, correlation_op);
301
302 if (normalize && !result.empty()) {
303 auto [min_it, max_it] = std::ranges::minmax_element(result);
304 double max_abs = std::max(std::abs(*min_it), std::abs(*max_it));
305 if (max_abs > 0.0) {
306 std::ranges::transform(result, result.begin(),
307 [max_abs](double val) { return val / max_abs; });
308 }
309 }
310
311 working_buffer[i].resize(result.size());
312 working_buffer[i] = std::move(result);
313 }
314
315 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
316}
317
318/**
319 * @brief Auto-correlation using FFT (IN-PLACE)
320 * @tparam DataType OperationReadyData type
321 * @param input Input data - will be modified
322 * @param normalize Whether to normalize the result
323 * @return Auto-correlated data
324 */
325template <OperationReadyData DataType>
326DataType transform_auto_correlate_fft(DataType& input, bool normalize = true)
327{
328 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
329
330 // For auto-correlation using FFT:
331 // R(k) = IFFT(FFT(x) * conj(FFT(x)))
332
333 auto correlation_op = [](const Eigen::VectorXcd& input_fft,
334 const Eigen::VectorXcd& /* unused */,
335 Eigen::VectorXcd& result_fft) {
336 std::ranges::transform(input_fft, input_fft, result_fft.begin(),
337 [](const std::complex<double>& a, const std::complex<double>& b) {
338 return a * std::conj(b);
339 });
340 };
341
342 for (auto& span : target_data) {
343 std::vector<double> signal_copy(span.begin(), span.end());
344 auto result = fft_convolve_helper(span, signal_copy, correlation_op);
345
346 if (normalize && !result.empty()) {
347 double max_val = *std::max_element(result.begin(), result.end());
348 if (max_val > 0.0) {
349 std::ranges::transform(result, result.begin(),
350 [max_val](double val) { return val / max_val; });
351 }
352 }
353
354 std::copy(result.begin(), result.begin() + std::min(result.size(), span.size()), span.begin());
355 }
356
357 auto reconstructed_data = target_data
358 | std::views::transform([](const auto& span) {
359 return std::vector<double>(span.begin(), span.end());
360 })
361 | std::ranges::to<std::vector>();
362
363 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
364}
365
366/**
367 * @brief Auto-correlation using FFT (OUT-OF-PLACE)
368 * @tparam DataType OperationReadyData type
369 * @param input Input data - will NOT be modified
370 * @param working_buffer Buffer for operations (will be resized if needed)
371 * @param normalize Whether to normalize the result
372 * @return Auto-correlated data
373 */
374template <OperationReadyData DataType>
375DataType transform_auto_correlate_fft(DataType& input, std::vector<std::vector<double>>& working_buffer, bool normalize = true)
376{
377 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
378
379 auto correlation_op = [](const Eigen::VectorXcd& input_fft,
380 const Eigen::VectorXcd&,
381 Eigen::VectorXcd& result_fft) {
382 std::ranges::transform(input_fft, input_fft, result_fft.begin(),
383 [](const std::complex<double>& a, const std::complex<double>& b) {
384 return a * std::conj(b);
385 });
386 };
387
388 for (size_t i = 0; i < target_data.size(); ++i) {
389 std::vector<double> signal_copy(target_data[i].begin(), target_data[i].end());
390 auto result = fft_convolve_helper(target_data[i], signal_copy, correlation_op);
391
392 if (normalize && !result.empty()) {
393 double max_val = *std::max_element(result.begin(), result.end());
394 if (max_val > 0.0) {
395 std::ranges::transform(result, result.begin(),
396 [max_val](double val) { return val / max_val; });
397 }
398 }
399
400 working_buffer[i].resize(result.size());
401 working_buffer[i] = std::move(result);
402 }
403
404 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
405}
406
407/**
408 * @brief Matched filter using cross-correlation for signal detection (IN-PLACE)
409 * @tparam DataType OperationReadyData type
410 * @param input Input data - WILL BE MODIFIED
411 * @param reference_signal Reference signal for matching
412 * @return Matched filter output
413 */
414template <OperationReadyData DataType>
415DataType transform_matched_filter(DataType& input, const std::vector<double>& reference_signal)
416{
417 return transform_cross_correlate(input, reference_signal, true);
418}
419
420/**
421 * @brief Matched filter using cross-correlation for signal detection (OUT-OF-PLACE)
422 * @tparam DataType OperationReadyData type
423 * @param input Input data - will NOT be modified
424 * @param reference_signal Reference signal for matching
425 * @param working_buffer Buffer for operations (will be resized if needed)
426 * @return Matched filter output
427 */
428template <OperationReadyData DataType>
429DataType transform_matched_filter(DataType& input, const std::vector<double>& reference_signal, std::vector<std::vector<double>>& working_buffer)
430{
431 return transform_cross_correlate(input, reference_signal, true, working_buffer);
432}
433
434/**
435 * @brief Deconvolution using frequency domain division (IN-PLACE)
436 * Useful for removing known impulse responses
437 * @tparam DataType OperationReadyData type
438 * @param input Input data - WILL BE MODIFIED
439 * @param impulse_to_remove Impulse response to remove
440 * @param regularization Regularization factor for numerical stability
441 * @return Deconvolved data
442 */
443template <OperationReadyData DataType>
444DataType transform_deconvolve(DataType& input, const std::vector<double>& impulse_to_remove, double regularization = 1e-6)
445{
446 auto [target_data, structure_info] = OperationHelper::extract_structured_double(input);
447
448 auto deconvolution_op = [regularization](const Eigen::VectorXcd& input_fft,
449 const Eigen::VectorXcd& kernel_fft,
450 Eigen::VectorXcd& result_fft) {
451 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
452 [regularization](const std::complex<double>& signal, const std::complex<double>& kernel) {
453 double magnitude_sq = std::norm(kernel);
454 if (magnitude_sq < regularization) {
455 return std::complex<double>(0.0, 0.0);
456 }
457 return signal * std::conj(kernel) / (magnitude_sq + regularization);
458 });
459 };
460
461 for (auto& span : target_data) {
462 auto result = fft_convolve_helper(span, impulse_to_remove, deconvolution_op);
463 std::copy(result.begin(), result.begin() + std::min(result.size(), span.size()), span.begin());
464 }
465
466 auto reconstructed_data = target_data
467 | std::views::transform([](const auto& span) {
468 return std::vector<double>(span.begin(), span.end());
469 })
470 | std::ranges::to<std::vector>();
471
472 return OperationHelper::reconstruct_from_double<DataType>(reconstructed_data, structure_info);
473}
474
475/**
476 * @brief Deconvolution using frequency domain division (OUT-OF-PLACE)
477 * Useful for removing known impulse responses
478 * @tparam DataType OperationReadyData type
479 * @param input Input data - will NOT be modified
480 * @param impulse_to_remove Impulse response to remove
481 * @param regularization Regularization factor for numerical stability
482 * @param working_buffer Buffer for operations (will be resized if needed)
483 * @return Deconvolved data
484 */
485template <OperationReadyData DataType>
486DataType transform_deconvolve(DataType& input, const std::vector<double>& impulse_to_remove, double regularization, std::vector<std::vector<double>>& working_buffer)
487{
488 auto [target_data, structure_info] = OperationHelper::setup_operation_buffer(input, working_buffer);
489
490 auto deconvolution_op = [regularization](const Eigen::VectorXcd& input_fft,
491 const Eigen::VectorXcd& kernel_fft,
492 Eigen::VectorXcd& result_fft) {
493 std::ranges::transform(input_fft, kernel_fft, result_fft.begin(),
494 [regularization](const std::complex<double>& signal, const std::complex<double>& kernel) {
495 double magnitude_sq = std::norm(kernel);
496 if (magnitude_sq < regularization) {
497 return std::complex<double>(0.0, 0.0);
498 }
499 return signal * std::conj(kernel) / (magnitude_sq + regularization);
500 });
501 };
502
503 working_buffer.clear();
504 working_buffer.resize(target_data.size());
505
506 for (size_t i = 0; i < target_data.size(); ++i) {
507 working_buffer[i] = fft_convolve_helper(target_data[i], impulse_to_remove, deconvolution_op);
508 }
509
510 return OperationHelper::reconstruct_from_double<DataType>(working_buffer, structure_info);
511}
512
513}
static std::tuple< std::vector< std::span< double > >, DataStructureInfo > extract_structured_double(T &compute_data)
Extract structured double data from IO container or direct ComputeData with automatic container handl...
static auto setup_operation_buffer(T &input, std::vector< std::vector< double > > &working_buffer)
Setup operation buffer from IO or ComputeData type.
DataType transform_auto_correlate_fft(DataType &input, bool normalize=true)
Auto-correlation using FFT (IN-PLACE)
DataType transform_matched_filter(DataType &input, const std::vector< double > &reference_signal)
Matched filter using cross-correlation for signal detection (IN-PLACE)
DataType transform_deconvolve(DataType &input, const std::vector< double > &impulse_to_remove, double regularization=1e-6)
Deconvolution using frequency domain division (IN-PLACE) Useful for removing known impulse responses.
std::vector< double > fft_convolve_helper(std::span< const double > data_span, std::span< const double > kernel, OperationFunc &&operation, bool return_full_size=true)
Common FFT convolution helper to eliminate code duplication.
DataType transform_convolve_with_fir(DataType &input, const std::vector< double > &impulse_response)
Direct convolution using existing FIR filter infrastructure (IN-PLACE) This leverages the existing,...
DataType transform_cross_correlate(DataType &input, const std::vector< double > &template_signal, bool normalize=true)
Cross-correlation using FFT (convolution with time-reversed impulse) (IN-PLACE)
DataType transform_convolve(DataType &input, const std::vector< double > &impulse_response)
Convolution transformation using existing infrastructure with C++20 ranges (IN-PLACE)
void normalize(std::vector< double > &data, double target_peak)
Normalize single-channel data to specified peak level (in-place)
Definition Yantra.cpp:558