web-dev-qa-db-fra.com

Interpolation linéaire en virgule flottante

Pour effectuer une interpolation linéaire entre deux variables a et b à partir d'une fraction f, j'utilise actuellement ce code:

float lerp(float a, float b, float f) 
{
    return (a * (1.0 - f)) + (b * f);
}

Je pense qu'il y a probablement un moyen plus efficace de le faire. J'utilise un microcontrôleur sans FPU, donc les opérations en virgule flottante sont effectuées dans le logiciel. Ils sont raisonnablement rapides, mais il reste environ 100 cycles à additionner ou à multiplier.

Aucune suggestion?

n.b. Par souci de clarté dans l'équation du code ci-dessus, nous pouvons omettre de spécifier 1.0 en tant que littéral à virgule flottante explicite.

21
Thomas O

Sans tenir compte des différences de précision, cette expression est équivalente à

float lerp(float a, float b, float f)
{
    return a + f * (b - a);
}

C'est 2 additions/soustractions et 1 multiplication au lieu de 2 additions/soustractions et 2 multiplications.

21
aioobe

Si vous utilisez un micro-contrôleur sans FPU, la virgule flottante coûtera très cher. Pourrait être facilement vingt fois plus lent pour une opération en virgule flottante. La solution la plus rapide consiste simplement à faire tous les calculs en utilisant des nombres entiers. 

Le nombre de places après le point binaire fixe ( http://blog.credland.net/2013/09/binary-fixed-point-explanation.html?q=fixed+binary+point ) est: XY_TABLE_FRAC_BITS. 

Voici une fonction que j'utilise: 

inline uint16_t unsignedInterpolate(uint16_t a, uint16_t b, uint16_t position) {
    uint32_t r1;
    uint16_t r2;

    /* 
     * Only one multiply, and one divide/shift right.  Shame about having to
     * cast to long int and back again.
     */

    r1 = (uint32_t) position * (b-a);
    r2 = (r1 >> XY_TABLE_FRAC_BITS) + a;
    return r2;    
}

Avec la fonction en ligne, il devrait être env. 10-20 cycles. 

Si vous avez un micro-contrôleur 32 bits, vous pourrez utiliser des entiers plus grands et obtenir des nombres plus grands ou une plus grande précision sans compromettre les performances. Cette fonction était utilisée sur un système 16 bits. 

7
Jim Credland

En supposant que des calculs en virgule flottante soient disponibles, l'algorithme de l'OP est bon et est toujours supérieur à l'option a + f * (b - a) en raison de la perte de précision lorsque a et b diffèrent de manière significative en amplitude.

Par exemple:

// OP's algorithm
float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

// Algebraically simplified algorithm
float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

Dans cet exemple, en supposant que les flottants 32 bits lint1(1.0e20, 1.0, 1.0) renverront correctement 1.0, alors que lint2 renverra incorrectement 0.0.

La plus grande partie de la perte de précision provient des opérateurs d'addition et de soustraction lorsque les opérandes diffèrent de manière significative en amplitude. Dans le cas ci-dessus, les coupables sont la soustraction dans b - a et l'addition dans a + f * (b - a). L'algorithme de l'OP n'en souffre pas car les composants sont complètement multipliés avant addition.


Pour le a = 1e20, b = 1 cas, voici un exemple de résultats différents. Programme de test:

#include <stdio.h>
#include <math.h>

float lint1 (float a, float b, float f) {
    return (a * (1.0f - f)) + (b * f);
}

float lint2 (float a, float b, float f) {
    return a + f * (b - a);
}

int main () {
    const float a = 1.0e20;
    const float b = 1.0;
    int n;
    for (n = 0; n <= 1024; ++ n) {
        float f = (float)n / 1024.0f;
        float p1 = lint1(a, b, f);
        float p2 = lint2(a, b, f);
        if (p1 != p2) {
            printf("%i %.6f %f %f %.6e\n", n, f, p1, p2, p2 - p1);
        }
    }
    return 0;
}

Sortie, légèrement ajustée pour le formatage:

 f Lint1 lint2 lint2-Lint1 
 0,828125 17187500894208393216 17187499794696765440 -1.099512e + 12 
 0,890625 10937500768952909824 10937499669441282048 -1.099512e + 12 
 0,914062 8593750447104196608 8593749897348382720 -5.497558e + 11 .__ 0,945312 5468750384476454912 5468749834720641024 -5.497558e + 11. 
 0.957031 4296875223552098304 4296874948674191360 -2.748779e + 11 
 0,972656 2734375192238227456 2734374917360320512 -2.748779e + 11 
 0,978516 2148437611776049152 2148437474337095680 -1.374390e ​​+ 11 
 0,986328 1367187596119113728 1367187458680160256 -1.374390e ​​+ 11 
 0,989258 1074218805888024576 1074218737168547840 -6,871948e + 10 
 0,993164 683593798059556864 683593729340080128 -6,871948e + 10 
 1,000000 1 0 -1,000000e + 00.
5
Jason C

Si vous codez pour un microcontrôleur sans opérations à virgule flottante, il est préférable de ne pas utiliser de nombres à virgule flottante, mais d'utiliser l'arithmétique en virgule fixe .

3
Gareth Rees

Il a été écrit par Google auparavant, mais c'est simple et vous pouvez écrire le vôtre, mais pourquoi? Quand c'est là pour vous.

new FloatEvaluator().evaluate(fraction, startValue, endValue)

Cette fonction renvoie le résultat de l'interpolation linéaire des valeurs de début et de fin, la fraction représentant la proportion entre les valeurs de début et de fin.

0
steve moretz

Si vous voulez que le résultat final soit un entier, il serait peut-être plus rapide d'utiliser également des entiers pour l'entrée.

int lerp_int(int a, int b, float f)
{
    //float diff = (float)(b-a);
    //float frac = f*diff;
    //return a + (int)frac;
    return a + (int)(f * (float)(b-a));
}

Cela fait deux lancers et un float se multiplient. Si un transtypage est plus rapide qu'un ajout/soustraction flottant sur votre plate-forme et si une réponse entière vous est utile, cela pourrait être une alternative raisonnable.

0
mtrw