18

I have a non-grid-aligned set of input values associated with grid-aligned output values. Given a new input value I want to find the output:

                                  Four points in a rectangle with inputs varying on each axis, but outputs representing rectangular inputs.

(These are X,Y coordinates, calibrating an imprecise not-square eye-tracking input device to exact locations on screen.)

This looks like Bilinear Interpolation, but my input values are not grid-aligned. Given an input, how can I figure out a reasonable output value?


Answer: In this case where I have sets of input and output points, what is actually needed is to perform inverse bilinear interpolation to find the U,V coordinates of the input point within the quad, and then perform normal bilinear interpolation (as described in Nico's answer below) on the output quad using those U,V coordinates.

5
  • Can you assume linearity? Should rotation be allowed/needed? Commented May 28, 2014 at 20:26
  • @wildplasser As noted by the red numbers in the diagram, the values may be skewed as a parallelogram, or trapezoid, but not rotated. Imagine a projector pointing at a screen off-axis. Commented May 28, 2014 at 20:28
  • Ah sorry, I had not checked the exact values. BTW: why not just .5 pixel for both X and Y and rounding down (or up for the opposite corner) Commented May 28, 2014 at 20:33
  • I only named it a pixel for convinience. The discretisation/quantisation seems the same to me. Commented May 28, 2014 at 20:59
  • @wildplasser I apologize, but I do not understand your suggestion. Commented May 28, 2014 at 21:00

3 Answers 3

17

You can bilinearly interpolate in any convex tetragon. A cartesian grid is just a bit simpler because the calculation of interpolation parameters is trivial. In the general case you interpolate as follows:

parameters alpha, beta
interpolated value = (1 - alpha) * ((1 - beta) * p1 + beta * p2) + alpha * ((1 - beta) * p3 + beta * p4)

In order to calculate the parameters, you have to solve a system of equations. Put your input values in the places of p1 through p4 and solve for alpha and beta.

Then put your output values in the places of p1 through p4 and use the calculated parameters to calculate the final interpolated output value.

For a regular grid, the parameter calculation comes down to:

alpha = x / cell width
beta  = y / cell height

which automatically solves the equations.

Here is a sample interpolation for alpha=0.3 and beta=0.6

Sample interpolation

Actually, the equations can be solved analytically. However, the formulae are quite ugly. Therefore, iterative methods are probably nicer. There are two solutions for the system of equations. You need to pick the solution where both parameters are in [0, 1].

First solution:

alpha = -(b e - a f + d g - c h + sqrt(-4 (c e - a g) (d f - b h) +
        (b e - a f + d g - c h)^2))/(2 c e - 2 a g)    
beta  = (b e - a f - d g + c h + sqrt(-4 (c e - a g) (d f - b h) + 
        (b e - a f + d g - c h)^2))/(2 c f - 2 b g)

where

a = -p1.x + p3.x
b = -p1.x + p2.x
c = p1.x - p2.x - p3.x + p4.x
d = interpolated_point.x - p1.x
e = -p1.y + p3.y
f = -p1.y + p2.y
g = p1.y - p2.y - p3.y + p4.y
h = interpolated_point.y - p1.y

Second solution:

alpha = (-b e + a f - d g + c h + sqrt(-4 (c e - a g) (d f - b h) + 
        (b e - a f + d g - c h)^2))/(2 c e - 2 a g)
beta  = -((-b e + a f + d g - c h + sqrt(-4 (c e - a g) (d f - b h) + 
        (b e - a f + d g - c h)^2))/( 2 c f - 2 b g))
Sign up to request clarification or add additional context in comments.

5 Comments

It's a general interpolation scheme. You can interpolate anything - be it a single value, a vector or entire tensors. I don't completely get your scenario, so I can't tell if bilinear interpolation suits your needs, but you asked for it. The parameters are usually calculated with the system of equations. Since it's not linear, iterative methods are your friend. Actually it's the other way around than you stated. You already know the intersection and need to find the parameters that lead to this intersection.
Thank you for this. As shown in the diagram I put in my answer, you are right, this is nicely general purpose. :) FWIW I put a fun little experiment up to see what happens with concave tetragons, to test rotations, and more: phrogz.net/tmp/bilinear-colors.html
Is there a typo in the expression for beta? The denominator contains the term (cf-bg) so I think the expression inside the square root should also contain this term. I make the term inside the square root (-4(de-ah)(cf-bg) +(be-af-dg-ch)^2)
Thank you very much for this. I think I mostly get it and it seems to be applicable to 3D as well. Am I right that in 3D your systems for solving alpha and beta would be over specified?
Just looked at this again and I think the term inside the square root should be (-4(de-ah)(cf-bg) +(be-af-dg+ch)^2). There was a typo in my previous suggested correction - apologies!
4

Here's my own technique, along with code for deriving the resulting value. It requires three lerps of the output values (and three percentage calculations to determine the lerp percentages):

Note that this is not bilinear interpolation. It does not remap the quad of input points to the quad of output values, as some input points can result in output values outside the output quad.

Here I'm showing the non-aligned input values on a Cartesian plane (using the sample input values from the question above, multiplied by 10 for simplicity).

                     

To calculate the 'north' point (top green dot), we calculate the percentage across the X axis as

  (inputX - northwestX) / (northeastX - northwestX)
= (-4.2 - -19) / (10 - -19)
= 0.51034

We use this percentage to calculate the intercept at the Y axis by lerping between the top Y values:

  (targetValue - startValue) * percent + startValue
= (northeastY  - northwestY) * percent + northwestY
= (-8 - -7) * 0.51034 + -7
= -7.51034

We do the same on the 'south' edge:

  (inputX - southwestX) / (southeastX - southwestX)
= (-4.2 - -11) / (9 - -11)
= 0.34

  (southeastY - southwestY) * percent + southwestY
= (7 - 4) * 0.34 + 4
= 5.02

Finally, we use these two values to calculate the final percentage between the north and south edges:

  (inputY - southY) / (northY - southY)
= (1 - 5.02) / (-7.51034 - 5.02)
= 0.3208

With these three percentages in hand we can calculate our final output values by lerping between the points:

nw = Vector(-150,-100)
ne = Vector( 150,-100)
sw = Vector(-150, 100)
se = Vector( 150, 100)

north  = lerp( nw, ne, 0.51034)       --> (  3.10, -100.00)
south  = lerp( sw, se, 0.34)          --> (-48.00,  100.00)
result = lerp( south, north, 0.3208)  --> (-31.61,   35.84)

Finally, here is some (Lua) code performing the above. It uses a mutable Vector object that supports the ability to copy values from another vector and lerp its values towards another vector.

-- Creates a bilinear interpolator
-- Corners should be an object with nw/ne/sw/se keys,
-- each of which holds a pair of mutable Vectors
-- { nw={inp=vector1, out=vector2}, … }
function tetragonalBilinearInterpolator(corners)
  local sides = {
    n={ pt=Vector(), pts={corners.nw, corners.ne} },
    s={ pt=Vector(), pts={corners.sw, corners.se} }
  }

  for _,side in pairs(sides) do
    side.minX = side.pts[1].inp.x
    side.diff = side.pts[2].inp.x - side.minX
  end

  -- Mutates the input vector to hold the result
  return function(inpVector)
    for _,side in pairs(sides) do
      local pctX = (inpVector.x - side.minX) / side.diff
      side.pt:copyFrom(side.pts[1].inp):lerp(side.pts[2].inp,pctX)
      side.inpY = side.pt.y
      side.pt:copyFrom(side.pts[1].out):lerp(side.pts[2].out,pctX)
    end
    local pctY = (inpVector.y-sides.s.inpY)/(sides.n.y-sides.s.inpY)
    return inpVector:copyFrom(sides.s.pt):lerp(sides.n.pt,pctY)
  end
end

local interp = tetragonalBilinearInterpolator{
  nw={ inp=Vector(-19,-7),  out=Vector(-150,-100) },
  ne={ inp=Vector( 10,-8),  out=Vector( 150,-100) },
  sw={ inp=Vector(-11, 4),  out=Vector(-150, 100) },
  se={ inp=Vector(  9, 7),  out=Vector( 150, 100) }
}
print(interp(Vector(-4.2, 1))) --> <-31.60 35.84>

5 Comments

And now there's a JavaScript implementation on this test page I wrote: phrogz.net/tmp/bilinear-colors.html
It's fine if it fits your needs. But please don't call it bilinear interpolation. It is not. E.g. to determine a point near the top left corner would require an extrapolation of the bottom edge. Nice sample, by the way.
@NicoSchertler I don't understand your comment, and would love more information.
Well, first point, it's not a bilinear interpolation. That's just a question of definition. Nothing to argue about. Secondly, how would you determine the value at say (-15, -6) with your scheme? You would have to find a point on the bottom line with x=-15. But there is no point on this line (which would be interpolation), but there is a point on the line's extension. Sou you have to guess how the line would continue, which is called extrapolation.
@NicoSchertler My question was what part of the 'definition' makes my solution incompatible. However, I think that I finally understand. For my problem I want to perform inverse bilinear interpolation on my inputs to find the alpha/beta values (u,v), and then use bilinear interpolation on the output values using those to come up with the desired output.
0

One way is to convert your 4 input points to 4 new points that are in a rectangle.

You can use the min and max x and y values to create a rectangle that is outside the 4 original points. These are your new input points.

inputs:

import numpy as np
input = np.array([[-1.9,-0.7],[1.0,-0.8],[0.9,0.7],[-1.1,0.4]])
output = np.array([[-150,-100],[150,-100],[150,100],[-150,100]])

New points:

import matplotlib.pyplot as plt

x = input[:,0]
y = input[:,1]
plt.scatter(x, y,label="input")

x_sorted = np.sort(x)
y_sorted = np.sort(y)

x_grid = [x_sorted[0],x_sorted[0],x_sorted[-1],x_sorted[-1]]
y_grid = [y_sorted[0],y_sorted[-1],y_sorted[-1],y_sorted[0]]
plt.scatter(x_grid, y_grid,label="new_input",marker='+')

plt.legend(loc=(1.04, 0))
plt.show() 

result: new input points

Use inverse bilinear interpolation/extrapolation to find predicted output values for your new input points.

The first step is to find the alpha (relative x) values and beta(relative y value) values for the original points:

alpha = (x-x_sorted[0])/(x_sorted[-1]-x_sorted[0])
beta = (y-y_sorted[0])/(y_sorted[-1]-y_sorted[0])

The next step is to use symbolic math or matrix inversion to solve the inverse bilinear interpolation/extrapolation.

matrix = np.zeros((4,4))

for i in range(4):
    matrix[i][0] =  alpha[i] * beta[i] - alpha[i] - beta[i] + 1
    matrix[i][1] = -alpha[i] * beta[i] + beta[i]
    matrix[i][2] =  alpha[i] * beta[i]
    matrix[i][3] = -alpha[i] * beta[i] + alpha[i]

output_grid = np.array([np.linalg.solve(matrix, output[:,0]), np.linalg.solve(matrix, output[:,1])])

When using bilinear interpolation using these new input points and output values you get the same output values on the original points.

from scipy.interpolate import RegularGridInterpolator

z1 = np.array([
    [output_grid[0][0], output_grid[0][3]],
    [output_grid[0][1], output_grid[0][2]]
]) 

z2 = np.array([
    [output_grid[1][0], output_grid[1][3]],
    [output_grid[1][1], output_grid[1][2]]
]) 

interp_func1 = RegularGridInterpolator(([y_sorted[0],y_sorted[-1]], [x_sorted[0],x_sorted[-1]]), z1)
interp_func2 = RegularGridInterpolator(([y_sorted[0],y_sorted[-1]], [x_sorted[0],x_sorted[-1]]), z2)

value = interp_func1([[input[0][1], input[0][0]]])[0]
print(value) # print x output of first original point
value = interp_func2([[input[0][1], input[0][0]]])[0]
print(value) # print y output of first original point

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.