【问题标题】:Mapping a numeric range onto another将数字范围映射到另一个范围
【发布时间】:2022-03-25 22:13:12
【问题描述】:

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

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,我不知道如何计算输出值。

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

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

【问题讨论】:

    标签: c math arduino


    【解决方案1】:

    让我们忘记数学并尝试直观地解决这个问题。

    首先,如果我们想将 [0, x] 范围内的输入数字映射到输出范围 [0, y] 中,我们只需按适当的比例缩放即可。 0 到 0,xy,数字 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_startoutput = output_start + y,将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,而 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))
    

    【讨论】:

    • 几十年来我至少写过几次这个函数,但这个解释比我给自己的要好。 +1 用于消除鸿沟 - 对于每次运行 650 万次计算很有用。
    • 这是我在这个网站上看到的最好的答案之一。谢谢!
    • 解决问题的好方法!很好的回答!
    • 我发现这里的操作顺序不正确。应该是output_start + ((output_end - output_start) * (input - input_start)) / (input_end - input_start) 。见this equation
    • 这假定一个线性映射。如果您想要正弦曲线、抛物线曲线、指数曲线、对数曲线、反抛物线曲线、椭圆曲线或圆形曲线,我开发了 Desmos graph with those formulas
    【解决方案2】:

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

    【讨论】:

    • 仅供参考,他们的map 功能已损坏。它是通过简单地将所有参数更改为 int 来从浮点函数转换而来的,并且它的分布不完整。
    • @Mud 不过,如果我们将所有内容都改回浮点数还是很有用的。
    【解决方案3】:

    公式是

    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 从 output_start 开始。

    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
    

    如您所见,它现在似乎已正确映射。

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        保证将任何范围映射到任何范围

        我编写了这个方法,它完全遵循将一个数字范围映射到另一个数字范围的代数公式。计算是使用双精度来完成的,以保持精度,然后在最后,它将返回一个 Double,无论您在方法参数中指定多少小数位。

        这个函数的工作原理是这样的......注意我标记了范围 A 和 B 的末端 - 那是因为没有必要让你的范围开始低和结束高或类似的东西......你可以让你的第一个范围是 300 到 -7000,第二个范围是 -3500 到 5500 ......您为范围选择的数字没有区别,它会正确映射它们。

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

        或者您可以将函数声明为返回一个 int,然后只需将最后一行从 return (double) 更改为 return (int)。

        在 OPs 问题中,他们会使用如下函数:

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

        这里是函数:

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

        我什至在计数器中使用了这个函数来淡出屏幕上的某些内容,但是由于不透明度是介于 0 和 1 之间的值,所以我设置了一个计数器从 0 到 100 计数,然后我映射范围 0 - 100 到 0 - 1 并且它基于将数字 0 到 100 提供给它来获取 0 到 1 之间的所有分数......这是一种更简洁的方法,因为使用这样的函数的主代码看起来比实际上更好即时计算。

        您的里程可能会有所不同...尽情享受吧! :-)

        【讨论】:

        • 很棒的功能。为此,我为可能超出所需“来自”范围的 sourceNumbers 添加了快速边界逻辑,并且效果很好。
        • @JordanGrant - 添加检查是有意义的,特别是如果输入数字在运行时未知(即用户输入的数字)。很高兴你发现这很有用。 :)
        【解决方案6】:
        output = ((input - input_start)/(input_end - input_start)) * (output_end - output_start) + output_start
        

        这样做是按比例找出输入“进入”输入范围的“多远”。然后,它将该比例应用于输出范围的大小,以绝对方式找出输出应该在输出范围内多远。然后它添加输出范围的开始以获得实际的输出编号。

        【讨论】:

        • 这将始终给出500,除了输入254,它将产生5500
        • 应该是output = ((out_end - out_start)/(input_end - input_start)) * input + output_start;
        • @jackw11111 不错。这里改进版input * ((output_end - output_start) / (input_end - input_start)) + output_start
        • @rofrol 我的错,实际上应该是output = (output_end - output_start)/(input_end - input_start) *( input - input_start) + output_start;,使用公式y - y1 = m(x-x1)
        • 我从这里 stackoverflow.com/questions/5731863/… 结束使用相同的算法,并修复了 ES6 版本 rosettacode.org/wiki/Map_range#JavaScript 的精度问题
        猜你喜欢
        • 2020-03-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-11
        • 1970-01-01
        • 2023-03-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多