Use-case in a nutshell: Interpolate fixed-sized data (a lookup table) that provides output values for combinations of a (fixed) number of inputs. The appropriate scaling is done externally, each axis starts at 0 and increments by 1. The output is a floating point value regardless of the table data type, the idea being that the user of this class can determine if rounding/casting is needed.
This function attempts to generalise (bi|tri|etc)linear interpolation in a way that's efficient on a microcontroller (with hardware FPU).
A working example that can be copy-pasted:
#include <cmath>
#include <array>
#include <cstddef>
#include <cstdint>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace interpolate {
/**
* @brief Linear interpolation for N-dimensional data. Saturates / clamps search values to dataset range.
*
* This function is mainly meant to interpolate singleton values -such as lookup tables-, not entire images.
*
* @tparam DIMS Size of each dimension in supplied data array, excluding the last. For example 7 for a table with 7 columns and 3 rows.
* @tparam Td Type of data
* @tparam N Count of data
* @tparam Ts Type of search values / indices
* @param search... Search indices, the amount equal to the amount of dimensions.
* @return Td Interpolated value
*/
template <std::size_t... DIMS,
typename Td = decltype(0.0),
std::size_t N,
typename... Ts>
constexpr auto linear(const Td(&data)[N], const Ts& ...search) {
using index_t = std::size_t;
using fp_t = decltype(0.0); // Use the system default literal floating point type
using result_t = std::conditional_t<std::is_floating_point_v<Td> && (sizeof(Td) >= sizeof(fp_t)), Td, fp_t>; // Ensure we're always working in precise and reasonably performant floating point
constexpr index_t D {sizeof...(Ts)};
using search_set = std::array<std::common_type_t<Ts...>, D>;
if constexpr (sizeof...(Ts) > 1) {
constexpr index_t LAST_S { N / (DIMS * ...) };
constexpr std::size_t SIZES[D] { DIMS..., LAST_S };
constexpr std::size_t MAX[D] { (DIMS - 1u)..., LAST_S - 1 };
constexpr std::size_t L = 0;
constexpr std::size_t H = 1;
static_assert(sizeof...(DIMS) + 1 == sizeof...(Ts), "Amount of dimension sizes doesn't match search parameter count");
static_assert((DIMS * ...) != 0 && LAST_S != 0, "Can't interpolate singularities");
static_assert(N == (LAST_S * (DIMS * ...)), "Data size doesn't match provided dimension sizes");
index_t i[D][2] {{0}}; // High & low indices
fp_t w[D][2] {{0}}; // Associated weights
result_t result {0};
for (index_t j = 0; j < D; ++j) {
const auto searchval = search_set{{search...}}[j];
// Perform input clamping
if (searchval <= 0) {
// Keeping the indices one apart should do better in vectorisation
i[j][L] = 0;
i[j][H] = 1;
w[j][L] = 1;
w[j][H] = 0;
} else if (searchval >= MAX[j]) {
i[j][L] = MAX[j]-1;
i[j][H] = MAX[j];
w[j][L] = 0;
w[j][H] = 1;
} else {
i[j][L] = static_cast<index_t>(searchval);
if constexpr (false) {
// Sinus curve based fitting. TODO: Find an elegant way to toggle this
i[j][H] = i[j][L] + ((search_set{{search...}}[j] - i[j][L]) > 0);
w[j][H] = std::sin((i[j][L] + 0.5 - searchval) * M_PI) / -2.0 + 0.5;
} else {
// Linear
w[j][H] = searchval - static_cast<index_t>(searchval);
i[j][H] = i[j][L] + 1;
}
w[j][L] = 1 - w[j][H];
}
}
// Accumulate 2^D weighed results
for (index_t j = 0; j < (0b1 << D); ++j) {
fp_t weight { w[0][j & 0b1] };
index_t offset { 1 };
index_t idx { i[0][j & 0b1] };
for (index_t k = 1; k < D; ++k) {
const bool n = (j >> k) & 0b1;
weight *= w[k][n];
offset *= SIZES[k-1];
idx += i[k][n] * offset;
}
result += weight * data[idx];
}
return result;
} else {
const auto searchval = search_set{{search...}}[0];
if (searchval <= 0) {
return static_cast<result_t>(data[0]);
} else if (searchval >= N) {
return static_cast<result_t>(data[N-1]);
} else {
const auto i = static_cast<index_t>(searchval);
const auto w = searchval - i;
if (w == 0) {
return static_cast<result_t>(data[i]);
} else {
return static_cast<result_t>(data[i] * (1 - w) + data[i + 1] * w);
}
}
}
}
}
// volatile
constexpr
float
// unsigned
data[] {
// 0 1 2 3
/* 0 */ 1000, 2000, 3000, 4000,
/* 1 */ 5000, 6000, 7000, 8000,
/* 2 */ 9000, 10000, 11000, 12000,
/* 3 */ 1000, 2000, 3000, 4000,
/* 4 */ 5000, 6000, 7000, 8000,
/* 5 */ 9000, 10000, 11000, 12000,
/* 6 */ 1000, 2000, 3000, 4000,
/* 7 */ 5000, 6000, 7000, 8000,
/* 8 */ 9000, 10000, 11000, 12000
};
int main() {
volatile unsigned interpolated1 __attribute__((unused)) = interpolate::linear(data, 2.5);
volatile unsigned interpolated2 __attribute__((unused)) = interpolate::linear<4>(data, 2.5, 1.5);
volatile unsigned interpolated3 __attribute__((unused)) = interpolate::linear<4,3>(data, 2.25, 1.25, 0.5);
}
I'd love to hear comments & critiques!