将一系列数值映射到另一个范围

95

我正在寻找一些想法,如何在Python中将一个范围的值转换为另一个范围。我正在处理一个硬件项目,从传感器读取数据并返回一系列值,然后使用这些数据驱动需要不同范围值的执行器。

例如,假设传感器返回1到512的值范围,而执行器的驱动范围为5到10。我希望有一个函数,可以传递一个值和两个范围,并返回映射到第二个范围的值。如果这样的一个函数被命名为translate,它可以像这样使用:

sensor_value = 256
actuator_value = translate(sensor_value, 1, 512, 5, 10)
在这个例子中,我期望输出actuator_value7.5,因为sensor_value在可能的输入范围的中间位置。

3
感谢大家提供的所有答案,我接受了Adam Luchjenbroers的答案,因为它与我所想的非常相似,而且没有引入第三方库来完成一个相对简单的任务。 - Tendayi Mawushe
10个回答

120

一个解决方案是:

def translate(value, leftMin, leftMax, rightMin, rightMax):
    # Figure out how 'wide' each range is
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin

    # Convert the left range into a 0-1 range (float)
    valueScaled = float(value - leftMin) / float(leftSpan)

    # Convert the 0-1 range into a value in the right range.
    return rightMin + (valueScaled * rightSpan)

你可以使用代数来提高效率,但会牺牲可读性。


@Adam,谢谢你的回答。我遇到了同样的问题,花了一些时间才找到正确的解决方案。然后我想在sf上发布新的问题,并询问是否有更有效的解决方法。你能否用代数方式展示解决方案? - Matt
5
很好的解决方案。建议在代码中使用“to”和“from”代替“left”和“right”,因为后者可能会引起混淆。 - catalyst294
1
@catalyst294 - 必须使用 from_,因为 from 是 Python 中的保留字。 - PaulMcG
Python之禅:“显式优于隐式”。我怀疑你不会看到太多速度的提升,所以最好保持代码的美观! - Spencer

119

使用 scipy.interpolate.interp1d

你还可以使用 scipy.interpolate 包来进行这样的转换(如果你不介意依赖于 SciPy 的话):

>>> from scipy.interpolate import interp1d
>>> m = interp1d([1,512],[5,10])
>>> m(256)
array(7.4951076320939336)

或者将其从0级Scipy数组转换回普通浮点数:

>>> float(m(256))
7.4951076320939336

您也可以轻松地在一个命令中进行多个转换:

>>> m([100,200,300])
array([ 5.96868885,  6.94716243,  7.92563601])

作为额外加分,您可以进行非均匀映射,例如如果您想将[1,128]映射到[1,10],[128,256]映射到[10,90]和[256,512]映射到[90,100],则可以按照以下方式实现:

>>> m = interp1d([1,128,256,512],[1,10,90,100])
>>> float(m(400))
95.625

interp1d 创建分段线性插值对象(可以像函数一样调用)。

使用 numpy.interp

正如 ~unutbu 提到的那样,numpy.interp 也是一个选择(依赖较少):

>>> from numpy import interp
>>> interp(256,[1,512],[5,10])
7.4951076320939336

37
你也可以使用 numpy.interp(256,[1,512],[5,10]),以减少对 numpy 的依赖。 - unutbu
2
要将array([ 5.96868885, 6.94716243, 7.92563601])转换为列表,请使用m([100,200,300]).tolist() - zanetu
1
此外,scipy.interpolate.interp1d 的速度可能比 numpy.interp 慢得多。 (请参见此问题。)如果您关心性能,请先对代码进行基准测试。 - zanetu
对于非均匀插值,我想将0到2之间的数字映射到-15和1之间,并将高于2和6之间的数字映射到15和1之间。这可能吗?我尝试使用interp1d([0,2,2.1,6],[-15,1,15,1]),但我收到错误:x_new中的一个值超出了插值范围。 - Varlor
此外,是否也可以进行非线性插值,例如指数或平方插值? - Varlor
这个是否考虑了负输入? - Atif Ali

30

这实际上是创建闭包的一个很好的案例,也就是编写一个返回函数的函数。由于您可能有许多这些值,因此计算和重新计算每个值的这些值跨度和因子,也不会在传递那些最小/最大限制时有太大价值。

相反,尝试这样做:

def make_interpolater(left_min, left_max, right_min, right_max): 
    # Figure out how 'wide' each range is  
    leftSpan = left_max - left_min  
    rightSpan = right_max - right_min  

    # Compute the scale factor between left and right values 
    scaleFactor = float(rightSpan) / float(leftSpan) 

    # create interpolation function using pre-calculated scaleFactor
    def interp_fn(value):
        return right_min + (value-left_min)*scaleFactor

    return interp_fn

现在,您可以将您的处理器编写为:

# create function for doing interpolation of the desired
# ranges
scaler = make_interpolater(1, 512, 5, 10)

# receive list of raw values from sensor, assign to data_list

# now convert to scaled values using map 
scaled_data = map(scaler, data_list)

# or a list comprehension, if you prefer
scaled_data = [scaler(x) for x in data_list]

我喜欢这个答案,因为它不会将转换后的值夹在指定的最小/最大值之间,而是允许它们超出范围。 - Clay

14

我在Python中寻找同样的东西,将0-300度的角度映射到原始的Dynamixel值0-1023,或者根据执行器方向映射到1023-0。

最终,我选择了非常简单的方法。

变量:

x:input value; 
a,b:input range
c,d:output range
y:return value

功能:

def mapFromTo(x,a,b,c,d):
   y=(x-a)/(b-a)*(d-c)+c
   return y

用法:

dyn111.goal_position=mapFromTo(pos111,0,300,0,1024)

7
def translate(sensor_val, in_from, in_to, out_from, out_to):
    out_range = out_to - out_from
    in_range = in_to - in_from
    in_val = sensor_val - in_from
    val=(float(in_val)/in_range)*out_range
    out_val = out_from+val
    return out_val

7

简单的映射范围函数:

def mapRange(value, inMin, inMax, outMin, outMax):
    return outMin + (((value - inMin) / (inMax - inMin)) * (outMax - outMin))

4
def maprange(a, b, s):
    (a1, a2), (b1, b2) = a, b
    return  b1 + ((s - a1) * (b2 - b1) / (a2 - a1))


a = [from_lower, from_upper]
b = [to_lower, to_upper]

发现在https://rosettacode.org/wiki/Map_range#Python_

  • 不夹紧变换后的值到范围ab(它外推)
  • from_lower > from_upperto_lower > to_upper时也起作用

1

所有现有的答案都在CC BY-SA许可下发布。这是我写的一个答案;在法律允许的范围内,我放弃了所有与版权或相关邻近权利有关的权利(知识共享CC0公共领域贡献协议)。

def remap(number, from_min, from_max, to_min, to_max):                         
    number_s = number - from_min
    from_max_s = from_max - from_min
    to_max_s = to_max - to_min
    return ((number_s / from_max_s) * to_max_s) + to_min

0

你可以使用lambda函数

translate = lambda a, b, c, d, e: (a - b) * (e - d) / (c - b) + d

sensor_value = 256
translate(sensor_value, 1, 512, 5, 10)
>> 7.495107632093934

0

v 是值。将 a-b 范围映射到 c-d 范围。

def map_range(v, a, b, c, d):
       return (v-a) / (b-a) * (d-c) + c

使用方法

a = map_range(4, 0, 10, 0, 1)
print(a) # -> 0.4

1
请记住,Stack Overflow 不仅仅是为了解决当前的问题,还要帮助未来的读者找到类似问题的解决方案,这需要理解底层代码。对于我们社区中的初学者来说,这一点尤为重要,他们可能不熟悉语法。鉴于此,请问您能否编辑您的回答,包括对您所做的操作进行解释以及为什么您认为这是最佳方法? - Jeremy Caney

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