相位 unwrap 与 wrap 算法详解(附代码)
最近接手了一个项目,光通信方面的,我负责编写初测结果的数据处理算法,其中有一个算法,叫做 unwrap 与 wrap,之前没有听说过。通过询问同事与搜索引擎,原来,所谓的 unwrap 是将在 +180 度到 -180 度之间跳变的相位信号,也就是函数值,变为不跳变的形态。而 wrap 则是其逆运算,即将信号恢复成跳变状态。
这样概括实在太模糊,我们用数据与图象来详细说明。
基本概念与数学关系
假设有一组相位数据(在后续说明中,为方便起见,我们只粗略地用整数部分来进行说明,比如第一个值我们就当作 -59 度):
-59.634834
-92.078697
-126.824913
-158.910297
166.959301
133.708772
96.759715
61.819861
26.786247
-7.944068
...(后面还有很多数据)
如图,这些数据呈现一种类似周期性的循环,当然这并不是真的周期函数,只是因为当它的数值跨越正负 180 度时,就会根据其变化趋势,跳变到边界重新计算。这种状态我们称为相位的卷绕/折叠(网上说法不一),用 wrap 这个单词表示,所以相位的解卷绕/展开被称为 unwrap。
具体到这个示例数据,我们可以得到以下信息:
- 数据本身是递减趋势,从 -59 度不断递减,如 -59、-92、-126、-158。
- 数据中有一个跳变点,即 -158 度到 166 度,也就是图像中第一个正的峰值,跳变后继续下降,直到下次跳变。
unwrap 要做的事情,就是消除这个跳变,让 wrap 函数中 166 度这个点,恢复为 -158 - n 度(n 是正值,为两者之间的间隔)。那么 n 是多少呢,其实很简单,-158 度距离 -180 度为 22 度,达到 -180 度后溢出,起点跳变为 180 度,溢出的值从 180 度继续计算,直到 166 度,距离为 14 度,所以合起来,n 为 22 + 14 = 36 度。
根据这样的规则,wrap 中 166 度这个点,对应的其实是 unwrap 下的 -158 - 36 = -194 度。同时你会发现,166 + 194 恰好是 360 度,而从 -180 跳到 180,也是将函数值增加了 360 度。所以 wrap 的本质,就是当相位超出边界时,补偿 360 度并继续计算,而 unwrap 就是去除这个补偿。所以,wrap 状态下的 166 度对应 unwrap 状态下的 -194 度,之后的 133 度对应 133 - 360 = -227 度,后面每个 wrap 值也都减去 360 度,直到遇见下一个跳变点。
显而易见,第二个 -180 度跳变点,对应的 unwrap 度数为 -180 - 360 = -540 度,其后的值由于跳变,将再次被补偿 360 度,合起来就是 720 度。所以,第二次跳变后的 166 度值,对应的 unwrap 值其实是 -554 度。如此循环,每跳变一次,就要让 wrap 值在之前的基础上,再减去一次 360 度,才能变为正确的 unwrap 值。
由于这里的例子是递减的,wrap 从 -180 跳变到 +180,增加了 360 度,因此 unwrap 计算时需要反向减去 360 度。若是递增函数,那么每次 wrap 自然是从 +180 跳变到 -180,减少 360 度,那么每次 unwrap 就需要重新加上 360 度。你可以将示例数据全部取反,就可以很直观的理解。
因此 unwrap 与 wrap 之间的关系,可以表示为:
$$unwrap(x) = wrap(x) -360 * wrapNum(x) $$
或
$$unwrap(x) = wrap(x) +360 * wrapNum(x) $$
分别对应递减与递增的情况。其中 unwrap(x) 和 wrap(x) 表示两种状态下的函数值,wrapNum 表示跳变次数。
在这里我们将 +360 与 -360 统一使用变量 wrapCycle 来代替,即递减时,wrapCycle 为 +360,递增时为 -360。上面的公式就可以统一起来:
$$unwrap(x) = wrap(x) - wrapCycle * wrapNum(x) $$
unwrap(x) 与 wrap(x) 的图象如下:
计算方法
如公式$$unwrap(x) = wrap(x) - wrapCycle * wrapNum(x) $$所示,要使函数在 wrap 与 unwrap 之间转换,重点在于确定 wrapCycle 与 wrapNum。
wrapCycle 代表了相位函数是递增还是递减,大多数应用场景下,相位都是单调变化的,因此比较好确定,可以直接比对两个相邻 wrap 值的大小关系。若 wrap(1) > wrap(2),则递减,wrapCycle = 360;若 wrap(1) < wrap(2),递增,wrapCycle = -360;若两者相同,则可以换下一个值进行比对,一般来讲这种情况极少。
但这里有一个 bug,就是必须保证比较的两个值,不处于跳变点两端。也就是说,如果比对 -59 和 -92,那自然是正确的,但如果比对的值,类似于 -158 和 166,从而得到了递增的结论,那显然是错误的。因此这里就牵扯到了如何确定跳变点的问题。
同样的,要得到 wrapNum,也需要确认跳变点的个数。因此,核心问题就是如何确认跳变点。
1. wrap 函数的跳变点确认
如图,确定两个 wrap 值之间是否发生了跳变,是一个比较直观的问题。 一般的策略是,如果相邻两个值的差距大于 180 度,则说明发生了一次跳变。
但这里会产生一个“歧义”问题,也就是说,对于一个完全未知的 wrap 状态下的数据,我们无法确定相邻两个值之间的差距,到底是因为跳变产生的,还是它本身的变化就是如此。再具体点,也就是说,当你看到相邻两个点的 wrap 值突然变化了 180 度以上,就认为它发生了跳变,其实并不算充要条件,只有加上“相邻两点之间的实际相位变化不会超过 180 度”这样的条件,才能认为这两个 wrap 值的差距是跳变产生的,而不是它的相位值本来就这样变化。
另一个是跳变丢失的问题,也就是指,采集原始数据时,由于间隔太大,导致丢失了一些可以被判断为跳变的数据(差距超出 180 度的点没采集到),这样也无法判断两个值之间是否发生过跳变。解决方案就是,增加数据密集程度,保证两点之间至多只有一个跳变点。
因此,如果要准确的将采集到的数据从 wrap 变为 unwrap,需要你对数据的性质有足够的了解,并且采集点的密集程度要合适。
2. unwrap 函数的跳变点确认
unwrap 函数还原成 wrap 函数相对来讲没有那么多坑。由于 wrap 转 unwrap 的机制,对递增函数而言,其最小值不会小于 -180 度;对递减函数而言,其最大值不会超过 180 度。因此对于不同的情况,以 -180 度或 +180 度为起点,360 度为周期,每经过一个周期,就代表发生了一次跳变,使 wrapNum 加 1,然后乘以 wrapCycle 即可得出需要补偿的角度。
例如,一个递减 unwrap 函数中,如何计算 -600 度对应的 wrap 值?很简单,若设定 180 ~ -180 为第 0 个周期,则第 1 个周期为 -180 ~ -540,第 2 个周期为 -540 ~ -900,-600 处于该周期,wrapNum = 2,wrapCycle = 360,所以相应的 wrap 值为 -600 + 2 * 360 = 120。
编程示例
在这里使用 C 语言做一个示例,只考虑常规情况,即单调且不会出现歧义问题。
/*
描述:对函数进行解卷绕/卷绕
参数:
input:输入函数值
points:需要进行运算的点数,最大为 input 的数组长度
toggle:切换选项。输入 1 进行解卷绕 unwrap,输入 2 进行卷绕 wrap
*/
double* wrapToggle(double input[], int points, int toggle) {
if (points <= 1 || (toggle != 1 && toggle != 2)) {
return input;
}
int PI = 180,
cycleNum = 0,
cycle = 0;
double* result = (double*)malloc(sizeof(double) * points);
// 根据曲线的趋势,判断补偿周期的正负
if (input[0] > input[1]) {
cycle = 2 * PI;
}
else {
cycle = -2 * PI;
}
// 第一个值不变
result[0] = input[0];
// 判断是 unwrap 还是 wrap 运算
// unwrap 解卷绕
if (toggle == 1) {
// 若第 2 个点就跳变,说明之前判断的 cycle 是反的。
if (fabs(input[0] - input[1]) > PI) {
cycle = -cycle;
}
for (int i = 1; i < points; i++) {
// 相邻点差距大于 180 度,说明发生了跳变,补偿周期 +1
if (fabs(input[i - 1] - input[i])>PI) {
cycleNum++;
}
// 每个值都补偿一次周期
result[i] = input[i] - double(cycle) * cycleNum;
}
}
// wrap 卷绕
else if (toggle == 2) {
for (int i = 0; i < points; i++) {
// unwrap 函数绝对值大于 180 的才进行计算
if (fabs(input[i]) > PI) {
cycleNum = abs(int((input[i] + cycle / 2) / 360)) + 1;
result[i] = input[i] + double(cycle) * cycleNum;
}
else {
result[i] = input[i];
}
}
}
return result;
}