20

I want to calculate perspective transform (a matrix for warpPerspective function) starting from angles of rotation and distance to the object.

How to do that?

I found the code somewhere on OE. Sample program is below:

#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

Mat frame;

int alpha_int;
int dist_int;
int f_int;

double w;
double h; 
double alpha; 
double dist; 
double f;

void redraw() {

    alpha = (double)alpha_int/1000.;
    //dist = 1./(dist_int+1);
    //dist = dist_int+1;
    dist = dist_int-50;
    f = f_int+1;

    cout << "alpha = " << alpha << endl;
    cout << "dist = " << dist << endl;
    cout << "f = " << f << endl;

    // Projection 2D -> 3D matrix
    Mat A1 = (Mat_<double>(4,3) <<
        1,              0,              -w/2,
        0,              1,              -h/2,
        0,              0,              1,
        0,              0,              1);

    // Rotation matrices around the X axis
    Mat R = (Mat_<double>(4, 4) <<
        1,              0,              0,              0,
        0,              cos(alpha),     -sin(alpha),    0,
        0,              sin(alpha),     cos(alpha),     0,
        0,              0,              0,              1);

    // Translation matrix on the Z axis 
    Mat T = (Mat_<double>(4, 4) <<
        1,              0,              0,              0,
        0,              1,              0,              0,
        0,              0,              1,              dist,
        0,              0,              0,              1);

    // Camera Intrisecs matrix 3D -> 2D
    Mat A2 = (Mat_<double>(3,4) <<
        f,              0,              w/2,            0,
        0,              f,              h/2,            0,
        0,              0,              1,              0);

    Mat m = A2 * (T * (R * A1));

    cout << "R=" << endl << R << endl;
    cout << "A1=" << endl << A1 << endl;
    cout << "R*A1=" << endl << (R*A1) << endl;
    cout << "T=" << endl << T << endl;
    cout << "T * (R * A1)=" << endl << (T * (R * A1)) << endl;
    cout << "A2=" << endl << A2 << endl;
    cout << "A2 * (T * (R * A1))=" << endl << (A2 * (T * (R * A1))) << endl;
    cout << "m=" << endl << m << endl;

    Mat frame1;


    warpPerspective( frame, frame1, m, frame.size(), INTER_CUBIC | WARP_INVERSE_MAP);

    imshow("Frame", frame);
    imshow("Frame1", frame1);
}

void callback(int, void* ) {
    redraw();
}

void main() {


    frame = imread("FruitSample_small.png", CV_LOAD_IMAGE_COLOR);
    imshow("Frame", frame);

    w = frame.size().width;
    h = frame.size().height; 

    createTrackbar("alpha", "Frame", &alpha_int, 100, &callback);
    dist_int = 50;
    createTrackbar("dist", "Frame", &dist_int, 100, &callback);
    createTrackbar("f", "Frame", &f_int, 100, &callback);

    redraw();

    waitKey(-1);
}

But unfortunately, this transform does something strange

enter image description here

Why? What is another half of image above when alpha>0? And how to rotate around other axes? Why dist works so strange?

Suzan Cioc
  • 26,725
  • 49
  • 190
  • 355
  • it looks like your width and height values are single pixels. – Geoff Jun 13 '13 at 13:37
  • setting hundreds change nothing – Suzan Cioc Jun 13 '13 at 14:06
  • To know why this code works the way it works take a look at the equations in the following paper http://www.eee.nuigalway.ie/Research/car/documents/docualain_issc10.pdf . I though still feel this code is bit weird. I would change `alpha = (double)alpha_int/1000.;` to `alpha = (double)alpha_int*CV_PI/180.;` and also I not sure how A1 matrix is calculated. I would set A1 as `Mat A1 = (Mat_(4,3) << 1, 0, -w/2, 0, 0, 0, //Y-axis zero 0, 1, -h/2, 0, 0, 1);` – enthusiasticgeek Jul 21 '13 at 22:26
  • 1
    And of course you have rotation around x-axis [i.e. pitch or tilt] (take a look at the diagram in the pdf listed above). If your camera is rotated with roll 'gamma' you will have to multiply by another 4x4 matrix with gamma parameter. – enthusiasticgeek Jul 21 '13 at 22:32
  • I would remove dist_int track bar and set dist = -height/sin(alpha) (make sure alpha>0). height is the height where camera is placed from the ground. – enthusiasticgeek Jul 21 '13 at 22:48

3 Answers3

51

I have had the luxury of time to think out both math and code. I did this a year or two ago. I even typeset this in beautiful LaTeX.

I intentionally designed my solution so that no matter what rotation angles are provided, the entire input image is contained, centered, within the output frame, which is otherwise black.

The arguments to my warpImage function are rotation angles in all 3 axes, the scale factor and the vertical field-of-view angle. The function outputs the warp matrix, the output image and the corners of the source image within the output image.

The Math (for code, look below)

Page 1 enter image description here

The LaTeX source code is here.

The Code (for math, look above)

Here is a test application that warps the camera

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <math.h>


using namespace cv;
using namespace std;


static double rad2Deg(double rad){return rad*(180/M_PI);}//Convert radians to degrees
static double deg2Rad(double deg){return deg*(M_PI/180);}//Convert degrees to radians




void warpMatrix(Size   sz,
                double theta,
                double phi,
                double gamma,
                double scale,
                double fovy,
                Mat&   M,
                vector<Point2f>* corners){
    double st=sin(deg2Rad(theta));
    double ct=cos(deg2Rad(theta));
    double sp=sin(deg2Rad(phi));
    double cp=cos(deg2Rad(phi));
    double sg=sin(deg2Rad(gamma));
    double cg=cos(deg2Rad(gamma));

    double halfFovy=fovy*0.5;
    double d=hypot(sz.width,sz.height);
    double sideLength=scale*d/cos(deg2Rad(halfFovy));
    double h=d/(2.0*sin(deg2Rad(halfFovy)));
    double n=h-(d/2.0);
    double f=h+(d/2.0);

    Mat F=Mat(4,4,CV_64FC1);//Allocate 4x4 transformation matrix F
    Mat Rtheta=Mat::eye(4,4,CV_64FC1);//Allocate 4x4 rotation matrix around Z-axis by theta degrees
    Mat Rphi=Mat::eye(4,4,CV_64FC1);//Allocate 4x4 rotation matrix around X-axis by phi degrees
    Mat Rgamma=Mat::eye(4,4,CV_64FC1);//Allocate 4x4 rotation matrix around Y-axis by gamma degrees

    Mat T=Mat::eye(4,4,CV_64FC1);//Allocate 4x4 translation matrix along Z-axis by -h units
    Mat P=Mat::zeros(4,4,CV_64FC1);//Allocate 4x4 projection matrix

    //Rtheta
    Rtheta.at<double>(0,0)=Rtheta.at<double>(1,1)=ct;
    Rtheta.at<double>(0,1)=-st;Rtheta.at<double>(1,0)=st;
    //Rphi
    Rphi.at<double>(1,1)=Rphi.at<double>(2,2)=cp;
    Rphi.at<double>(1,2)=-sp;Rphi.at<double>(2,1)=sp;
    //Rgamma
    Rgamma.at<double>(0,0)=Rgamma.at<double>(2,2)=cg;
    Rgamma.at<double>(0,2)=-sg;Rgamma.at<double>(2,0)=sg;

    //T
    T.at<double>(2,3)=-h;
    //P
    P.at<double>(0,0)=P.at<double>(1,1)=1.0/tan(deg2Rad(halfFovy));
    P.at<double>(2,2)=-(f+n)/(f-n);
    P.at<double>(2,3)=-(2.0*f*n)/(f-n);
    P.at<double>(3,2)=-1.0;
    //Compose transformations
    F=P*T*Rphi*Rtheta*Rgamma;//Matrix-multiply to produce master matrix

    //Transform 4x4 points
    double ptsIn [4*3];
    double ptsOut[4*3];
    double halfW=sz.width/2, halfH=sz.height/2;

    ptsIn[0]=-halfW;ptsIn[ 1]= halfH;
    ptsIn[3]= halfW;ptsIn[ 4]= halfH;
    ptsIn[6]= halfW;ptsIn[ 7]=-halfH;
    ptsIn[9]=-halfW;ptsIn[10]=-halfH;
    ptsIn[2]=ptsIn[5]=ptsIn[8]=ptsIn[11]=0;//Set Z component to zero for all 4 components

    Mat ptsInMat(1,4,CV_64FC3,ptsIn);
    Mat ptsOutMat(1,4,CV_64FC3,ptsOut);

    perspectiveTransform(ptsInMat,ptsOutMat,F);//Transform points

    //Get 3x3 transform and warp image
    Point2f ptsInPt2f[4];
    Point2f ptsOutPt2f[4];

    for(int i=0;i<4;i++){
        Point2f ptIn (ptsIn [i*3+0], ptsIn [i*3+1]);
        Point2f ptOut(ptsOut[i*3+0], ptsOut[i*3+1]);
        ptsInPt2f[i]  = ptIn+Point2f(halfW,halfH);
        ptsOutPt2f[i] = (ptOut+Point2f(1,1))*(sideLength*0.5);
    }

    M=getPerspectiveTransform(ptsInPt2f,ptsOutPt2f);

    //Load corners vector
    if(corners){
        corners->clear();
        corners->push_back(ptsOutPt2f[0]);//Push Top Left corner
        corners->push_back(ptsOutPt2f[1]);//Push Top Right corner
        corners->push_back(ptsOutPt2f[2]);//Push Bottom Right corner
        corners->push_back(ptsOutPt2f[3]);//Push Bottom Left corner
    }
}

void warpImage(const Mat &src,
               double    theta,
               double    phi,
               double    gamma,
               double    scale,
               double    fovy,
               Mat&      dst,
               Mat&      M,
               vector<Point2f> &corners){
    double halfFovy=fovy*0.5;
    double d=hypot(src.cols,src.rows);
    double sideLength=scale*d/cos(deg2Rad(halfFovy));

    warpMatrix(src.size(),theta,phi,gamma, scale,fovy,M,&corners);//Compute warp matrix
    warpPerspective(src,dst,M,Size(sideLength,sideLength));//Do actual image warp
}


int main(void){
    int c = 0;
    Mat m, disp, warp;
    vector<Point2f> corners;
    VideoCapture cap(0);

    while(c != 033 && cap.isOpened()){
        cap >> m;
        warpImage(m, 5, 50, 0, 1, 30, disp, warp, corners);
        imshow("Disp", disp);
        c = waitKey(1);
    }
}
Iwillnotexist Idonotexist
  • 12,747
  • 3
  • 39
  • 62
  • 1
    As a small note, although my derivations only include two rotation matrices (Rtheta and Rphi, the Z and X-axis rotation matrices) and therefore only two rotation axes, the code does contain 3 rotation matrices (Rtheta, Rphi and Rgamma), covering Z, X and Y rotations. – Iwillnotexist Idonotexist Nov 23 '13 at 05:42
  • Your math produces a bigger resolution than the original image which is expected for the worse case scenario yaw. Although if I discarded the yaw transformation I would expect the resolution to not be changed, however with zero yaw and normal resolution i get a clipped image. Can you explain me why? – Paulo Neves Jul 23 '14 at 17:31
  • Are you sure it's _clipped_? The way I calculated the distance, with no rotation the output image has precisely the same resolution as the input image (as though it had been copied), except that it is centered within a large black padding. The output therefore has more _pixels_, without having more (or less) _resolution_. Shrinking it to make it fit within the same size as the input image will of course make it look shrunk. If you want to crop, you can use the corner points that I compute to calculate the tightest bounding box. – Iwillnotexist Idonotexist Jul 23 '14 at 17:48
  • @aiwarrior The code right now is designed s.t. the image is being rotated at a fixed position in front of you, and that the center of the image is always at the same offset (x,y) in the center of the output image for a given scale, which implies that for a given scale, the output image is always the same size, and for scale = 1.0, angles 0 the input image is reproduced pixel-for-pixel within the larger output image. I take it you instead want the smallest image fully containing the warped input? – Iwillnotexist Idonotexist Jul 24 '14 at 00:47
  • I solved the problem already. Removing the yaw rotation i removed the hypotenuse of the sides. Then in the ptsOutPt2f instead of adding Point(1,1) i add a scaling factor given by sz.width/sideLength. Your paper is really good by the way. Can I extend your work in my thesis based on the explanation given by your paper? Great work by the way. – Paulo Neves Jul 28 '14 at 19:21
  • @aiwarrior You may; I can also provide the LaTeX/TikZ code for it, if you want it. In fact, you would not be the first to use my work in a thesis! – Iwillnotexist Idonotexist Jul 28 '14 at 20:41
  • Thanks a lot this way I can easily make the relevant changes. How should I cite you? – Paulo Neves Jul 29 '14 at 01:40
  • 1
    Thank you a lot. One small question though. Why translate z by the hypotenuse and not by d/(2 * tg(fov/2)) – Paulo Neves Aug 02 '14 at 21:41
  • The reason for that is that merely doing d / 2tan(fovy/2) does fully account for the worst-case in-plane (theta) rotation, but as soon as you tilt this pathological case forward, the top corner will be clipped off. Try this yourself: With your head level, hold a ruler in your hand vertically, with its bottom in front of your eyes, and bring it forwards towards yourself while staring up at the top of the ruler. Stop when you can no longer bring it any closer without losing sight of the ruler's top. Now start tilting the top of the ruler towards your head; You'll lose sight of the ruler's top. – Iwillnotexist Idonotexist Aug 03 '14 at 03:21
  • @IwillnotexistIdonotexist If I am right, the rotation matrix Rgamma has a typo in it ```Rgamma.at(2,0)=sg;``` should be ```-sg``` instead – hAcKnRoCk Mar 07 '18 at 10:39
  • @hAcKnRoCk I suspect you are right, I wonder how this code ever worked! – Iwillnotexist Idonotexist Mar 07 '18 at 11:37
  • 1
    @hAcKnRoCk I fixed it, although if I eyeball every gamma angle with both the fixed and unfixed version I see no difference even though I should have; It's very wierd. I wasn't expecting this. – Iwillnotexist Idonotexist Mar 07 '18 at 12:17
  • 2
    @IwillnotexistIdonotexist Thanks for the quick response. I am trying to implement the same in python. And I caught this typo. Apart from this, I understand that `ptsIn` are the coordinates of the object with reference frame origin centered at (0,0,0). However, I did not quite get what are ```ptsInPt2f``` and ```ptOutPt2f```. Could you help me with this please? – hAcKnRoCk Mar 07 '18 at 12:23
  • 2
    @hAcKnRoCk Cool! About `ptsInPt2f` and `ptsOutPt2f`: The `ptsInPt2f` are the four 2D coordinates of the corners in the original picture `(TL, TR, BR, BL)`, and are a rectangle that is the same as the size of the original picture. The `ptsOutPt2f` are the four 2D coordinates of the respective corners of the warped target in the output image. They're used to calculate the output perspective transform `M` (and input to `warpPerspective()`) to warp the source image. – Iwillnotexist Idonotexist Mar 07 '18 at 12:50
  • 5
    @IwillnotexistIdonotexist Thanks again. Here is the version I coded up in python https://nbviewer.jupyter.org/github/manisoftwartist/perspectiveproj/blob/master/perspective.ipynb I would love to know what you think about it. Secondly, I am wondering how to incorporate the camera intrinsics/extrinsics matrices to simulate a mobile phone camera looking at a scene (the source image in this case). – hAcKnRoCk Mar 07 '18 at 21:00
  • 1
    @hAcKnRoCk Hmm it follows my C++ code extremely "literally", but otherwise I trust it works. For instance I'd personally use `F = P.dot(T).dot(R)`. About the camera intrinsic matrix: That one will _pre_-multiply the `F` matrix. As you go right to left from input coordinates to their projection, you rotate, translate (extrinsic matrix), then project (perfect pinhole camera model). A further matrix left-multiplication by custom parameters can implement the camera's intrinsic distortions. – Iwillnotexist Idonotexist Mar 08 '18 at 03:54
  • 1
    @IwillnotexistIdonotexist Thanks for your very useful comments. Well, "literally" yes - to check correctness and allow minimum variations from the original and No, because it need not be the same of course. I replaced the way you construct the rotation matrix with rodrigues' formula which looks more elegant IMO. – hAcKnRoCk Mar 08 '18 at 13:22
  • @IwillnotexistIdonotexist Hi, for a zero rotations about all three axes and a fov of 55 degrees, I do not get an output image which is the same size as the input. What mapping does ```ptsOutPt2f[i] = (ptOut+Point2f(1,1))*(sideLength*0.5);``` represent exactly? – hAcKnRoCk Mar 14 '18 at 17:05
  • @hAcKnRoCk If you look at my technical drawing, the square frame within which the rotated image is embedded is strictly bigger than the rotated image itself. Its `sideLength` is calculated to satisfy two criteria: 1) At (0,0,0) rotation angles, the size _in pixels_ of the _embedded_ input image _within the output image_ is the same as the input image. 2) When any corner of the input is rotated by an angle theta into a position directly above the center, and then panned by an angle fovy/2 towards the camera, that corner just barely touches the border of the output image. – Iwillnotexist Idonotexist Mar 14 '18 at 17:16
  • @IwillnotexistIdonotexist I observed that the input image is simply embedded at the centre of the output (of size squarelength) when the rotations are 0. Thanks. would appreciate greatly if you could help me with what this transformation ```ptsOutPt2f[i] = (ptOut+Point2f(1,1))*(sideLength*0.5)``` represents?..why translate by 1,1 and whats the point of scaling by half thesidelength? – hAcKnRoCk Mar 14 '18 at 18:14
  • 1
    @hAcKnRoCk It translates from the range [-1, +1] to [0, 2] to [0, sideLength], because the coordinates of a pixel are typically positive and integer. – Iwillnotexist Idonotexist Mar 14 '18 at 18:19
  • @IwillnotexistIdonotexist Dropbox is giving me a 404 error when I try to access your Latex documentation. Could you please send it to me? – A. Hendry May 14 '18 at 15:34
  • @A.Hendry Dropbox apparently reorganized things and broke the old link. Does [the new one](https://www.dropbox.com/s/72wngnybqtfs2ib/TaylorMath.zip?dl=0) work? – Iwillnotexist Idonotexist May 14 '18 at 16:11
  • Yes this works! Thank you very much! I too would like to use this code and cite you in my project. How should I cite you? – A. Hendry May 14 '18 at 18:39
  • Also, I have a question. I'm simply taking images and rotating them about `x` by `phi`. The bottom of my original image appears in about the middle of the new window for the rotated image. How do I get the coordinates of the bottom of the new rotated image? – A. Hendry May 14 '18 at 18:41
  • @IwillnotexistIdonotexist What does the shell command `echo -e "T2xleGEgQmlsYW5pdWsgKFVuaXZlcnNpdHkgb2YgT3R0YXdhIC0gVklWQSBMYWIp\nCg==" | openssl enc -a -d` do? And yes, I'm looking for the `Rect`. – A. Hendry May 14 '18 at 18:51
  • @IwillnotexistIdonotexist If I can send you a picture, I'll show you what I'm after. The bottom two coordinates of the transform are actually off the image. The top two are on the image though. The bottom edge of the image after the transform is unchanged (the new image looks as if I tilted it in the depth direction about the bottom edge) – A. Hendry May 14 '18 at 18:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/171027/discussion-between-iwillnotexist-idonotexist-and-a-hendry). – Iwillnotexist Idonotexist May 14 '18 at 18:59
  • @IwillnotexistIdonotexist Thank you for your help yesterday. By chance, is there a way I can shift the bottom of the image to the x-axis so that I'm always rotating about the bottom of the image? (I'd like the bottom edge to remain unchanged and the rest to transform) – A. Hendry May 15 '18 at 15:07
  • I was able to figure this out. i just had to complement my transforms by first translating the image up by h/2, then rotate, then translate down by h/2 and perform the remaining transforms. – A. Hendry May 15 '18 at 16:55
  • @A.Hendry If I understood you right, that will take a substantial rederivation of my math, and a restriction to only the phi angle (pitch forward-backward). A lot of my math was concerned specifically with the geometric constraint that the _center pixel_ stays in the center, but all of the image must be seen under worst-case all-angle rotation. The math’s a bit different for your conditions. I figure worst-case tilt is FOVY/2 (for image height) and 90deg (for image width). – Iwillnotexist Idonotexist May 15 '18 at 16:56
  • HI @PauloNeves. Would you be able please to show the code that you have changed for the output size calculation? What exactly do you mean with 'Removing the yaw rotation'? Many thanks. – Avrohom May 24 '19 at 14:37
  • @IwillnotexistIdonotexist: Hi, Fantastic post! Is there anyway how to get the transformation output size to just about surrounds the transformed image? ie, calculate the output size so that the output does not have that extra black space around? – Avrohom May 26 '19 at 14:43
  • @Avrohom You can compute the bounding box of the returned points and crop the image to those bounds, or you could fiddle with the `T` matrix to bring the transformed image apparently closer. – Iwillnotexist Idonotexist May 28 '19 at 08:45
  • Very nice post @IwillnotexistIdonotexist! I'm still trying to make sure I understand the exact representations of everything. One question that I'm already asking myself, Is why we have n&f to begin with here? I know these are a convention for creating depth buffers in CG but can't we do with a purer version 3x4 projection here (dropping 3rd row of the matrix) if we don't use the z values afterwards? – Bambi Jan 12 '20 at 15:20
  • @Bambi I think you may be right, and the matrix could be made 3x4 the way you propose. Once the point is projected, z is ignored. – Iwillnotexist Idonotexist Jan 12 '20 at 23:46
  • I think that the math and code can be simplified a little because `f - n` is equal to `d` – Leland Hepworth Apr 16 '21 at 22:01
  • Thanks for the great document. I have two cameras with 30 degrees angle and 5 cm distance between them. I can rotate them +- 15 degrees without problem but I couldn't understand the unit of the translation matrix. How can I transform my image in metric units? – enesdemirag May 24 '21 at 13:26
1

I have gotten the similar problem. It problem turns out to be that after backprojection into 3D coordinates, since we don't have the intrinsic parameters of the camera, we use some guess value or even 1 as in your matrix A1. During Rotation this could result in the image plane on the "wrong side" of the image plane, having negative depth value (z < 0) . After projection, when you divide your coordinates with z you get something weird like what you have shown.

So the solution turned out to be scale the focallength to be something relative to your image size.

say your image has dimension (H, W), set focallength to be for example H/3. In this case your A1 will be

[H/3, 0, W/2]
[0, H/3, H/2]
[0,   0,   1]
yfi
  • 137
  • 12
0

Here's my Java conversion of the popular C++ example code answer. I have no way of knowing the veracity of this Java code, the original C++ code or the underlying equations, nor what the several comments about tweaking the equations mean other than to say it seems to work. This was for experimental purposes for playing with image processing with high school students.

package app;

import java.util.Arrays;
import java.util.stream.Collectors;

import org.opencv.highgui.HighGui;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.core.Point;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint2f;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;

public class App {
    static {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // Load the native library.
    }

    static void warpMatrix(Size   sz,
                    double theta,
                    double phi,
                    double gamma,
                    double scale,
                    double fovy,
                    Mat   M,
                    MatOfPoint2f corners) {

        double st=Math.sin(Math.toRadians(theta));
        double ct=Math.cos(Math.toRadians(theta));
        double sp=Math.sin(Math.toRadians(phi));
        double cp=Math.cos(Math.toRadians(phi));
        double sg=Math.sin(Math.toRadians(gamma));
        double cg=Math.cos(Math.toRadians(gamma));

        double halfFovy=fovy*0.5;
        double d=Math.hypot(sz.width,sz.height);
        double sideLength=scale*d/Math.cos(Math.toRadians(halfFovy));
        double h=d/(2.0*Math.sin(Math.toRadians(halfFovy)));
        double n=h-(d/2.0);
        double f=h+(d/2.0);

        Mat F=new Mat(4,4, CvType.CV_64FC1);//Allocate 4x4 transformation matrix F
        Mat Rtheta=Mat.eye(4,4, CvType.CV_64FC1);//Allocate 4x4 rotation matrix around Z-axis by theta degrees
        Mat Rphi=Mat.eye(4,4, CvType.CV_64FC1);//Allocate 4x4 rotation matrix around X-axis by phi degrees
        Mat Rgamma=Mat.eye(4,4, CvType.CV_64FC1);//Allocate 4x4 rotation matrix around Y-axis by gamma degrees

        Mat T=Mat.eye(4,4, CvType.CV_64FC1);//Allocate 4x4 translation matrix along Z-axis by -h units
        Mat P=Mat.zeros(4,4, CvType.CV_64FC1);//Allocate 4x4 projection matrix
                                                // zeros instead of eye as in github manisoftwartist/perspectiveproj

        //Rtheta Z
        Rtheta.put(0,0, ct);
        Rtheta.put(1,1, ct);
        Rtheta.put(0,1, -st);
        Rtheta.put(1,0, st);
        //Rphi X
        Rphi.put(1,1, cp);
        Rphi.put(2,2, cp);
        Rphi.put(1,2, -sp);
        Rphi.put(2,1, sp);
        //Rgamma Y
        Rgamma.put(0,0, cg);
        Rgamma.put(2,2, cg);
        Rgamma.put(0,2, -sg); // sign reversed? Math different convention than computer graphics according to Wikipedia
        Rgamma.put(2,0, sg);
        //T
        T.put(2,3, -h);
        //P Perspective Matrix (see also in computer vision a camera matrix or (camera) projection matrix is a 3x4 matrix which describes the mapping of a pinhole camera from 3D points in the world to 2D points in an image.)
        P.put(0,0, 1.0/Math.tan(Math.toRadians(halfFovy)));
        P.put(1,1, 1.0/Math.tan(Math.toRadians(halfFovy)));
        P.put(2,2, -(f+n)/(f-n));
        P.put(2,3, -(2.0*f*n)/(f-n));
        P.put(3,2, -1.0);
        System.out.println("P " + P.dump());
        System.out.println("T " + T.dump());
        System.out.println("Rphi " + Rphi.dump());
        System.out.println("Rtheta " + Rtheta.dump());
        System.out.println("Rgamma " + Rgamma.dump());
        //Compose transformations
        //F=P*T*Rphi*Rtheta*Rgamma;//Matrix-multiply to produce master matrix
        //gemm(Mat src1, Mat src2, double alpha, Mat src3, double beta, Mat dst)
        //dst = alpha*src1.t()*src2 + beta*src3.t(); // w or w/o the .t() transpose
        // D=α∗AB+β∗C
        Mat F1 = new Mat();
        Mat F2 = new Mat();
        Mat F3 = new Mat();
        Core.gemm(P, T, 1, new Mat(), 0, F1);
        Core.gemm(F1, Rphi, 1, new Mat(), 0, F2);
        Core.gemm(F2, Rtheta, 1, new Mat(), 0, F3);
        Core.gemm(F3, Rgamma, 1, new Mat(), 0, F);
        P.release();
        T.release();
        Rphi.release();
        Rtheta.release();
        Rgamma.release();
        F1.release();
        F2.release();
        F3.release();

        //Transform 4x4 points
        double[] ptsIn = new double[4*3];
        double[] ptsOut = new double[4*3];
        double halfW=sz.width/2, halfH=sz.height/2;

        ptsIn[0]=-halfW;ptsIn[ 1]= halfH;
        ptsIn[3]= halfW;ptsIn[ 4]= halfH;
        ptsIn[6]= halfW;ptsIn[ 7]=-halfH;
        ptsIn[9]=-halfW;ptsIn[10]=-halfH;
        ptsIn[2]=ptsIn[5]=ptsIn[8]=ptsIn[11]=0;//Set Z component to zero for all 4 components

        Mat ptsInMat = new Mat(1,4,CvType.CV_64FC3);
        ptsInMat.put(0,0, ptsIn);

        Mat ptsOutMat = new Mat(1,4,CvType.CV_64FC3);

        System.out.println("ptsInMat " + ptsInMat + " " + ptsInMat.dump());
        System.out.println("F " + F + " " + F.dump());
        Core.perspectiveTransform(ptsInMat, ptsOutMat, F);//Transform points
        System.out.println("ptsOutMat " + ptsOutMat + " " + ptsOutMat.dump());
        ptsInMat.release();
        F.release();
        ptsOutMat.get(0, 0, ptsOut);
        ptsOutMat.release();
        System.out.println(toString(ptsOut));
        System.out.println(halfW + " " + halfH);

        //Get 3x3 transform and warp image
        Point[] ptsInPt2f = new Point[4];
        Point[] ptsOutPt2f = new Point[4];
        for(int i=0;i<4;i++){
            ptsInPt2f[i] = new Point(0, 0);
            ptsOutPt2f[i] = new Point(0, 0);
            System.out.println(i);
            System.out.println("points " + ptsIn [i*3+0] + " " + ptsIn [i*3+1]);
            Point ptIn = new Point(ptsIn [i*3+0], ptsIn [i*3+1]);
            Point ptOut = new Point(ptsOut[i*3+0], ptsOut[i*3+1]);

            ptsInPt2f[i].x  = ptIn.x+halfW;
            ptsInPt2f[i].y  = ptIn.y+halfH;

            ptsOutPt2f[i].x = (ptOut.x+1) * sideLength*0.5;
            ptsOutPt2f[i].y = (ptOut.y+1) * sideLength*0.5;
           System.out.println("ptsOutPt2f " + ptsOutPt2f[i]);
        }  

        Mat ptsInPt2fTemp =  Mat.zeros(4,1,CvType.CV_32FC2);
        ptsInPt2fTemp.put(0, 0,
            ptsInPt2f[0].x,ptsInPt2f[0].y,
            ptsInPt2f[1].x,ptsInPt2f[1].y,
            ptsInPt2f[2].x,ptsInPt2f[2].y,
            ptsInPt2f[3].x,ptsInPt2f[3].y);

        Mat ptsOutPt2fTemp = Mat.zeros(4,1,CvType.CV_32FC2);
        ptsOutPt2fTemp.put(0, 0,
            ptsOutPt2f[0].x,ptsOutPt2f[0].y,
            ptsOutPt2f[1].x,ptsOutPt2f[1].y,
            ptsOutPt2f[2].x,ptsOutPt2f[2].y,
            ptsOutPt2f[3].x,ptsOutPt2f[3].y);

        System.out.println("ptsInPt2fTemp " + ptsInPt2fTemp.dump());
        System.out.println("ptsOutPt2fTemp " + ptsOutPt2fTemp.dump());
        Mat warp=Imgproc.getPerspectiveTransform(ptsInPt2fTemp, ptsOutPt2fTemp);
        warp.copyTo(M);
        ptsInPt2fTemp.release();
        warp.release();

        //Load corners vector
        if(corners != null)
        {
            corners.put(0,0, ptsOutPt2f[0].x, ptsOutPt2f[0].y//Push Top Left corner
            , ptsOutPt2f[1].x, ptsOutPt2f[1].y//Push Top Right corner
            , ptsOutPt2f[2].x, ptsOutPt2f[2].y//Push Bottom Right corner
            , ptsOutPt2f[3].x, ptsOutPt2f[3].y);//Push Bottom Left corner
        }
        ptsOutPt2fTemp.release();
        System.out.println("corners " + corners + " " + corners.dump());
    }

    static void warpImage(Mat src,
                   double    theta,
                   double    phi,
                   double    gamma,
                   double    scale,
                   double    fovy,
                   Mat      dst,
                   Mat      M,
                   MatOfPoint2f corners){
        double halfFovy=fovy*0.5;
        double d=Math.hypot(src.cols(),src.rows());
        double sideLength=scale*d/Math.cos(Math.toRadians(halfFovy));
        System.out.println("d " + d + ", sideLength " + sideLength);
        warpMatrix(src.size(), theta, phi, gamma, scale, fovy, M, corners);//Compute warp matrix
        System.out.println("M " + M + " " + M.dump());
        Imgproc.warpPerspective(src, dst, M, new Size(sideLength,sideLength));//Do actual image warp
    }

    public static void main(String[] args)
    {
        int c = 0;
        Mat m = new Mat();
        Mat disp = new Mat();
        Mat warp = new Mat();
        MatOfPoint2f corners = new MatOfPoint2f(new Point(0,0),new Point(0,0),new Point(0,0),new Point(0,0));

        String filename = "lena.jpg";
        m = Imgcodecs.imread(filename, Imgcodecs.IMREAD_COLOR);
        if (m.empty()) {
            System.out.println("Error opening image");
            System.exit(-1);
        }

        double scale = 1.;
        double fovy = 53.;
        double halfFovy=fovy*0.5;

        VideoCapture cap;
        cap = new VideoCapture();
        cap.open(0);
        cap.read(m);
        warpImage(m, 5, 50, 0, 1, 30, disp, warp, corners); // fovy = rad2deg(arctan2(640,480)) = 53 ??

        while(true) {
            cap.read(m);
            double d=Math.hypot(m.cols(),m.rows());
            double sideLength=scale*d/Math.cos(Math.toRadians(halfFovy));
            Imgproc.warpPerspective(m, disp, warp, new Size(sideLength,sideLength));//Do actual image warp
            HighGui.imshow("Disp", disp);
            HighGui.imshow("Orig", m);
            c = HighGui.waitKey(25);
            if (c != -1) break;
        }

        m.release();
        disp.release();
        warp.release();
        corners.release();
        System.exit(0);
    }

    static String toString(double[] array) {
        return Arrays.stream(array)
                .mapToObj(i -> String.format("%5.2f", i))
                .collect(Collectors.joining(", ", "[", "]"));
                //.collect(Collectors.joining("|", "|", "|"));
            }
}