Pinhole Camera Model

We'll start with a relatively simple example. Borrowing from the camera calibration example, we'll use the Pinhole Camera Model. This model maps points in 3D to pixels. $f$ is the focal length, $c_x, c_y$ are the camera's principal point, $X, Y, Z$ is a point in 3D, and $u, v$ are the resulting pixel locations. Check out the camera calibration blog post for a refresher on these parameters.

$$ \begin{bmatrix}u_{pix} \\v_{pix}\end{bmatrix} =\begin{bmatrix}f \frac{X}{Z} + c_x \\ f \frac{Y}{Z} + c_y\end{bmatrix} $$

In keeping with the optimization theme, we'll convert this into a residual function. The residual here is there difference between an observed pixel location and the expected pixel location we get by passing the corresponding 3D point through the model. To perform camera calibration as described in the Camera Calibration from Scratch blog post, we iteratively change the camera parameters ($f, c_x, c_y$) until the sum of squared residuals is minimized.

$$ r(u, v, f, c_x, c_y, X, Y, Z) = \begin{bmatrix}u_{obs} \\v_{obs}\end{bmatrix} - \begin{bmatrix}f \frac{X}{Z} + c_x \\ f \frac{Y}{Z} + c_y\end{bmatrix} $$

Performing this minimization requires providing residuals and Jacobians of those residuals to the optimization algorithm. We've just provided the expression for the residual, so now let's focus on the Jacobian.

First we'll start by enumerating the symbols in the function in question. We pass to the symbols() function a list of symbol names we'll be using and it'll return handles to those names. The symbol names and python variable names don't need to be the same even though they are here.

Next we'll build the residual vector expression. We'll do by creating a Matrix.

Now we build the parameter vector, i.e. the list of parameters we wish optimize. Since this is a camera calibration example, this is just $ f, c_x, c_y$, the focal length and principal point of our camera. The other parameters are observations or constants we don't optimize.

Now we just call the jacobian() method on the residual vector, passing the parameter vector. This will generate the Jacobian of the residual expression with respect to the parameters we passed. Note the dimensions. The residual has two rows and thus so does the Jacobian. There are three input parameters and thus the Jacobian has three columns.

If we instead wanted to optimize the 3D point($ \begin{bmatrix}X & Y & Z\end{bmatrix} $) like one might see in iterative PnP or Bundle Adjustment problems, we would change the parameter vector to contain those points.

Brown Conrady Distortion Model

Now let's try something more complicated. The Pinhole model can be extended to better model cameras with lens distortion). The Brown-Conrady model is one of the most common of such models; OpenCV implements it in its calib3d library. The model has additional coefficients ($k1, k2, k3, p1, p2$), with $k$ coefficients modeling radial distortion and $p$ coefficients modeling tangential distortion. We'll use the same notation conventions as with the Pinhole model and show how a 3D point gets mapped to a pixel in the presence of distortion:

$$ \begin{bmatrix}x' \\y'\end{bmatrix} = \begin{bmatrix}X/Z \\Y/Z\end{bmatrix} $$$$ r^2 = x'^2 + y'^2 $$$$ \begin{bmatrix} x'' \\ y'' \end{bmatrix} = \begin{bmatrix} x' ( 1+ k_1r^2 + k_2 r^4 + k_3 r^6) + 2p_1 x' y' + p_2 ( r^2 + 2 x' ^2) \\ y' ( 1+ k_1r^2 + k_2 r^4 + k_3 r^6) + p_1 ( r^2 + 2 y' ^2) + 2 p_2 x' y' \end{bmatrix} $$$$ \begin{bmatrix}u_{calc} \\v_{calc}\end{bmatrix} = \begin{bmatrix}f x'' + c_x \\ f y'' + c_y\end{bmatrix} $$

And as a residual: $$ r(u, v, f, c_x, c_y, X, Y, Z, k_1, k_2, k_3, p_1, p_2) = \begin{bmatrix}u_{obs} \\v_{obs}\end{bmatrix} - \begin{bmatrix}f x'' + c_x \\ f y'' + c_y\end{bmatrix} $$

We'll start by modeling the distortion in SymPy:

Now we'll construct the matrix residual expression:

And calculate the Jacobian:

Great! we've got our Jacobian. It just looks a little hairy. SymPy can further simplify expressions and also perform common subexpression elimination which is useful to reduce the amount of transcription necessary to use this elsewhere. In this case we can also just simplify some of this by inspection. In the above definition of the model we defined a few intermediate variables ($x', y', x'', y'', r^2$) for convenience. We can substitute those back into the equation. This isn't just for having a cleaner visual output; it's likely that you'll have to compute these subexpressions to evaluate the residual function. We'll use the subs() function to perform these substituions. Unfortunately this function won't recognized the subexpressions (like xpp) generated heretofor. Instead we'll have to generate some dummy symbols and replace the corresponding expressions with those dummies.

Conclusion

And there you have a it: a nice, clean, simplified, expression for the Jacobian of our residual function with respect to the camera parameters ($f, c_x, c_y, k_1, k_2, k_3, p_1, p_2$). Using the techniques in this notebook, you should be able to come up with Jacobians for your own function using SymPy.