Общий многомерный шаблон линейной интерполяции C ++

Вкратце о сценарии использования: интерполировать данные фиксированного размера (таблица поиска), которые предоставляют выходные значения для комбинаций (фиксированного) количества входов. Соответствующее масштабирование выполняется извне, каждая ось начинается с 0 и увеличивается на 1. Результатом является значение с плавающей запятой независимо от типа данных таблицы, идея заключается в том, что пользователь этого класса может определить, требуется ли округление / приведение.

Эта функция пытается обобщить (bi | tri | etc) линейная интерполяция таким образом, чтобы это было эффективно на микроконтроллере (с аппаратным FPU).

Рабочий пример, который можно скопировать:

#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);
}

Я хотел бы услышать комментарии и критику!

1 ответ
1

Во-первых, довольно странный тип ввода const Td(&data)[N] — обычно хотелось бы std::span или эквивалент, и я не думаю, что исправление значений всех измерений в качестве параметра шаблона полезно. Количество измерений действительно должно быть шаблоном, иначе реализация будет несколько более проблематичной из-за требований использования динамической памяти.

Второй, const Ts& ...search вы должны обернуть это как std::array<floating_type, dims> или что-то в этом роде вместо последовательности произвольных параметров.

Возможно, это странность программирования для микроконтроллера, но в целом я бы посоветовал обернуть все это в класс и сохранить важные параметры, такие как SIZES[D], MAX[D], и, что более важно, размеры шагов как члены класса. И, надеюсь, весь класс constexpr. Я не уверен, какую версию C ++ вы используете, и, следовательно, не совсем уверен, что это возможно.

Наконец, алгоритм интерполяции неэффективен. То, что вы делаете, это для каждой вершины n размерный куб вычисляет произведение весов и суммирует все вместе. Что неэффективно, поскольку вы делаете O(n) продукты для каждой вершины с кучей довольно произвольного доступа к памяти.

Более быстрый подход — интерполировать по одному измерению и получить n-1 мерный куб ценностей — и повторяем. Таким образом вы сделаете O(1) продукты для каждой вершины. Полагаю, без примера я не понимаю, что я имею в виду.

Позвольте дать вам одно: у вас есть 2x2x2 блокировать {{{1,2}{3,4}},{{5,6}{7,8}}} и вы хотите вычислить интерполяцию в точке (0.3, 0.4, 0.8). Итак, сначала вы интерполируете по первому измерению и получите блок 2×2:

{{1.3, 3.3}, {5.3, 7.3}} // here 1*(1-0.3) + 2*0.3 = 1.3, 3*(1-0.3) + 4*0.3 = 3.3 etc...

Затем вы интерполируете по второму измерению (значение интерполяции = 0,4), чтобы получить блок размером 2 {2.1, 6.1} и, наконец, по последнему измерению (интерп. значение = 0,8) и получаем 2.1*(1-0.8)+6.1*0.8 = 5.3.

Здесь, как вы можете видеть, это довольно простой двойной цикл, который просто применяется a[k] = a[2k]*(1-p)+a[2k+1]*p для большинства операций.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *