反双线性插值?

42

我有四个二维点,p0 = (x0,y0), p1 = (x1,y1)等等,它们形成了一个四边形。在我的情况下,这个四边形不是矩形,但它至少应该是凸的。

  p2 --- p3
  |      |
t |  p   |
  |      |
  p0 --- p1
     s

我正在使用双线性插值。S和T在[0..1]之间,插值点由以下公式给出:

bilerp(s,t) = t*(s*p3+(1-s)*p2) + (1-t)*(s*p1+(1-s)*p0)

问题是这样的。我有一个2D点p,我知道它在四边形内。我想找到s,t,当使用双线性插值时,可以给我该点。

是否有一个简单的公式来反转双线性插值?


感谢解决方案。我将Naaff的解决方案实现发布为维基。


4
请参见这篇文章 - Phrogz
谢谢Phrogz,那篇文章让我“领悟”了。 - Tim Crews
程序图形大师Iñigo Quilez。看看吧。有时候会有一些未经解释的公式,但总体来说是一个很好的资源:https://iquilezles.org/www/articles/ibilinear/ibilinear.htm - Darkgaze
8个回答

30
我认为最简单的方法是把你的问题看作是一个交点问题:点p与由p0、p1、p2和p3定义的任意二维双线性曲面相交的参数位置(s,t)在哪里。
解决这个问题的方法与tspauld的建议类似。
从x和y的角度出发,可以得到两个方程式:
x = (1-s)*( (1-t)*x0 + t*x2 ) + s*( (1-t)*x1 + t*x3 )
y = (1-s)*( (1-t)*y0 + t*y2 ) + s*( (1-t)*y1 + t*y3 )

求解t:
t = ( (1-s)*(x0-x) + s*(x1-x) ) / ( (1-s)*(x0-x2) + s*(x1-x3) )
t = ( (1-s)*(y0-y) + s*(y1-y) ) / ( (1-s)*(y0-y2) + s*(y1-y3) )

我们现在可以将这两个方程相等,以消除t。将所有内容移到左侧并简化,我们得到一个形式为的方程式:
A*(1-s)^2 + B*2s(1-s) + C*s^2 = 0

在哪里:

A = (p0-p) X (p0-p2)
B = ( (p0-p) X (p1-p3) + (p1-p) X (p0-p2) ) / 2
C = (p1-p) X (p1-p3)

请注意,我使用运算符X表示2D叉积(例如,p0 X p1 = x0*y1 - y0*x1)。我将此方程式格式化为二次Bernstein多项式,这使得事情更加优雅且更具数值稳定性。s的解是该方程式的根。我们可以使用Bernstein多项式的二次公式找到根:
s = ( (A-B) +- sqrt(B^2 - A*C) ) / ( A - 2*B + C )

二次公式由+-得到两个答案。如果您只对p位于双线性表面内的解感兴趣,则可以丢弃任何s不在0和1之间的答案。要找到t,只需将s代回我们通过求解以s为变量的t而得到的上述两个方程之一即可。
我应该指出一个重要的特殊情况。如果分母A-2*B+C=0,则您的二次多项式实际上是线性的。在这种情况下,您必须使用一个更简单的方程来找到s:
s = A / (A-C)

这将给你一个确切的解,除非A-C = 0。如果A = C,则有两种情况:A=C=0意味着所有s的值都包含p,否则没有任何s的值包含p。

你如何解释每组(4)点有两组解决方案的情况?直观地说,应该只有一个解决方案。 - Juan Campa
没关系,我的直觉是错误的。如果没有任何线段是平行的,那么有两个解决方案,正如你所暗示的,一个在四边形内部(如果是凸的),另一个在外部。非常好的答案! - Juan Campa

13

可以使用牛顿法迭代地解决下列非线性方程组:

p = p0*(1-s)*(1-t) + p1*s*(1-t) + p2*s*t + p3*(1-s)*t.

请注意,这里有两个方程(方程的x和y分量相等),以及两个未知数(s和t)。
为了应用牛顿法,我们需要残差residual r,即:
r = p - (p0*(1-s)*(1-t) + p1*s*(1-t) + p2*s*t + p3*(1-s)*t),

并且Jacobian矩阵 J,通过对残差进行微分得到。对于我们的问题,Jacobian矩阵为:

J(:,1) = dr/ds = -p0*(1-t) + p1*(1-t) + p2*t - p3*t   (first column of matrix)
J(:,2) = dr/dt =  -p0*(1-s) - p1*s + p2*s + p3*(1-s)    (second column).

要使用牛顿法,首先需要一个初始猜测值(s,t),然后进行以下迭代数次:

(s,t) = (s,t) - J^-1 r,

每次迭代都使用新的{s}和{t}值重新计算J和r。在每次迭代中,主要成本在于将Jacobian的逆应用于残差(J^-1 r),通过解一个以J为系数矩阵,以r为右手边的2x2线性系统。

方法的直觉:

直观地说,如果四边形是平行四边形,那么解决问题就会容易得多。牛顿法使用连续的平行四边形近似来解决四边形问题。在每次迭代中,我们

  1. 使用点(s,t)处的局部导数信息来用平行四边形近似表示四边形。

  2. 通过解一个线性系统找到在平行四边形近似下的正确(s,t)值。

  3. 跳转到这个新点并重复。

该方法的优点:

作为牛顿类型方法的预期,收敛速度非常快。随着迭代的进行,该方法不仅越来越接近真实点,而且局部平行四边形逼近也变得更加准确,因此收敛速度本身也增加了(在迭代方法的术语中,我们说牛顿法是二次收敛)。在实践中,这意味着每次迭代的正确数字数量大约增加一倍。
以下是我进行的随机试验的迭代与误差的代表性表格(有关代码,请参见下文):
Iteration  Error
1          0.0610
2          9.8914e-04
3          2.6872e-07
4          1.9810e-14
5          5.5511e-17 (machine epsilon)

经过两次迭代,误差已经足够小,对于大多数实际应用来说几乎不会被注意到,并且经过5次迭代后,结果精确到机器精度的限度。

如果您固定迭代次数(比如,对于大多数实际应用,使用3次迭代,而对于需要非常高的精度,则使用8次迭代),那么该算法具有非常简单和直接的逻辑结构,适合高性能计算。无需检查各种特殊边缘情况*,也无需根据结果使用不同的逻辑。它只是一个包含几个简单公式的for循环。以下我将强调这种方法相对于其他答案中和互联网上发现的传统基于公式的方法的几个优点:

  • 易于编码。只需编写一个for循环并键入几个公式。

  • 没有条件或分支(if/then),这通常可以实现更好的流水线效率

  • 每次迭代仅需要1次除法,无需平方根(如果编写得好)。所有其他操作都是简单的加法、减法和乘法。平方根和除法通常比加法/乘法/乘法慢几倍,并且可能会在某些体系结构上损坏缓存效率(最明显的是在某些嵌入式系统上)。事实上,如果您在现代编程语言中查看平方根除法的实际计算方式,它们都使用牛顿法的变体,有时是硬件中的牛顿法,有时是软件中的牛顿法,具体取决于体系结构。

  • 可以轻松进行矢量化,以一次处理大量四边形数组。请参见下面的矢量化代码,以了解如何执行此操作的示例。

  • 扩展到任意维度。该算法可直接扩展到任意维度的反多线性插值(2d、3d、4d等)。我在下面包含了一个3D版本,人们可以想象编写一个简单的递归版本,或使用自动微分库来转到n维。牛顿法通常表现出无关维数的收敛速度,因此原则上该方法应该可扩展到当前硬件上的几千个维度(在此之后,构建和解决n乘以n矩阵J可能会成为限制因素)。

当然,这些大多数也取决于硬件、编译器和许多其他因素,所以你的结果可能会有所不同。
代码:
无论如何,这是我的Matlab代码:(我将所有内容都发布到公共领域)
基本的2D版本:
function q = bilinearInverse(p,p1,p2,p3,p4,iter)
%Computes the inverse of the bilinear map from [0,1]^2 to the convex
% quadrilateral defined by the ordered points p1 -> p2 -> p3 -> p4 -> p1.
%Uses Newton's method. Inputs must be column vectors.
    q = [0.5; 0.5]; %initial guess
    for k=1:iter
        s = q(1);
        t = q(2);
        r = p1*(1-s)*(1-t) + p2*s*(1-t) + p3*s*t + p4*(1-s)*t - p;%residual
        Js = -p1*(1-t) + p2*(1-t) + p3*t - p4*t; %dr/ds
        Jt = -p1*(1-s) - p2*s + p3*s + p4*(1-s); %dr/dt
        J = [Js,Jt];
        q = q - J\r;
        q = max(min(q,1),0);
    end
end

示例用法:

% Test_bilinearInverse.m
p1=[0.1;-0.1]; 
p2=[2.2;-0.9]; 
p3=[1.75;2.3]; 
p4=[-1.2;1.1];

q0 = rand(2,1);
s0 = q0(1); 
t0 = q0(2);
p = p1*(1-s0)*(1-t0) + p2*s0*(1-t0) + p3*s0*t0 + p4*(1-s0)*t0;

iter=5;
q = bilinearInverse(p,p1,p2,p3,p4,iter);

err = norm(q0-q);
disp(['Error after ',num2str(iter), ' iterations: ', num2str(err)])

示例输出:

>> test_bilinearInverse
Error after 5 iterations: 1.5701e-16

快速向量化的2D版本:

function [ss,tt] = bilinearInverseFast(px,py, p1x,p1y, p2x,p2y, p3x,p3y, p4x,p4y, iter)
%Computes the inverse of the bilinear map from [0,1]^2 to the convex
% quadrilateral defined by the ordered points p1 -> p2 -> p3 -> p4 -> p1,
% where the p1x is the x-coordinate of p1, p1y is the y-coordinate, etc.
% Vectorized: if you have a lot of quadrilaterals and 
% points to interpolate, then p1x(k) is the x-coordinate of point p1 on the
% k'th quadrilateral, and so forth.
%Uses Newton's method. Inputs must be column vectors.
    ss = 0.5 * ones(length(px),1);
    tt = 0.5 * ones(length(py),1);
    for k=1:iter
        r1 = p1x.*(1-ss).*(1-tt) + p2x.*ss.*(1-tt) + p3x.*ss.*tt + p4x.*(1-ss).*tt - px;%residual
        r2 = p1y.*(1-ss).*(1-tt) + p2y.*ss.*(1-tt) + p3y.*ss.*tt + p4y.*(1-ss).*tt - py;%residual

        J11 = -p1x.*(1-tt) + p2x.*(1-tt) + p3x.*tt - p4x.*tt; %dr/ds
        J21 = -p1y.*(1-tt) + p2y.*(1-tt) + p3y.*tt - p4y.*tt; %dr/ds
        J12 = -p1x.*(1-ss) - p2x.*ss + p3x.*ss + p4x.*(1-ss); %dr/dt
        J22 = -p1y.*(1-ss) - p2y.*ss + p3y.*ss + p4y.*(1-ss); %dr/dt

        inv_detJ = 1./(J11.*J22 - J12.*J21);

        ss = ss - inv_detJ.*(J22.*r1 - J12.*r2);
        tt = tt - inv_detJ.*(-J21.*r1 + J11.*r2);

        ss = min(max(ss, 0),1);
        tt = min(max(tt, 0),1);
    end
end

为了加快速度,此代码隐式使用以下公式计算2x2矩阵的逆:

[a,b;c,d]^-1 = (1/(ad-bc))[d, -b; -c, a]

示例用法:

% test_bilinearInverseFast.m
n_quads = 1e6; % 1 million quads
iter = 8;

% Make random quadrilaterals, ensuring points are ordered convex-ly
n_randpts = 4;
pp_xx = zeros(n_randpts,n_quads);
pp_yy = zeros(n_randpts,n_quads);
disp('Generating convex point ordering (may take some time).')
for k=1:n_quads
    while true
        p_xx = randn(4,1);
        p_yy = randn(4,1);
        conv_inds = convhull(p_xx, p_yy);
        if length(conv_inds) == 5
            break
        end
    end
    pp_xx(1:4,k) = p_xx(conv_inds(1:end-1));
    pp_yy(1:4,k) = p_yy(conv_inds(1:end-1));
end

pp1x = pp_xx(1,:);
pp1y = pp_yy(1,:);
pp2x = pp_xx(2,:);
pp2y = pp_yy(2,:);
pp3x = pp_xx(3,:);
pp3y = pp_yy(3,:);
pp4x = pp_xx(4,:);
pp4y = pp_yy(4,:);

% Make random interior points
ss0 = rand(1,n_quads);
tt0 = rand(1,n_quads);

ppx = pp1x.*(1-ss0).*(1-tt0) + pp2x.*ss0.*(1-tt0) + pp3x.*ss0.*tt0 + pp4x.*(1-ss0).*tt0;
ppy = pp1y.*(1-ss0).*(1-tt0) + pp2y.*ss0.*(1-tt0) + pp3y.*ss0.*tt0 + pp4y.*(1-ss0).*tt0;
pp = [ppx; ppy];

% Run fast inverse bilinear interpolation code:
disp('Running inverse bilinear interpolation.')
tic
[ss,tt] = bilinearInverseFast(ppx,ppy, pp1x,pp1y, pp2x,pp2y, pp3x,pp3y, pp4x,pp4y, 10);
time_elapsed = toc;

disp(['Number of quadrilaterals: ', num2str(n_quads)])
disp(['Inverse bilinear interpolation took: ', num2str(time_elapsed), ' seconds'])

err = norm([ss0;tt0] - [ss;tt],'fro')/norm([ss0;tt0],'fro');
disp(['Error: ', num2str(err)])

示例输出:

>> test_bilinearInverseFast
Generating convex point ordering (may take some time).
Running inverse bilinear interpolation.
Number of quadrilaterals: 1000000
Inverse bilinear interpolation took: 0.5274 seconds
Error: 8.6881e-16

3D版本:

包括一些代码来显示收敛进度。

function ss = trilinearInverse(p, p1,p2,p3,p4,p5,p6,p7,p8, iter)
%Computes the inverse of the trilinear map from [0,1]^3 to the box defined
% by points p1,...,p8, where the points are ordered consistent with
% p1~(0,0,0), p2~(0,0,1), p3~(0,1,0), p4~(1,0,0), p5~(0,1,1),
% p6~(1,0,1), p7~(1,1,0), p8~(1,1,1)
%Uses Gauss-Newton method. Inputs must be column vectors.
    tol = 1e-9;
    ss = [0.5; 0.5; 0.5]; %initial guess
    for k=1:iter
        s = ss(1);
        t = ss(2);
        w = ss(3);

        r = p1*(1-s)*(1-t)*(1-w) + p2*s*(1-t)*(1-w) + ...
            p3*(1-s)*t*(1-w)     + p4*(1-s)*(1-t)*w + ...
            p5*s*t*(1-w)         + p6*s*(1-t)*w + ...
            p7*(1-s)*t*w         + p8*s*t*w - p;

        disp(['k= ', num2str(k), ...
            ', residual norm= ', num2str(norm(r)),...
            ', [s,t,w]= ',num2str([s,t,w])])
        if (norm(r) < tol)
            break
        end

        Js = -p1*(1-t)*(1-w) + p2*(1-t)*(1-w) + ...
             -p3*t*(1-w)     - p4*(1-t)*w + ...
              p5*t*(1-w)     + p6*(1-t)*w + ...
             -p7*t*w         + p8*t*w;

         Jt = -p1*(1-s)*(1-w) - p2*s*(1-w) + ...
               p3*(1-s)*(1-w) - p4*(1-s)*w + ...
               p5*s*(1-w)     - p6*s*w + ...
               p7*(1-s)*w     + p8*s*w;

         Jw = -p1*(1-s)*(1-t) - p2*s*(1-t) + ...
              -p3*(1-s)*t     + p4*(1-s)*(1-t) + ...
              -p5*s*t         + p6*s*(1-t) + ...
               p7*(1-s)*t     + p8*s*t;

        J = [Js,Jt,Jw];
        ss = ss - J\r;
    end
end

使用示例:

%test_trilinearInverse.m
h = 0.25;
p1 = [0;0;0] + h*randn(3,1);
p2 = [0;0;1] + h*randn(3,1);
p3 = [0;1;0] + h*randn(3,1);
p4 = [1;0;0] + h*randn(3,1);
p5 = [0;1;1] + h*randn(3,1);
p6 = [1;0;1] + h*randn(3,1);
p7 = [1;1;0] + h*randn(3,1);
p8 = [1;1;1] + h*randn(3,1);

s0 = rand;
t0 = rand;
w0 = rand;
p = p1*(1-s0)*(1-t0)*(1-w0) + p2*s0*(1-t0)*(1-w0) + ...
            p3*(1-s0)*t0*(1-w0)     + p4*(1-s0)*(1-t0)*w0 + ...
            p5*s0*t0*(1-w0)         + p6*s0*(1-t0)*w0 + ...
            p7*(1-s0)*t0*w0         + p8*s0*t0*w0;

ss = trilinearInverse(p, p1,p2,p3,p4,p5,p6,p7,p8);

disp(['error= ', num2str(norm(ss - [s0;t0;w0]))])

示例输出:

test_trilinearInverse
k= 1, residual norm= 0.38102, [s,t,w]= 0.5         0.5         0.5
k= 2, residual norm= 0.025324, [s,t,w]= 0.37896     0.59901     0.17658
k= 3, residual norm= 0.00037108, [s,t,w]= 0.40228     0.62124     0.15398
k= 4, residual norm= 9.1441e-08, [s,t,w]= 0.40218     0.62067     0.15437
k= 5, residual norm= 3.3548e-15, [s,t,w]= 0.40218     0.62067     0.15437
error= 4.8759e-15

在输入点的顺序上必须小心,因为反向多线性插值只有在形状具有正体积时才是明确定义的,在三维中,选择使形状变为自身内部的点要容易得多。


我最喜欢你的解决方案并给它点了赞。然而,在处理3D时,我无法获得快速收敛。相反,我得到了一个相当停滞不前norm(r)下降(或者你上面使用的是什么?)。 - Sebastian
你是在使用随机点进行测试吗?如果是这种情况,那么“盒子”很可能不是一个盒子,而是一些形状会自己翻转的东西。那么问题就没有被明确定义,会有多个局部甚至全局最小值。只要你能保证输入点实际上形成了一个明确定义的内部体积,我认为该方法应该可以工作。 - Nick Alger
1
好的,我写了一个似乎可以工作的三维版本,并将其添加到我的答案中。 - Nick Alger
没问题。如果有人想使用,我也会将这个3D代码发布到公共领域。 - Nick Alger
1
我有一个Python实现,还将此答案中的四边形向量化:https://dev59.com/uVgR5IYBdhLWcg3wHqXB#65566295 - pthibault
显示剩余2条评论

8
这是我对Naaff解决方案的实现,作为社区wiki。再次感谢。
这是一个C语言实现,但应该也能用于C ++。它包括一个模糊测试函数。
#include <stdlib.h>
#include <stdio.h>
#include <math.h>

int equals( double a, double b, double tolerance )
{
    return ( a == b ) ||
      ( ( a <= ( b + tolerance ) ) &&
        ( a >= ( b - tolerance ) ) );
}

double cross2( double x0, double y0, double x1, double y1 )
{
    return x0*y1 - y0*x1;
}

int in_range( double val, double range_min, double range_max, double tol )
{
    return ((val+tol) >= range_min) && ((val-tol) <= range_max);
}

/* Returns number of solutions found.  If there is one valid solution, it will be put in s and t */
int inverseBilerp( double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, double x, double y, double* sout, double* tout, double* s2out, double* t2out )
{
    int t_valid, t2_valid;

    double a  = cross2( x0-x, y0-y, x0-x2, y0-y2 );
    double b1 = cross2( x0-x, y0-y, x1-x3, y1-y3 );
    double b2 = cross2( x1-x, y1-y, x0-x2, y0-y2 );
    double c  = cross2( x1-x, y1-y, x1-x3, y1-y3 );
    double b  = 0.5 * (b1 + b2);

    double s, s2, t, t2;

    double am2bpc = a-2*b+c;
    /* this is how many valid s values we have */
    int num_valid_s = 0;

    if ( equals( am2bpc, 0, 1e-10 ) )
    {
        if ( equals( a-c, 0, 1e-10 ) )
        {
            /* Looks like the input is a line */
            /* You could set s=0.5 and solve for t if you wanted to */
            return 0;
        }
        s = a / (a-c);
        if ( in_range( s, 0, 1, 1e-10 ) )
            num_valid_s = 1;
    }
    else
    {
        double sqrtbsqmac = sqrt( b*b - a*c );
        s  = ((a-b) - sqrtbsqmac) / am2bpc;
        s2 = ((a-b) + sqrtbsqmac) / am2bpc;
        num_valid_s = 0;
        if ( in_range( s, 0, 1, 1e-10 ) )
        {
            num_valid_s++;
            if ( in_range( s2, 0, 1, 1e-10 ) )
                num_valid_s++;
        }
        else
        {
            if ( in_range( s2, 0, 1, 1e-10 ) )
            {
                num_valid_s++;
                s = s2;
            }
        }
    }

    if ( num_valid_s == 0 )
        return 0;

    t_valid = 0;
    if ( num_valid_s >= 1 )
    {
        double tdenom_x = (1-s)*(x0-x2) + s*(x1-x3);
        double tdenom_y = (1-s)*(y0-y2) + s*(y1-y3);
        t_valid = 1;
        if ( equals( tdenom_x, 0, 1e-10 ) && equals( tdenom_y, 0, 1e-10 ) )
        {
            t_valid = 0;
        }
        else
        {
            /* Choose the more robust denominator */
            if ( fabs( tdenom_x ) > fabs( tdenom_y ) )
            {
                t = ( (1-s)*(x0-x) + s*(x1-x) ) / ( tdenom_x );
            }
            else
            {
                t = ( (1-s)*(y0-y) + s*(y1-y) ) / ( tdenom_y );
            }
            if ( !in_range( t, 0, 1, 1e-10 ) )
                t_valid = 0;
        }
    }

    /* Same thing for s2 and t2 */
    t2_valid = 0;
    if ( num_valid_s == 2 )
    {
        double tdenom_x = (1-s2)*(x0-x2) + s2*(x1-x3);
        double tdenom_y = (1-s2)*(y0-y2) + s2*(y1-y3);
        t2_valid = 1;
        if ( equals( tdenom_x, 0, 1e-10 ) && equals( tdenom_y, 0, 1e-10 ) )
        {
            t2_valid = 0;
        }
        else
        {
            /* Choose the more robust denominator */
            if ( fabs( tdenom_x ) > fabs( tdenom_y ) )
            {
                t2 = ( (1-s2)*(x0-x) + s2*(x1-x) ) / ( tdenom_x );
            }
            else
            {
                t2 = ( (1-s2)*(y0-y) + s2*(y1-y) ) / ( tdenom_y );
            }
            if ( !in_range( t2, 0, 1, 1e-10 ) )
                t2_valid = 0;
        }
    }

    /* Final cleanup */
    if ( t2_valid && !t_valid )
    {
        s = s2;
        t = t2;
        t_valid = t2_valid;
        t2_valid = 0;
    }

    /* Output */
    if ( t_valid )
    {
        *sout = s;
        *tout = t;
    }

    if ( t2_valid )
    {
        *s2out = s2;
        *t2out = t2;
    }

    return t_valid + t2_valid;
}

void bilerp( double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, double s, double t, double* x, double* y )
{
    *x = t*(s*x3+(1-s)*x2) + (1-t)*(s*x1+(1-s)*x0);
    *y = t*(s*y3+(1-s)*y2) + (1-t)*(s*y1+(1-s)*y0);
}

double randrange( double range_min, double range_max )
{
    double range_width = range_max - range_min;
    double rand01 = (rand() / (double)RAND_MAX);
    return (rand01 * range_width) + range_min;
}

/* Returns number of failed trials */
int fuzzTestInvBilerp( int num_trials )
{
    int num_failed = 0;

    double x0, y0, x1, y1, x2, y2, x3, y3, x, y, s, t, s2, t2, orig_s, orig_t;
    int num_st;
    int itrial;
    for ( itrial = 0; itrial < num_trials; itrial++ )
    {
        int failed = 0;
        /* Get random positions for the corners of the quad */
        x0 = randrange( -10, 10 );
        y0 = randrange( -10, 10 );
        x1 = randrange( -10, 10 );
        y1 = randrange( -10, 10 );
        x2 = randrange( -10, 10 );
        y2 = randrange( -10, 10 );
        x3 = randrange( -10, 10 );
        y3 = randrange( -10, 10 );
        /*x0 = 0, y0 = 0, x1 = 1, y1 = 0, x2 = 0, y2 = 1, x3 = 1, y3 = 1;*/
        /* Get random s and t */
        s = randrange( 0, 1 );
        t = randrange( 0, 1 );
        orig_s = s;
        orig_t = t;
        /* bilerp to get x and y */
        bilerp( x0, y0, x1, y1, x2, y2, x3, y3, s, t, &x, &y );
        /* invert */
        num_st = inverseBilerp( x0, y0, x1, y1, x2, y2, x3, y3, x, y, &s, &t, &s2, &t2 );
        if ( num_st == 0 )
        {
            failed = 1;
        }
        else if ( num_st == 1 )
        {
            if ( !(equals( orig_s, s, 1e-5 ) && equals( orig_t, t, 1e-5 )) )
                failed = 1;
        }
        else if ( num_st == 2 )
        {
            if ( !((equals( orig_s, s , 1e-5 ) && equals( orig_t, t , 1e-5 )) ||
                   (equals( orig_s, s2, 1e-5 ) && equals( orig_t, t2, 1e-5 )) ) )
               failed = 1;
        }

        if ( failed )
        {
            num_failed++;
            printf("Failed trial %d\n", itrial);
        }
    }

    return num_failed;
}

int main( int argc, char** argv )
{
    int num_failed;
    srand( 0 );

    num_failed = fuzzTestInvBilerp( 100000000 );

    printf("%d of the tests failed\n", num_failed);
    getc(stdin);

    return 0;
}

5

由于您正在处理2D图像,因此您的bilerp函数实际上是两个方程,一个用于x轴,另一个用于y轴。它们可以重写为以下形式:

x = t * s * A.x + t * B.x + s * C.x + D.x
y = t * s * A.y + t * B.y + s * C.y + D.y

位置:

A = p3 - p2 - p1 + p0
B = p2 - p0
C = p1 - p0
D = p0

重写第一个公式,用s表示t,代入第二个公式,解出s

1

这是我的实现...我猜它比tfiniga的更快

void invbilerp( float x, float y, float x00, float x01, float x10, float x11,  float y00, float y01, float y10, float y11, float [] uv ){

// substition 1 ( see. derivation )
float dx0 = x01 - x00;
float dx1 = x11 - x10;
float dy0 = y01 - y00;
float dy1 = y11 - y10;

// substitution 2 ( see. derivation )
float x00x = x00 - x;
float xd   = x10 - x00;
float dxd  = dx1 - dx0; 
float y00y = y00 - y;
float yd   = y10 - y00;
float dyd  = dy1 - dy0;

// solution of quadratic equations
float c =   x00x*yd - y00y*xd;
float b =   dx0*yd  + dyd*x00x - dy0*xd - dxd*y00y;
float a =   dx0*dyd - dy0*dxd;
float D2 = b*b - 4*a*c;
float D  = sqrt( D2 );
float u = (-b - D)/(2*a);

// backsubstitution of "u" to obtain "v"
float v;
float denom_x = xd + u*dxd;
float denom_y = yd + u*dyd;
if( abs(denom_x)>abs(denom_y) ){  v = -( x00x + u*dx0 )/denom_x;  }else{  v = -( y00y + u*dy0 )/denom_y;  }
uv[0]=u;
uv[1]=v;

/* 
// do you really need second solution ? 
u = (-b + D)/(2*a);
denom_x = xd + u*dxd;
denom_y = yd + u*dyd;
if( abs(denom_x)>abs(denom_y) ){  v = -( x00x + u*dx0 )/denom_x;  }else{  v2 = -( y00y + u*dy0 )/denom_y;  }
uv[2]=u;
uv[3]=v;
*/ 
}

和派生

// starting from bilinear interpolation
(1-v)*(  (1-u)*x00 + u*x01 ) + v*( (1-u)*x10 + u*x11 )     - x
(1-v)*(  (1-u)*y00 + u*y01 ) + v*( (1-u)*y10 + u*y11 )     - y

substition 1:
dx0 = x01 - x00
dx1 = x11 - x10
dy0 = y01 - y00
dy1 = y11 - y10

we get:
(1-v) * ( x00 + u*dx0 )  + v * (  x10 + u*dx1  )  - x   = 0
(1-v) * ( y00 + u*dy0 )  + v * (  y10 + u*dy1  )  - y   = 0

we are trying to extract "v" out
x00 + u*dx0   + v*(  x10 - x00 + u*( dx1 - dx0 ) )  - x = 0
y00 + u*dy0   + v*(  y10 - y00 + u*( dy1 - dy0 ) )  - y = 0

substition 2:
x00x = x00 - x
xd   = x10 - x00
dxd  = dx1 - dx0 
y00y = y00 - y
yd   = y10 - y00
dyd  = dy1 - dy0 

// much nicer
x00x + u*dx0   + v*(  xd + u*dxd )  = 0
y00x + u*dy0   + v*(  yd + u*dyd )  = 0

// this equations for "v" are used for back substition
v = -( x00x + u*dx0 ) / (  xd + u*dxd  )
v = -( y00x + u*dy0 ) / (  yd + u*dyd  )

// but for now, we eliminate "v" to get one eqution for "u"  
( x00x + u*dx0 ) / (  xd + u*dxd )  =  ( y00y + u*dy0 ) / (  yd + u*dyd  )

put denominators to other side

( x00x + u*dx0 ) * (  yd + u*dyd )  =  ( y00y + u*dy0 ) * (  xd + u*dxd  )

x00x*yd + u*( dx0*yd + dyd*x00x ) + u^2* dx0*dyd = y00y*xd + u*( dy0*xd + dxd*y00y ) + u^2* dy0*dxd  

// which is quadratic equation with these coefficients 
c =   x00x*yd - y00y*xd
b =   dx0*yd  + dyd*x00x - dy0*xd - dxd*y00y
a =   dx0*dyd - dy0*dxd

1
如果你遇到了问题,可以尝试将x10/y10和x01/y01的输入翻转,然后它就能完美地工作了。也就是说,使用你的01节点作为你的10节点,反之亦然。这是因为“常见”点顺序的推导起点如下: (1-v) * ((1-u) * x00 + u * x10) + v * ((1-u) * x01 + u * x11)。这里展示的公式如下: (1-v) * ((1-u) * x00 + u * x01) + v * ((1-u) * x10 + u * x11)。请注意,x01和x10被翻转了。代码和推导是正确的;只是与最常见的顶点顺序相比,“颠倒了”。 - Luke Briggs
1
注释中的部分“你真的需要第二个解决方案吗?”有一些错误:denom_xdenom_y未定义,而v2应该是v - user202729
1
测试结果只对一半的输入范围正确,对于另一半,则会给出超出范围的值,无论您使用第一种还是第二种解决方案(即使在修复第二个方程式源代码中的拼写错误后)。它还无法处理取负数平方根的情况。 - Adam

1

如果你只有一个p的值,该值在正方形的四个角落的最小值和最大值之间,通常不可能找到一个单一的解(s,t),使得双线性插值器提供给你这个值。

通常情况下,在正方形内有无限多个解(s,t)。它们沿着一个曲线(双曲线)路径穿过正方形。

如果您的函数是矢量值函数,因此您在正方形中的某个未知点上具有两个已知值?如果在正方形的每个角落处都已知两个参数的值,则可能存在一个解,但不能保证。请记住,我们可以将其视为两个单独的、独立的问题。每个问题的解将沿着正方形内的双曲线等高线。如果这对等高线在正方形内相交,则存在解。如果它们不相交,则不存在解。

您还问是否存在解决该问题的简单公式。抱歉,我没有看到真正的答案。就像我说的,这些曲线是双曲线。

一种解决方法是切换到另一种插值方法。因此,不使用双线性插值,而是将正方形分成一对三角形。在每个三角形内部,我们现在可以使用真正的线性插值。因此,现在我们可以解决每个三角形中的2个未知数的线性方程组。除了极少数情况下对应的分段线性等高线恰好重合之外,每个三角形可能有一个解。


1
一些回答略微误解了你的问题。即,他们假设你已经知道了未知插值函数的值,而不是要找到四边形内插值位置p(x,y)的(s,t)坐标。这是一个更简单的问题,并且保证有一个解是通过四边形的两条直线相交得到的。
其中一条直线将穿过线段p0p1和p2p3,另一条将穿过p0p2和p1p3,类似于轴对齐的情况。这些直线由p(x,y)的位置唯一定义,并且显然在此点相交。
考虑只穿过p0p1和p2p3的直线,我们可以想象出这样的一族直线,对于我们选择的每个不同的s值,每个都在不同的宽度处切割四边形。如果我们固定一个s值,我们可以通过设置t=0和t=1来找到两个端点。
因此,首先假设该直线具有以下形式: y = a0*x + b0
然后,如果我们固定给定的s值,我们就知道了这条直线的两个端点。它们是:
(1-s)p0 + (s)p1
(1-s)p2 + (s)p3

给定这两个端点,我们可以通过将它们代入线的方程并解出a0和b0作为s的函数来确定线的族。设置一个s值即可得到特定线的公式。现在我们需要找出这个族中哪条线与我们的点p(x,y)相交。只需将p(x,y)的坐标代入我们的线公式,然后解出目标s值即可。

同样的方法也可以用来找到t。


x=Cst和y=Cst的轨迹是双曲线分支,而不是直线,在(s,t)平面上。因此,有两个解决方案。 - user1196549

0

如果p是一个二维点,那么你可以轻松地得到它。在这种情况下,S是T处四边形总宽度的分数部分,T同样是S处四边形总高度的分数部分。

然而,如果p是一个标量,就不一定可能,因为双线性插值函数不一定是单调的。


我可能误解了,但这只适用于轴对齐的矩形,不是吗?我编辑了我的帖子以使其更清晰。 - tfinniga

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