Julia version of cv.getPerspectiveTransform?

I want to create and use a projective transform matrix between four points in one coordinate system to four points in another coordinate system. Is there a way to do this in Julia? In OpenCV I can call cv.getPerspectiveTransform and cv.perspectiveTransform to make and use the transformation.

For example:
(0, 0) → (333, 333)
(0, 100) → (345, 600)
(100, 0) → (555, 346)
(100, 100) → (610, 600)

So you can see that the shape may not be perfectly square from the four points.

It’s probably not affine, so the equation here will not work. Couldn’t find a similar one for projective transformations.

Perhaps you’ll find what you’re looking for here https://github.com/JuliaGeometry/CoordinateTransformations.jl#perspective-transformations

Can you show me how to setup the PerspectiveMap? I don’t see how to generate the matrix from the 4 points in that example …

Ok one solution is:

using OpenCV 

m = reshape(
  OpenCV.getPerspectiveTransform(
    reshape(Array{Float32}([0 0 0 100 100 0 100 100]), 1, 2 ,4), 
    reshape(Array{Float32}([333 333 345 600 345 550 550 625]), 1, 2, 4) , 
    Int32(0)), 
  3, 3)'

But I would prefer to not depend on OpenCV if possible. Also the matrix that OpenCV returns makes no sense to me - when I use it by taking m * [1, 1, 1] i get something outside the range of the coordinates in the new space. If I put in m * [100, 100, 1] I get something with the wrong signs altogether (the y coordinate is negative somehow).

Another option is to solve the system of equations myself and use the resulting formula … this is the 12 equations I’d need to reduce:

Does Julia have some way of solving these large sets of symbolic equations?

The resulting symbolic formula could be nasty — it might be more efficient simply to construct the matrix and solve it numerically each time you change the parameters.

Yeah I tried solving the full thing in a wolfram sandbox and the solver timed out.

Solving for converting from an arbitrary coordinate system to a square with side 100 (to simplify some of the equations) gave me this:


Yeah … no hope for implementing that in code.

This is for converting from a square with side = 100:

Somehow this “side” isn’t so bad. Messy to implement directly though.

@stevengl is right - I will need to do this numerically when necessary.

In my application I may need to make and use ~10-20 perspective transforms at 20fps or so. Is there any approach that will be efficient enough to support this? I imagine trying to run rref on that many matrices will be quite slow…

So this is what making it myself looks like:

using RowEchelon

function perspective_transform(
  ((x1, y1), (x2, y2), (x3, y3), (x4, y4)),  
  ((X1, Y1), (X2, Y2), (X3, Y3), (X4, Y4)))  

  m = [
    x1 y1 1 0 0 0 0 0   -X1 0 0 0 0
    x2 y2 1 0 0 0 0 0   0 -X2 0 0 0
    x3 y3 1 0 0 0 0 0   0 0 -X3 0 0
    x4 y4 1 0 0 0 0 0   0 0 0 -X4 0
    0 0 0 x1 y1 1 0 0   -Y1 0 0 0 0
    0 0 0 x2 y2 1 0 0   0 -Y2 0 0 0
    0 0 0 x3 y3 1 0 0   0 0 -Y3 0 0
    0 0 0 x4 y4 1 0 0   0 0 0 -Y4 0
    0 0 0 0 0 0 x1 y1    -1 0 0 0 -1
    0 0 0 0 0 0 x2 y2    0 -1 0 0 -1
    0 0 0 0 0 0 x3 y3    0 0 -1 0 -1
    0 0 0 0 0 0 x4 y4    0 0 0 -1 -1
  ]
  values = rref(m)[1:8, end]
  [
    values[1] values[2] values[3]
    values[4] values[5] values[6]
    values[7] values[8] 1
  ]
end

Is this the fastest way to set this up? that matrix is 75% zeros so maybe there’s a faster/cleaner way to do this? I’ve also heard that rref isn’t the most numerically stable, so if there’s a better option please tell me.

As for using it I get

function transform_perspective(m::Matrix, (x1, x2)) 
  t = m * [x1 x2 1]'
  [t[1] / t[3], t[2] / t[3]]
end

Of course both of these functions are specialized to the 2d case, but that’s all I need for now.

If you preallocate all the matrices such that you do not allocate new ones for each transform application there is a good chance you’ll make 20fps. For example, this

m = [
    x1 y1 ... 

allocates new memory, consider allocating that once outside the function (remember to use const) and just update the already allocated memory. Then make use of in-place multiplications using mul!.

Do you know about https://github.com/JuliaImages/ImageTransformations.jl? It might have everything you need. And checkout this answer from Tim Holy: Perspective warp an image in Julia - Stack Overflow

Hope this helps!

Seems like this resource allows someone to apply a transformation matrix (I don’t see a way to solve for one). So I can write the application function like:

using ImageTransformations

transform_perspective(m::Matrix, point::Vector) = PerspectiveMap() ∘ inv(LinearMap(m)) ∘ \x -> push(x, 1)

I’m not clear what I’m gaining compared to the simpler code above. Isn’t this a lot more operations to create all the transformation objects?

Yes, ImageTransformations.jl currently just allows you to apply transformations, not fit them - this would be a nice addition to the package. Your function can be sped up with static arrays (and by rewriting the rref to use \ to solve the linear equation):

using StaticArrays

function perspective_transform_SA(
         ((x1, y1), (x2, y2), (x3, y3), (x4, y4)),
         ((X1, Y1), (X2, Y2), (X3, Y3), (X4, Y4)))

    m = SA_F64[
        x1 y1 1 0 0 0 0 0   -X1 0 0 0
        x2 y2 1 0 0 0 0 0   0 -X2 0 0
        x3 y3 1 0 0 0 0 0   0 0 -X3 0
        x4 y4 1 0 0 0 0 0   0 0 0 -X4
        0 0 0 x1 y1 1 0 0   -Y1 0 0 0
        0 0 0 x2 y2 1 0 0   0 -Y2 0 0
        0 0 0 x3 y3 1 0 0   0 0 -Y3 0
        0 0 0 x4 y4 1 0 0   0 0 0 -Y4
        0 0 0 0 0 0 x1 y1    -1 0 0 0 
        0 0 0 0 0 0 x2 y2    0 -1 0 0 
        0 0 0 0 0 0 x3 y3    0 0 -1 0 
        0 0 0 0 0 0 x4 y4    0 0 0 -1 
    ]
    v = SA_F64[0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1]
    values = m \ v
    SA[
    values[1] values[2] values[3]
    values[4] values[5] values[6]
    values[7] values[8] 1
    ]
end
julia> @btime perspective_transform(((0, 0), (0, 100), (100, 0), (100, 100)),
                             ((333, 333), (345,600), (555, 346), (610, 600)))
  3.288 μs (59 allocations: 7.95 KiB)
3×3 Matrix{Float64}:
 2.50406      -0.476459    333.0
 0.307087      1.63268     333.0
 0.000511811  -0.00172887    1.0

julia> @btime perspective_transform_SA(((0, 0), (0, 100), (100, 0), (100, 100)),
                             ((333, 333), (345,600), (555, 346), (610, 600)))
  718.519 ns (0 allocations: 0 bytes)
3×3 SMatrix{3, 3, Float64, 9} with indices SOneTo(3)×SOneTo(3):
 2.50406      -0.476459    333.0
 0.307087      1.63268     333.0
 0.000511811  -0.00172887    1.0

At 718 ns per transform, that’s 1.4 million transforms per second. If you only need 400 transforms per second, this won’t be your limiting factor.

Okay, that definitely solves the problem! Thank you.