Python中的圆形插值

时间:2013-04-08 21:03:11

标签: python interpolation modular

我有两个系统,每个系统都有一个方向传感器(0-360度),但传感器可以提供截然不同的值,具体取决于每个系统的方向和每个传感器的线性度。我有一个机械参考,我可以用它来生成一个表,显示每个系统实际指向的位置。这会产生一个包含三列的表:

Physical  SystemA  SystemB
--------  -------  -------
 000.0     005.7    182.3
 005.0     009.8    178.4
 ...       ...      ...

从显示的数据中,我们可以看到SystemA离物理参考不远,但SystemB大约180度关闭,并且反方向(想象它是颠倒安装)。

我需要能够在所有三个值之间来回映射:如果SystemA报告某事物是105.7,我需要告诉用户物理方向是什么,然后告诉SystemB指向相同的位置。如果SystemB进行初始报告,则相同。用户可以请求两个系统指向所需的物理方向,因此需要告诉SystemA和SystemB指向哪里。

线性插值并不难,但是当数据向相反的方向发送时我遇到了麻烦,并且是模块化/循环的。

是否有Pythonic方法来完成所有这些映射?


编辑:让我们关注最困难的情况,我们有两个配对值列表:

A        B
-----    -----
  0.0    182.5
 10.0    172.3
 20.0    161.4
 ...      ...
170.0      9.7
180.0    359.1
190.0    348.2
 ...      ...
340.0    163.6
350.0    171.8

假设这些列表来自两个不同的雷达,其指针与北方或其他任何东西不对齐,但我们通过移动目标周围并看到每个雷达必须指向的位置来手动获取上述数据。

当雷达A说“我的目标是123.4!”时,我需要在哪里瞄准雷达B才能看到它?如果雷达B找到目标,我在哪里告诉雷达A指向?

列表A在最后一个元素和第一个元素之间进行换行,但列表B更靠近列表的中间。列表A单调增加,而列表B单调减少。请注意,A上的学位大小通常与B上的学位大小不同。

是否有一个简单的插补器在以下情况下正确包装:

  1. 从列表A插入到列表B。

  2. 从列表B到列表A的插值。

  3. 可以使用两个单独的插补器实例,一个用于在每个方向上进行。我假设线性(一阶)插值器没问题,但我可能希望将来使用高阶插值或样条插值。

    一些测试用例:

    • A = 356.7,B =?

    • A = 179.2,B =?

4 个答案:

答案 0 :(得分:1)

这对我有用。可能会使用一些清理工作。

class InterpolatedArray(object):
    """ An array-like object that provides interpolated values between set points.
    """
    points = None
    wrap_value = None
    offset = None

    def _mod_delta(self, a, b):
        """ Perform a difference within a modular domain.
            Return a value in the range +/- wrap_value/2.
        """
        limit = self.wrap_value / 2.
        val = a - b
        if val < -limit: val += self.wrap_value
        elif val > limit: val -= self.wrap_value
        return val

    def __init__(self, points, wrap_value=None):
        """Initialization of InterpolatedArray instance.

        Parameter 'points' is a list of two-element tuples, each of which maps
        an input value to an output value.  The list does not need to be sorted.

        Optional parameter 'wrap_value' is used when the domain is closed, to
        indicate that both the input and output domains wrap.  For example, a
        table of degree values would provide a 'wrap_value' of 360.0.

        After sorting, a wrapped domain's output values must all be monotonic
        in either the positive or negative direction.

        For tables that don't wrap, attempts to interpolate values outside the
        input range cause a ValueError exception.
        """
        if wrap_value is None:
            points.sort()   # Sort in-place on first element of each tuple
        else:   # Force values to positive modular range
            points = sorted([(p[0]%wrap_value, p[1]%wrap_value) for p in points])
            # Wrapped domains must be monotonic, positive or negative
            monotonic = [points[x][1] < points[x+1][1] for x in xrange(0,len(points)-1)]
            num_pos_steps = monotonic.count(True)
            num_neg_steps = monotonic.count(False)
            if num_pos_steps > 1 and num_neg_steps > 1: # Allow 1 wrap point
                raise ValueError("Table for wrapped domains must be monotonic.")
        self.wrap_value = wrap_value
        # Pre-compute inter-value slopes
        self.x_list, self.y_list = zip(*points)
        if wrap_value is None:
            intervals = zip(self.x_list, self.x_list[1:], self.y_list, self.y_list[1:])
            self.slopes = [(y2 - y1)/(x2 - x1) for x1, x2, y1, y2 in intervals]
        else:   # Create modular slopes, including wrap element
            x_rot = list(self.x_list[1:]); x_rot.append(self.x_list[0])
            y_rot = list(self.y_list[1:]); y_rot.append(self.y_list[0])
            intervals = zip(self.x_list, x_rot, self.y_list, y_rot)
            self.slopes = [self._mod_delta(y2, y1)/self._mod_delta(x2, x1) for x1, x2, y1, y2 in intervals]

    def __getitem__(self, x):       # Works with indexing operator []
        result = None
        if self.wrap_value is None:
            if x < self.x_list[0] or x > self.x_list[-1]:
                raise ValueError('Input value out-of-range: %s'%str(x))
            i = bisect.bisect_left(self.x_list, x) - 1
            result = self.y_list[i] + self.slopes[i] * (x - self.x_list[i])
        else:
            x %= self.wrap_value
            i = bisect.bisect_left(self.x_list, x) - 1
            result = self.y_list[i] + self.slopes[i] * self._mod_delta(x, self.x_list[i])
            result %= self.wrap_value
        return result

测试:

import nose

def xfrange(start, stop, step=1.):
    """ Floating point equivalent to xrange()."""
    while start < stop:
        yield start
        start += step

# Test simple inverted mapping for non-wrapped domain
pts = [(x,-x) for x in xfrange(1.,16., 1.)]
a = InterpolatedArray(pts)
for i in xfrange(1., 15., 0.1):
    nose.tools.assert_almost_equal(a[i], -i)
# Cause expected over/under range errors
result = False  # Assume failure
try: x = a[0.5]
except ValueError: result = True
assert result
result = False
try: x = a[15.5]
except ValueError: result = True
assert result

# Test simple wrapped domain
wrap = 360.
offset = 1.234
pts = [(x,((wrap/2.) - x)) for x in xfrange(offset, wrap+offset, 10.)]
a = InterpolatedArray(pts, wrap)
for i in xfrange(0.5, wrap, 0.1):
    nose.tools.assert_almost_equal(a[i], (((wrap/2.) - i)%wrap))

答案 1 :(得分:0)

但你可以使用线性插值。如果您的样本A值是例如7.75,类似于2.5度。如果样品B值为180.35,则它也类似于2.5度。棘手的部分是当值溢出时,如果可能的话。只需设置一组单元测试来检查你的算法是否有效,你应该快速前进。

答案 2 :(得分:0)

回答第1部分:包含校准值+漂移值的转换表。

基本上,如果DialA在物理上处于0时报告5.7,当它处于5时报告为9.7,那么我将漂移值设置为每个读出位置之间距离的+/- .25以考虑机械和读数漂移。

对第2部分的回答:在两个刻度盘上保持相同的值,同时显示预期的位置。

如果您不依赖于方向,则只需旋转输出拨盘直至其位于校准表的正确位置。

如果您依赖于方向,则需要跟踪最后1-2个值以确定方向。确定方向后,可以按所需方向移动从属拨盘,直到达到目标位置。

您的校准表也应包括方向(例如正面或负面)。

考虑到上述两个部分,您将能够补偿旋转偏移和方向翻转,并产生准确的位置和方向读数。

以下是给定校准表的一些代码,将产生位置和方向,这将解决显示问题并使从属拨号与主拨盘匹配:

#!/usr/bin/env python

# Calibration table
# calibrations[ device ][physical position]=recorded position
calibrations={}

calibrationsdrift=1.025

calibrations["WheelA"]={}

calibrations["WheelA"]={}
calibrations["WheelA"]["000.0"]=5.7
calibrations["WheelA"]["005.0"]=9.8
calibrations["WheelA"]["010.0"]=13.9
calibrations["WheelA"]["015.0"]=18.0

calibrations["WheelB"]={}
calibrations["WheelB"]["000.0"]=182.3
calibrations["WheelB"]["005.0"]=178.4
calibrations["WheelB"]["010.0"]=174.4
calibrations["WheelB"]["015.0"]=170.3


def physicalPosition( readout , device ):
        calibration=calibrations[device]
        for physicalpos,calibratedpos in calibration.items():
                if readout < ( calibratedpos + calibrationsdrift ):
                        if readout > ( calibratedpos - calibrationsdrift ):
                                return physicalpos
        return -0


print physicalPosition( 5.8 , "WheelA")
print physicalPosition( 9.8 , "WheelA")
print physicalPosition( 10.8 , "WheelA")


def physicalDirection( lastreadout, currentreadout, device ):
        lastposition=physicalPosition( lastreadout, device)
        currentposition=physicalPosition( currentreadout, device)
        # Assumes 360 = 0, so 355 is the last position before 0
        if lastposition < currentposition:
                if lastposition == 000.0:
                        if currentposition == 355:
                                return -1
                return 1
        else:
                return -1


print physicalDirection( 5.8, 10.8, "WheelA")
print physicalDirection( 10.8, 2.8, "WheelA")

print physicalDirection( 182, 174, "WheelB")
print physicalDirection( 170, 180, "WheelB")

运行程序显示方向已正确确定,即使对于WheelB,也是向后安装在面板/设备/等上:

$ ./dials 
000.0
005.0
005.0
1
-1
1
-1

请注意,输入功能的某些“读数”值已关闭。这可以通过漂移值来补偿。是否需要一个取决于您正在连接的设备。

答案 3 :(得分:0)

最简单的解决方案是使所有表格元素增加(或根据具体情况而减少),向各个元素添加或减去360以使其成为可能。将表格背靠背加倍,使其即使在所有添加和减少之后也覆盖0到360的整个范围。这使得简单的线性插值成为可能。然后,您可以在计算后使用模数360将其恢复到范围内。