将数字范围映射到另一个范围

133

数学从来不是我在学校里的强项 :(

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;

如何将输入值转换为该范围的相应输出值?

例如,输入值为“0”将等于输出值“500”,输入值为“254”将等于输出值“5500”。我无法计算出输入值为50或101时的输出值。

我肯定这很简单,我现在想不出来 :)

编辑:我只需要整数,没有分数或其他东西。

6个回答

278

让我们抛开数学,用直觉来解决这个问题。

首先,如果我们想要将范围在 [0, x] 内的输入数字映射到输出范围内 [0, y],我们只需要按适当的比例进行缩放。 0 映射为 0,x 映射为 y,而数字 t 将会映射为 (y/x)*t

因此,我们将简化您的问题为上述更简单的问题。

输入范围 [input_start, input_end] 共有 input_end - input_start + 1 个数字。因此它相当于范围 [0, r],其中 r = input_end - input_start

同样地,输出范围相当于 [0, R],其中 R = output_end - output_start

输入值 input 相当于 x = input - input_start。 根据第一段话,它将转换为 y = (R/r)*x。然后,我们可以通过添加 output_start 来将 y 值转换回原始的输出范围: output = output_start + y

这给出了:

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

或者,另一种方式:

/* 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)

现在,由于 C 语言中的除法会截断小数部分,因此你应该尝试使用浮点数进行计算,以获得更准确的答案:

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

如果想要更加正确,最后一步应该使用四舍五入而不是截断。您可以编写一个简单的round函数来实现:

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

然后:

output = output_start + round(slope * (input - input_start))

21
这个函数我在几十年间写过至少几次,但这个解释比我自己能够提供的要好。因为使用了除法,每次运行对6.5百万次计算都很有用,点个赞。 - delrocco
3
这是我在这个网站上看到过的最好的答案之一。谢谢! - foobarbaz
多么美妙的解决问题的方式!回答得非常出色! - AdeleGoldberg
非常感谢您以如此清晰、直观和美妙的方式解释这个问题! - Manuel Martinez
8
这假设了线性映射。如果您想要正弦、抛物线、指数、对数、反抛物线、椭圆或圆形的话,我已经开发了一个包含这些公式的Desmos图表 - rgm
显示剩余5条评论

44
Arduino内置了map函数。

示例:

/* 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);
}

这个页面还有相关的实现代码:

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

12
FYI,他们的 map 函数有问题。该函数是通过将所有参数更改为整数来从浮点函数转换而来的,但其分布存在缺陷。 - Mud
@Mud Still,如果我们把所有东西都改回浮点数,那会很有用。 - Kotauskas

30

这个公式是:

 

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

我会在此引用这篇文章:https://betterexplained.com/articles/rethinking-arithmetic-a-visual-guide/,因为它对我理解此内容非常有帮助。一旦你理解了这篇文章的内容,就很容易自己推导出这些公式。请注意,我以前也曾苦恼于如此问题。(我不是宣传,只是觉得非常有用)

假设你有一个范围为[input_start..input_end],让我们先将其归一化,使0为input_start,1为input_end。这是简化问题的简单技巧。

我们该如何做?我们需要将所有东西左移input_start的距离,这样如果输入x恰好是input_start,它应该返回零。

所以,假设f(x)是执行转换的函数。

f(x) = x - input_start

让我们试一试:

f(input_start) = input_start - input_start = 0

适用于input_start

此时,它尚未适用于input_end,因为我们尚未进行比例缩放。

让我们按范围长度将其缩小,然后最大值(input_end)将映射到一。

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

好的,让我们尝试使用 input_end

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

太棒了,看起来可以用。

好的,下一步,我们将实际缩放到输出范围。只需乘以输出范围的实际长度就像这样:

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

现在,实际上,我们已经快完成了,我们只需要将它向右移动,以便0从输出起始位置开始。

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

让我们快速尝试一下。

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

你会发现等式的第一部分基本上乘以零,因此将所有内容都抵消掉,得到

f(input_start) = output_start

让我们也试试input_end

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

这将最终成为:

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

正如您所看到的,现在它似乎被正确地映射了。


真正能够通用工作的人 - Gilian

17

这里的关键点是在正确的位置进行整数除法(包括四舍五入)。 到目前为止,没有任何答案得到正确的括号。 这是正确的方法:

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

output = (input - input_start)*output_range / input_range + output_start;

9

这个方法保证将任何范围映射到任何范围

我编写了这个方法,严格按照代数公式来映射一个范围内的数字到另一个范围。计算过程中使用双精度浮点数以保持精度,最后会返回一个指定小数位数的双精度浮点数。

在定义范围的低端和高端时,并不需要将其命名为“low”或“high”,因为无论哪一端的值较低或较高,该方法都能正确地映射数字。

例如,如果你将任何范围定义为[-100到300]或[300到-100],并没有任何区别。重新映射的结果仍然准确。

以下是如何在你的代码中使用该方法:

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

以下是如何使用该方法的示例:

源范围:-400 到 800
目标范围:10000 到 3500
重新映射的数字:250

double sourceA = -400;
double sourceB = 800;
double destA = 10000;
double destB = 3500;
double myNum = 250;
        
double newNum = mapOneRangeToAnother(myNum,sourceA,sourceB,destA,destB,2);
    
Result: 6479.17

如果你需要一个整数返回值,只需将精度设置为0位小数,并将结果转换为int类型,像这样:
int myResult = (int) mapOneRangeToAnother(myNumber, 500, 200, -350, -125, 0);

或者你可以声明该方法返回一个整数,并移除decimalPrecision参数,然后将最后两行改为:
int calcScale = (int) Math.pow(10, 0);
return (int) Math.round(finalNumber * calcScale) / calcScale;

在 OP 的问题中,他们会像这样使用该函数:
int myResult = (int) mapOneRangeToAnother(input, input_start, input_end, output_start, output_end, 0);

这是方法:

编辑:正如 @CoryGross 指出的那样,如果将相同的数字传入到 from 范围的两个端点中,就会发生除零错误。而且由于该方法应该根据两个数字范围计算一个新的数字,如果任一范围的端点具有相同的值,计算结果将毫无意义,因此在这种情况下需要返回 null。

此示例是用 Java 编写的

public static Double mapOneRangeToAnother(double sourceNumber, double fromA, double fromB, double toA, double toB, int decimalPrecision ) {
    double deltaA = fromB - fromA;
    double deltaB = toB - toA;
    if(deltaA == 0 || deltaB == 0) {  //One set of end-points is not a range, therefore, cannot calculate a meaningful number.
        return null;
    }
    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;
}

在我的使用情况中,我需要淡化JavaFX Control的不透明度,但由于不透明度是从0到1的数字,所以我只需使用该方法重新映射范围1到100(基于一个for循环,它将int从0增加到100)到范围0到1,并且效果完美。
虽然现在我知道我可以通过将增量从1更改为像.01这样的值来创建我的循环,就像这样:
for(double x=0; x<=1; x+=.01 {
    //Code to change controls opacity
}

我只是为了提醒那些可能在做类似于我所做的事情的人。这种方法如描述的那样完美地运作。

:-)


很棒的函数。我添加了快速边界逻辑,以处理可能落在所需“from”范围之外的sourceNumbers,并且效果非常好。 - Jordan Grant
@JordanGrant - 加入这个检查是有意义的,特别是如果输入的数字在运行时是未知的(例如用户输入的数字)。很高兴你觉得这很有用。 :) - Michael Sims
注意,如果由于除以零而传递了值使得 fromA == fromB,则会抛出异常。 - Cory Gross
@CoryGross,你能给我展示一下按照你所描述的调用方法的例子吗? - Michael Sims
例如,我认为调用 mapOneRangeToAnother(1, 1, 1, 2, 2, 0) 会因为除以零而抛出错误。 - Cory Gross
显示剩余4条评论

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

它的作用是比例地找出输入范围内的输入值。然后将该比例应用于输出范围的大小,以绝对值确定输出范围内的输出位置。然后加上输出范围的起始值,得到实际的输出数字。


这将始终返回 500,除了输入的 254 将产生 5500 - Sven Marnach
1
应该是 output = ((out_end - out_start)/(input_end - input_start)) * input + output_start; - jackw11111
1
@jackw11111 不错。这是改进版 input * ((output_end - output_start) / (input_end - input_start)) + output_start - rofrol
@rofrol 对不起,实际上应该是 output = (output_end - output_start)/(input_end - input_start) *( input - input_start) + output_start;,使用公式 y - y1 = m(x-x1) - jackw11111
我最终使用了这里的相同算法 https://dev59.com/82025IYBdhLWcg3w6KOO#5732390,并修复了 ES6 版本中的精度问题 https://rosettacode.org/wiki/Map_range#JavaScript。 - rofrol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接