Abbildung eines numerischen Bereichs auf einen anderen

73

Mathe war nie meine Stärke in der Schule :(

int input_start = 0;    // The lowest number of the range input.
int input_end = 254;    // The largest number of the range input.
int output_start = 500; // The lowest number of the range output.
int output_end = 5500;  // The largest number of the range output.

int input = 127; // Input value.
int output = 0;

Wie kann ich den Eingabewert in den entsprechenden Ausgabewert dieses Bereichs konvertieren?

Beispielsweise würde ein Eingabewert von "0" einem Ausgabewert von "500" entsprechen, ein Eingabewert von "254" würde einem Ausgabewert von "5500" entsprechen. Ich kann nicht herausfinden, wie ein Ausgabewert berechnet wird, wenn ein Eingabewert beispielsweise 50 oder 101 ist.

Ich bin sicher, es ist einfach, ich kann jetzt nicht denken :)

Edit: Ich brauche nur ganze Zahlen, keine Brüche oder so.

Joe
quelle

Antworten:

153

Vergessen wir die Mathematik und versuchen wir, dies intuitiv zu lösen.

Wenn wir Eingabenummern im Bereich [ 0, x] dem Ausgabebereich [ 0, y] zuordnen möchten , müssen wir zunächst nur um einen geeigneten Betrag skalieren. 0 geht zu 0, xgeht zu yund eine Zahl tgeht zu (y/x)*t.

Reduzieren wir Ihr Problem auf das oben beschriebene einfachere Problem.

Ein Eingabebereich von [ input_start, input_end] hat input_end - input_start + 1Zahlen. Es entspricht also einem Bereich von [ 0, r], wo r = input_end - input_start.

In ähnlicher Weise entspricht der Ausgabebereich [ 0, R], wobei R = output_end - output_start.

Eine Eingabe von inputentspricht x = input - input_start. Dies wird ab dem ersten Absatz übersetzt in y = (R/r)*x. Anschließend können wir den yWert durch Hinzufügen von output_start: wieder in den ursprünglichen Ausgabebereich übersetzen output = output_start + y.

Dies gibt uns:

output = output_start + ((output_end - output_start) / (input_end - input_start)) * (input - input_start)

Oder anders:

/* Note, "slope" below is a constant for given numbers, so if you are calculating
   a lot of output values, it makes sense to calculate it once.  It also makes
   understanding the code easier */
slope = (output_end - output_start) / (input_end - input_start)
output = output_start + slope * (input - input_start)

Da dies C ist und die Division in C abgeschnitten ist, sollten Sie versuchen, eine genauere Antwort zu erhalten, indem Sie die Dinge im Gleitkomma berechnen:

double slope = 1.0 * (output_end - output_start) / (input_end - input_start)
output = output_start + slope * (input - input_start)

Wenn Sie noch korrekter sein möchten, würden Sie im letzten Schritt eine Rundung anstelle einer Kürzung durchführen. Sie können dies tun, indem Sie eine einfache roundFunktion schreiben :

#include <math.h>
double round(double d)
{
    return floor(d + 0.5);
}

Dann:

output = output_start + round(slope * (input - input_start))
Alok Singhal
quelle
5
Ich habe diese Funktion im Laufe der Jahrzehnte mindestens ein paar Mal geschrieben, und dennoch ist diese Erklärung besser, als ich mir selbst geben könnte. +1 zum Herausziehen der Kluft - war nützlich für 6,5 Millionen Berechnungen pro Lauf.
Delrocco
2
Dies ist eine der besten Antworten, die ich auf dieser Website gesehen habe. Vielen Dank!
Foobarbaz
Was für eine schöne Art, sich dem Problem zu nähern! Hervorragend beantwortet!
AdeleGoldberg
Ich habe festgestellt, dass die Reihenfolge der Operationen hier falsch ist. Es sollte sein output_start + ((output_end - output_start) * (input - input_start)) / (input_end - input_start) . Siehe diese Gleichung
stacksonstacks
@stacksonstacks sorry ich sehe den Unterschied nicht. Du meinst, ich sollte zuerst multiplizieren und dann teilen? (a * b) / cist das gleiche wie (a / c) * b- zumindest mathematisch. Um die Gleitkomma-Genauigkeit zu gewährleisten und sicherzustellen, dass Sie keine Überläufe haben, müssen Sie die Reihenfolge der Operationen sehr genau beachten und können abhängig von den Eingabewerten sogar eine andere Reihenfolge der Operationen haben.
Alok Singhal
23

Arduino hat diese Karte integriert .

Beispiel:

/* Map an analog value to 8 bits (0 to 255) */
void setup() {}

void loop()
{
  int val = analogRead(0);
  val = map(val, 0, 1023, 0, 255);
  analogWrite(9, val);
}

Es hat auch die Implementierung auf dieser Seite:

long map(long x, long in_min, long in_max, long out_min, long out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
Dustin
quelle
11
Zu Ihrer Information, ihre mapFunktion ist gebrochen. Es wurde von einer Gleitkommafunktion konvertiert, indem einfach alle Argumente in int geändert wurden, und es hat eine fehlerhafte Verteilung.
Schlamm
@Mud Trotzdem ist es nützlich, wenn wir alles wieder in Floats ändern.
Kotauskas
14

Die Formel lautet

f (x) = (x - Eingabestart) / (Eingabeende - Eingabestart) * (Ausgabeende - Ausgabestart) + Ausgabestart

Ich werde diesen Beitrag hier anschließen: https://betterexplained.com/articles/rethinking-arithmetic-a-visual-guide/, da es mir sehr geholfen hat, dies intuitiv zu finden. Sobald Sie verstanden haben, was der Beitrag sagt, ist es trivial, diese Formeln selbst zu erstellen. Beachten Sie, dass ich auch mit solchen Fragen zu kämpfen hatte. (Ich habe keine Zugehörigkeit - fand es nur sehr nützlich)

Angenommen, Sie haben eine Reichweite [input_start..input_end]. Beginnen wir damit, sie so zu normalisieren, dass 0 ist input_startund 1 ist input_end. Dies ist eine einfache Technik, um das Problem zu vereinfachen.

wie machen wir das? Wir müssten alles, was noch übrig ist, um den Betrag input_start verschieben, so dass, wenn Eingabe x zufällig ist input_start, sie Null ergeben sollte.

Nehmen wir also an, es f(x)ist die Funktion, die die Konvertierung durchführt.

f(x) = x - input_start

Lass es uns versuchen:

f(input_start) = input_start - input_start = 0

arbeitet für input_start.

Zu diesem Zeitpunkt funktioniert es noch input_endnicht, da wir es nicht skaliert haben.

Skalieren wir es einfach um die Länge des Bereichs, dann wird der größte Wert (input_end) einem zugeordnet.

f(x) = (x - input_start) / (input_end - input_start)

ok, lass es uns versuchen mit input_end.

f(input_end) = (input_end - input_start) / (input_end - input_start) = 1

genial, scheint zu funktionieren.

Okay, im nächsten Schritt skalieren wir es tatsächlich auf den Ausgabebereich. Es ist so trivial wie nur das Multiplizieren mit der tatsächlichen Länge des Ausgabebereichs als solches:

f(x) = (x - input_start) / (input_end - input_start) * (output_end - output_start)

Jetzt sind wir fast fertig. Wir müssen es nur noch nach rechts verschieben, damit 0 von output_start ausgeht.

f(x) = (x - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

Lassen Sie es uns kurz versuchen.

f(input_start) = (input_start - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

Sie sehen, dass der erste Teil der Gleichung so ziemlich mit Null multipliziert wird, wodurch alles aufgehoben wird und Sie erhalten

f(input_start) = output_start

Lass es uns auch versuchen input_end.

f(input_end) = (input_end - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

was wiederum enden wird als:

f(input_end) = output_end - output_start + output_start = output_end

Wie Sie sehen können, scheint es jetzt korrekt zugeordnet zu sein.

Erti-Chris Eelmaa
quelle
12

Der entscheidende Punkt hierbei ist, die Ganzzahldivision (einschließlich Rundung) an der richtigen Stelle durchzuführen. Keine der bisherigen Antworten stimmte mit den Klammern überein. Hier ist der richtige Weg:

int input_range = input_end - input_start;
int output_range = output_end - output_start;

output = (input - input_start)*output_range / input_range + output_start;
Sven Marnach
quelle
3
output = ((input - input_start)/(input_end - input_start)) * (output_end - output_start) + output_start

Dabei wird proportional "wie weit" der Eingangsbereich der Eingabe ermittelt. Dieser Anteil wird dann auf die Größe des Ausgabebereichs angewendet, um in absoluten Zahlen herauszufinden, wie weit der Ausgabebereich im Ausgabebereich liegen sollte. Anschließend wird der Anfang des Ausgabebereichs hinzugefügt, um die tatsächliche Ausgangsnummer zu erhalten.

QuantumMechanic
quelle
Dies wird immer geben 500, mit Ausnahme der Eingabe 254, die ergeben wird 5500.
Sven Marnach
2

Dies ist GARANTIERT, um JEDEN Bereich JEDEM Bereich zuzuordnen

Ich habe diese Methode geschrieben, die genau der algebraischen Formel für die Zuordnung eines Zahlenbereichs zu einem anderen folgt. Die Berechnungen werden mit Doubles durchgeführt, um die Genauigkeit zu gewährleisten. Am Ende wird ein Double mit den in den Methodenargumenten angegebenen Dezimalstellen zurückgegeben.

Die Funktion funktioniert folgendermaßen ... Beachten Sie, dass ich die Enden der Bereiche A und B beschriftet habe - das liegt daran, dass es nicht erforderlich ist, dass Ihre Bereiche niedrig beginnen und hoch enden oder so etwas ... Sie können Ihren ersten Bereich 300 erstellen bis -7000 und Ihr zweiter Bereich -3500 bis 5500 ... macht keinen Unterschied, welche Zahlen Sie für Ihre Bereiche ausgewählt haben, es werden sie korrekt zugeordnet.

mapOneRangeToAnother(myNumber, fromRangeA, fromRangeB, toRangeA, toRangeB, decimalPrecision)

Und wenn Sie eine Ganzzahl zurück benötigen, sagen Sie ihr einfach, dass sie das Ergebnis mit einer Dezimalstelle von 1 zurückgeben und das Ergebnis wie folgt in int umwandeln soll:

int myResult = (int) mapOneRangeToAnother(myNumber, 500, 200, -350, -125, 1);

In der ursprünglichen Frage des OP würden sie die folgende Funktion verwenden:

int myResult = (int) mapOneRangeToAnother(input, input_start, input_end, output_start, output_end, 1);

und hier ist die Funktion:

public static double mapOneRangeToAnother(double sourceNumber, double fromA, double fromB, double toA, double toB, int decimalPrecision ) {
    double deltaA = fromB - fromA;
    double deltaB = toB - toA;
    double scale  = deltaB / deltaA;
    double negA   = -1 * fromA;
    double offset = (negA * scale) + toA;
    double finalNumber = (sourceNumber * scale) + offset;
    int calcScale = (int) Math.pow(10, decimalPrecision);
    return (double) Math.round(finalNumber * calcScale) / calcScale;
}

Ich habe diese Funktion sogar in einem Zähler verwendet, um etwas auf dem Bildschirm auszublenden, aber da die Deckkraft ein Wert zwischen 0 und 1 ist, habe ich einen Zähler eingerichtet, der von 0 bis 100 zählt, und dann den Bereich 0 - 100 bis 0 zugeordnet - 1 und es werden alle Brüche zwischen 0 und 1 ermittelt, wenn die Zahlen 0 bis 100 eingegeben werden. Dies ist eine sauberere Methode, da der Hauptcode mit einer solchen Funktion besser aussieht als mit der tatsächlichen Berechnung on the fly.

Ihr Kilometerstand kann variieren ... viel Spaß! :-)

Michael Sims
quelle