# Augmented Reality with OpenCV and OpenGL: the tricky projection matrix

I’ve been working lately on computer vision projects, involving Tensorflow for deep learning, OpenCV for computer vision and OpenGL for computer graphics. I’m especially interested in hybrid approaches, where I mix deep learning stuff, opencv stuff, and classic OpenGL pipeline. The main idea is to avoid framing problems as black box problems, throw a neural network at it and hope for the best. The main idea is rather to do the maximum amount of work with proven technologies, and let deep learning work only on a well-defined subset of the problem.

This time, I was working on an augmented reality problem, where I have an image, and I want to overlay stuff on it. In OpenCV, from an image you can estimate camera parameters, which are called “intrinsic camera parameters”. In OpenCV pinhole camera model, those parameters are: fx (horizontal focal length), fy (vertical focal length), cx (camera center X coord), cy (camera center Y coord).

This is the OpenCV camera matrix: You want to overlay stuff on the original image. Now you have estimated the OpenCV camera parameter, you need to turn it into an OpengL projection matrix, so that you can render stuff on top of the original image using the OpenGL graphics pipeline. This problem of computing the OpenGL projection matrix from OpenCV camera matrix is NOT easy.

First of all, the OpenCV camera matrix projects vertices directly to screen coordinates. (NOTE: don’t forget to then divide by z component). OpenGL projection matrix projects vertices to clip space. The conversion from clip space to NDC (which means division by w component) is handled by OpenGL, and the conversion from NDC to screen space is handled as well by OpenGL. So the first problem is that we’re not looking at the same transformations exactly.

The second problem is that in OpenGL you usually assume that your camera center is at the origin (it’s the convention). It’s not the case in OpenCV, your camera parameters cx and cy let you have the camera center anywhere, it’s a degree of freedom like any other. 90% of the OpenGL projection matrices formulas you will find on the Internet do not account for that.

In the end, I checked many sources:

https://jsantell.com/3d-projection

The formula there is accurate (you can replace K with OpenCV camera matrix).

Here is a source code sample to demonstrate how you can project a point with OpenCV and OpenGL and get the same results (therefore validating your matrices):

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.

 import cv2 import numpy as np import math cx = 88 #principal point x coord cy = 109 #principal point y coord w = 178 #image width h = 218 #image height near = 10 #near plane far = 20 #far plane fovy = 45.0/360.0*2.0*np.pi #45° in radians f = 0.5 * h / math.tan(fovy/2) #cf http://paulbourke.net/miscellaneous/lens/ (NOTE: focal length is in pixels) #we compute the OpenCV camera matrix camera_mtx = np.array([ [f, 0, cx], [0., f, cy], [0.,0.,1.] ], dtype=np.float64) #we compute the corresponding opengl projection matrix #NOTE: K00 = K11 = f, K10 = 0.0, K02 = cx, K12 = cy, K22 = 1.0 opengl_mtx = np.array([ [2*f/w, 0.0, (w – 2*cx)/w, 0.0], [0.0, –2*f/h, (h – 2*cy)/h, 0.0], [0.0, 0.0, (–far – near) / (far – near), –2.0*far*near/(far–near)], [0.0, 0.0, –1.0, 0.0] ]) #point is in opencv camera space (along Oz axis) point = np.array([1.0, 2.0, 15.0]) #Note: coords must be floats #### OpenCV projection screen_point, _ = cv2.projectPoints(np.array([point]), np.zeros(3), np.zeros(3), camera_mtx, np.zeros(5)) print(screen_point) #Note: we obtain the same result with this: (that's what cv2.projectPoints basically does: multiply points with camera matrix and then divide result by z coord) print(camera_mtx.dot(point)/point) #### OpenGL projection #we flip the point z coord, because in opengl camera is oriented along -Oz axis point = –point point2 = np.hstack([point,1.0]) #we add vertex w coord (usually done in vertex shader before multiplying by projection matrix) #we get the point in clip space clip_point = opengl_mtx.dot(point2) #NOTE: what follows "simulates" what happens in OpenGL after the vertex shader. #This is necessary so that we can make sure our projection matrix will yield the correct result when used in OpenGL #we get the point in NDC ndc_point = clip_point / clip_point #we get the screen coordinates viewport_point = (ndc_point + 1.0)/2.0 * np.array([w, h, 1.0, 1.0]) #opencv Oy convention is opposite of OpenGL so we reverse y coord viewport_point = h – viewport_point print(viewport_point) #Now you can see that viewport_point and screen_point have the same x/y coordinates! #This means you can now, from OpenCv camera matrix, use OpenGl to render stuff on top of the image, #thanks to the opengl projection matrix, computed from opencv camera matrix #NOTE: when near plane is small (a few units) and when focal length is small (ex: 10-12), #both results tend to diverge. I'm not sure why the formula starts falling apart at extreme values.

view raw

test.py

hosted with ❤ by GitHub

Full source code can be found here: https://github.com/francoisruty/fruty_opencv-opengl-projection-matrix

When you have your OpenGL projection matrix, you can then render and overlay all the stuff you need on your image. I initially expected this step to take me 1 or 2 hours and it ended up taking me like 6 or 7 hours, so I thought I would share the solution.

## 2 thoughts on “Augmented Reality with OpenCV and OpenGL: the tricky projection matrix”

1. Yixun Liu says:

Very good article!
I have a question about f = h / 2*math.tan(fovy/2)
Should be f = 0.5h / math.tan(fovy/2)?

Like

1. Fruty says:

you’re totally right! I fixed the code!

Like
{"ft_lang_label":"__label__en","ft_lang_prob":0.81031597,"math_prob":0.962856,"size":6264,"snap":"2023-14-2023-23","text_gpt3_token_len":1657,"char_repetition_ratio":0.13051118,"word_repetition_ratio":0.008368201,"special_character_ratio":0.2683589,"punctuation_ratio":0.14736013,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.9938969,"pos_list":[0,1,2,3,4,5,6],"im_url_duplicate_count":[null,6,null,null,null,null,null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2023-06-07T05:11:28Z\",\"WARC-Record-ID\":\"<urn:uuid:7a55d42d-1e37-42b2-80e8-12ec65ad8210>\",\"Content-Length\":\"101848\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:aace52db-ff9d-480b-b451-531a521e161e>\",\"WARC-Concurrent-To\":\"<urn:uuid:291fd1b3-8cf8-4ca3-8d04-1172107028a1>\",\"WARC-IP-Address\":\"192.0.78.25\",\"WARC-Target-URI\":\"https://fruty.io/2019/08/29/augmented-reality-with-opencv-and-opengl-the-tricky-projection-matrix/\",\"WARC-Payload-Digest\":\"sha1:T72XSJ6JE4NKJUZBO23ALP3DOXERIV3M\",\"WARC-Block-Digest\":\"sha1:ZTNG77VHLAVQQKDKF7A5DO6IM5HZJOFA\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2023/CC-MAIN-2023-23/CC-MAIN-2023-23_segments_1685224653608.76_warc_CC-MAIN-20230607042751-20230607072751-00775.warc.gz\"}"}