├── .gitignore ├── .isort.cfg ├── LICENSE ├── README.md ├── camera-models.ipynb ├── camera_models ├── __init__.py ├── _figures.py ├── _frame.py ├── _homogeneus.py ├── _image.py ├── _matrices.py ├── _principal_axis.py └── _utils.py ├── environment.yml ├── figures ├── 638px-Yaw_Axis_Corrected.svg.png └── Pinhole-camera.svg └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Visual Studio Code 132 | .vscode/ 133 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | force_grid_wrap=0 5 | use_parentheses=True 6 | line_length=88 7 | known_first_party=vinbigdata 8 | known_third_party=pandas 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mario Namtao Shianti Larcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Medium Badge](https://badgen.net/badge/icon/medium?icon=medium&label)](https://medium.com/analytics-vidhya/simple-camera-models-with-numpy-and-matplotlib-92281f15f9b2) 3 | 4 | ## Introduction 5 | 6 | This repository contains a notebook and a library to explain the operation of some simple camera models. The theory presented is almost entirely taken from the book [Multiple View Geometry in Computer Vision](https://www.robots.ox.ac.uk/~vgg/hzbook/) by Richard Hartley and Andrew Zisserman, in particular from chapter 6 "Camera Models". The purpose of this work is therefore not to explain the theory but to apply it, trying to improve the reader's understanding of these concepts. 7 | 8 | ## Get Started 9 | 10 | In order to use the notebook and library here, you must first create and activate a virtual environment using Anaconda. If you have not yet installed Anaconda, you can do so by following [these](https://docs.anaconda.com/anaconda/install/) instructions. After that you just need to run the following commands: 11 | ```bash 12 | $ cd camera-models 13 | $ conda env create --prefix ./env --file environment.yml 14 | $ conda activate ./env 15 | $ jupyter notebook 16 | ``` 17 | 18 | # Changelog 19 | 20 | ## 2022-06-29 21 | 22 | - Fix the inconsistent use of F instead of FOCAL_LENGTH in the last part of the notebook thanks to [@alexflorensa](https://github.com/alexflorensa) ([#2](https://github.com/mnslarcher/camera-models/pull/2)) 23 | 24 | ## 2022-04-24 25 | 26 | - Fix the inconsistent use of the rotation matrix thanks to [@estshorter](https://github.com/estshorter) ([#1](https://github.com/mnslarcher/camera-models/pull/1)) 27 | -------------------------------------------------------------------------------- /camera_models/__init__.py: -------------------------------------------------------------------------------- 1 | from ._figures import GenericPoint, Polygon 2 | from ._frame import ReferenceFrame 3 | from ._homogeneus import to_homogeneus, to_inhomogeneus 4 | from ._image import Image, ImagePlane 5 | from ._matrices import ( 6 | get_calibration_matrix, 7 | get_plucker_matrix, 8 | get_projection_matrix, 9 | get_rotation_matrix, 10 | ) 11 | from ._principal_axis import PrincipalAxis 12 | from ._utils import ( 13 | draw3d_arrow, 14 | get_plane_from_three_points, 15 | set_xyzlim3d, 16 | set_xyzticks, 17 | ) 18 | 19 | __all__ = [ 20 | "GenericPoint", 21 | "Image", 22 | "ImagePlane", 23 | "Polygon", 24 | "PrincipalAxis", 25 | "ReferenceFrame", 26 | "draw3d_arrow", 27 | "get_calibration_matrix", 28 | "get_plane_from_three_points", 29 | "get_plucker_matrix", 30 | "get_projection_matrix", 31 | "get_rotation_matrix", 32 | "to_homogeneus", 33 | "to_inhomogeneus", 34 | "set_xyzlim3d", 35 | "set_xyzticks", 36 | ] 37 | __version__ = "0.0.1" 38 | -------------------------------------------------------------------------------- /camera_models/_figures.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | from ._homogeneus import to_homogeneus, to_inhomogeneus 8 | from ._matrices import get_plucker_matrix, get_projection_matrix 9 | 10 | 11 | class GenericPoint: 12 | def __init__(self, X: np.ndarray, name: Optional[str] = None) -> None: 13 | self.values = X 14 | self.name = name 15 | 16 | def draw( 17 | self, 18 | f: float, 19 | px: float = 0.0, 20 | py: float = 0.0, 21 | C: Sequence[float] = (0.0, 0.0, 0.0), 22 | theta_x: float = 0.0, 23 | theta_y: float = 0.0, 24 | theta_z: float = 0.0, 25 | mx: float = 1.0, 26 | my: float = 1.0, 27 | s: float = 20.0, 28 | color: str = "tab:green", 29 | closed: bool = True, 30 | ax: Optional[plt.Axes] = None, 31 | ) -> plt.Axes: 32 | if ax is None: 33 | ax = plt.gca() 34 | 35 | P = get_projection_matrix( 36 | f, 37 | px=px, 38 | py=py, 39 | mx=mx, 40 | my=my, 41 | theta_x=theta_x, 42 | theta_y=theta_y, 43 | theta_z=theta_z, 44 | C=C, 45 | ) 46 | 47 | x = to_inhomogeneus(P @ to_homogeneus(self.values)) 48 | ax.scatter(*x, s=s, color=color) 49 | if self.name is not None: 50 | ax.text(*x, self.name) 51 | 52 | return ax 53 | 54 | def draw3d( 55 | self, 56 | pi: np.ndarray, 57 | C: Sequence[float] = (0.0, 0.0, 0.0), 58 | s: float = 20.0, 59 | color: str = "tab:green", 60 | closed=True, 61 | ax: Optional[Axes3D] = None, 62 | ) -> Axes3D: 63 | if ax is None: 64 | ax = plt.gca(projection="3d") 65 | 66 | L = get_plucker_matrix(np.asarray(C), self.values) 67 | x = to_inhomogeneus(L @ pi) 68 | ax.scatter3D(*self.values, s=s, color=color) 69 | ax.scatter3D(*x, s=s, color=color) 70 | ax.plot(*np.c_[C, self.values], color="tab:gray", alpha=0.5, ls="--") 71 | if self.name is not None: 72 | ax.text(*self.values, self.name) 73 | ax.text(*x, self.name.lower()) 74 | 75 | return ax 76 | 77 | 78 | class Polygon: 79 | def __init__(self, xyz: np.ndarray) -> None: 80 | self.values = xyz 81 | 82 | def draw( 83 | self, 84 | f: float, 85 | px: float = 0.0, 86 | py: float = 0.0, 87 | C: Sequence[float] = (0.0, 0.0, 0.0), 88 | theta_x: float = 0.0, 89 | theta_y: float = 0.0, 90 | theta_z: float = 0.0, 91 | mx: float = 1.0, 92 | my: float = 1.0, 93 | s: float = 20.0, 94 | color: str = "tab:green", 95 | closed: bool = True, 96 | ax: Optional[plt.Axes] = None, 97 | ) -> plt.Axes: 98 | if ax is None: 99 | ax = plt.gca() 100 | 101 | P = get_projection_matrix( 102 | f, 103 | px=px, 104 | py=py, 105 | mx=mx, 106 | my=my, 107 | theta_x=theta_x, 108 | theta_y=theta_y, 109 | theta_z=theta_z, 110 | C=C, 111 | ) 112 | x_list = [] 113 | for i, X in enumerate(self.values, 1): 114 | x = to_inhomogeneus(P @ to_homogeneus(X)) 115 | ax.scatter(*x, s=s, color=color) 116 | ax.text(*x, f"x{i}") 117 | x_list.append(x) 118 | 119 | if closed: 120 | x_list.append(x_list[0]) 121 | 122 | ax.plot(*np.vstack(x_list).T, color=color) 123 | return ax 124 | 125 | def draw3d( 126 | self, 127 | pi: np.ndarray, 128 | C: Sequence[float] = (0.0, 0.0, 0.0), 129 | s: float = 20.0, 130 | color: str = "tab:green", 131 | closed=True, 132 | ax: Optional[Axes3D] = None, 133 | ) -> Axes3D: 134 | if ax is None: 135 | ax = plt.gca(projection="3d") 136 | 137 | xyz = self.values.copy() 138 | x_list = [] 139 | for i, X in enumerate(xyz, 1): 140 | L = get_plucker_matrix(np.asarray(C), X) 141 | x = to_inhomogeneus(L @ pi) 142 | ax.scatter3D(*X, s=s, color=color) 143 | ax.scatter3D(*x, s=s, color=color) 144 | ax.plot(*np.c_[C, X], color="tab:gray", alpha=0.5, ls="--") 145 | ax.text(*X, f"X{i}") 146 | ax.text(*x, f"x{i}") 147 | x_list.append(x) 148 | 149 | if closed: 150 | xyz = np.vstack([xyz, xyz[0, :]]) 151 | x_list.append(x_list[0]) 152 | 153 | ax.plot(*xyz.T, color=color) 154 | ax.plot(*np.vstack(x_list).T, color=color) 155 | return ax 156 | -------------------------------------------------------------------------------- /camera_models/_frame.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | from ._utils import draw3d_arrow 8 | 9 | 10 | class ReferenceFrame: 11 | def __init__( 12 | self, 13 | origin: np.ndarray, 14 | dx: np.ndarray, 15 | dy: np.ndarray, 16 | dz: np.ndarray, 17 | name: str, 18 | ) -> None: 19 | self.origin = origin 20 | self.dx = dx 21 | self.dy = dy 22 | self.dz = dz 23 | self.name = name 24 | 25 | def draw3d( 26 | self, 27 | head_length: float = 0.3, 28 | color: str = "tab:blue", 29 | ax: Optional[Axes3D] = None, 30 | ) -> Axes3D: 31 | if ax is None: 32 | ax = plt.gca(projection="3d") 33 | 34 | ax.text(*self.origin + 0.5, f"({self.name})") 35 | ax = draw3d_arrow( 36 | ax=ax, 37 | arrow_location=self.origin, 38 | arrow_vector=self.dx, 39 | head_length=head_length, 40 | color=color, 41 | name="x", 42 | ) 43 | ax = draw3d_arrow( 44 | ax=ax, 45 | arrow_location=self.origin, 46 | arrow_vector=self.dy, 47 | head_length=head_length, 48 | color=color, 49 | name="y", 50 | ) 51 | ax = draw3d_arrow( 52 | ax=ax, 53 | arrow_location=self.origin, 54 | arrow_vector=self.dz, 55 | head_length=head_length, 56 | color=color, 57 | name="z", 58 | ) 59 | return ax 60 | -------------------------------------------------------------------------------- /camera_models/_homogeneus.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def to_inhomogeneus(X: np.ndarray) -> np.ndarray: 5 | if X.ndim > 1: 6 | raise ValueError("x must be one-dimensional.") 7 | 8 | return (X / X[-1])[:-1] 9 | 10 | 11 | def to_homogeneus(X: np.ndarray) -> np.ndarray: 12 | if X.ndim > 1: 13 | raise ValueError("X must be one-dimensional.") 14 | 15 | return np.hstack([X, 1]) 16 | -------------------------------------------------------------------------------- /camera_models/_image.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | from ._utils import get_plane_from_three_points 8 | 9 | 10 | class Image: 11 | def __init__(self, heigth: int, width: int) -> None: 12 | self.heigth = heigth 13 | self.width = width 14 | 15 | def draw(self, color: str = "tab:gray", ax: Optional[plt.Axes] = None) -> plt.Axes: 16 | if ax is None: 17 | ax = plt.gca() 18 | 19 | ax.set_xticks(np.arange(0, self.width + 1)) 20 | ax.set_yticks(np.arange(0, self.heigth + 1)) 21 | ax.grid(color=color) 22 | ax.set_xlim(0, self.width) 23 | ax.set_ylim(0, self.heigth) 24 | ax.set_aspect("equal") 25 | return ax 26 | 27 | 28 | class ImagePlane: 29 | def __init__( 30 | self, 31 | origin: np.ndarray, 32 | dx: np.ndarray, 33 | dy: np.ndarray, 34 | heigth: int, 35 | width: int, 36 | mx: float = 1.0, 37 | my: float = 1.0, 38 | ) -> None: 39 | self.origin = origin 40 | self.dx = dx 41 | self.dy = dy 42 | self.heigth = heigth 43 | self.width = width 44 | self.mx = mx 45 | self.my = my 46 | self.pi = get_plane_from_three_points(origin, origin + dx, origin + dy) 47 | 48 | def draw3d( 49 | self, color: str = "tab:gray", alpha: float = 0.5, ax: Optional[Axes3D] = None 50 | ) -> Axes3D: 51 | if ax is None: 52 | ax = plt.gca(projection="3d") 53 | 54 | xticks = np.arange(self.width + 1).reshape(-1, 1) * self.dx / self.mx 55 | yticks = np.arange(self.heigth + 1).reshape(-1, 1) * self.dy / self.my 56 | pts = (self.origin + xticks).reshape(-1, 1, 3) + yticks 57 | pts = pts.reshape(-1, 3) 58 | shape = len(xticks), len(yticks) 59 | X = pts[:, 0].reshape(shape) 60 | Y = pts[:, 1].reshape(shape) 61 | Z = pts[:, 2].reshape(shape) 62 | frame = np.c_[ 63 | self.origin, 64 | self.origin + self.dx * self.width / self.mx, 65 | self.origin 66 | + self.dx * self.width / self.mx 67 | + self.dy * self.heigth / self.my, 68 | self.origin + self.dy * self.heigth / self.my, 69 | self.origin, 70 | ] 71 | ax.plot(*frame, color="black") 72 | ax.plot_wireframe(X, Y, Z, color=color, alpha=alpha) 73 | return ax 74 | -------------------------------------------------------------------------------- /camera_models/_matrices.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | import numpy as np 4 | 5 | from ._homogeneus import to_homogeneus 6 | 7 | 8 | def get_plucker_matrix(A: np.ndarray, B: np.ndarray) -> np.ndarray: 9 | A = to_homogeneus(A) 10 | B = to_homogeneus(B) 11 | L = A.reshape(-1, 1) * B.reshape(1, -1) - B.reshape(-1, 1) * A.reshape(1, -1) 12 | return L 13 | 14 | 15 | def _get_roll_matrix(theta_x: float = 0.0) -> np.ndarray: 16 | Rx = np.array( 17 | [ 18 | [1.0, 0.0, 0.0], 19 | [0.0, np.cos(theta_x), -np.sin(theta_x)], 20 | [0.0, np.sin(theta_x), np.cos(theta_x)], 21 | ] 22 | ) 23 | return Rx 24 | 25 | 26 | def _get_pitch_matrix(theta_y: float = 0.0) -> np.ndarray: 27 | Ry = np.array( 28 | [ 29 | [np.cos(theta_y), 0.0, np.sin(theta_y)], 30 | [0.0, 1.0, 0.0], 31 | [-np.sin(theta_y), 0.0, np.cos(theta_y)], 32 | ] 33 | ) 34 | return Ry 35 | 36 | 37 | def _get_yaw_matrix(theta_z: float = 0.0) -> np.ndarray: 38 | Rz = np.array( 39 | [ 40 | [np.cos(theta_z), -np.sin(theta_z), 0.0], 41 | [np.sin(theta_z), np.cos(theta_z), 0.0], 42 | [0.0, 0.0, 1.0], 43 | ] 44 | ) 45 | return Rz 46 | 47 | 48 | def get_rotation_matrix( 49 | theta_x: float = 0.0, theta_y: float = 0.0, theta_z: float = 0.0 50 | ) -> np.ndarray: 51 | # Roll 52 | Rx = _get_roll_matrix(theta_x) 53 | # Pitch 54 | Ry = _get_pitch_matrix(theta_y) 55 | # Yaw 56 | Rz = _get_yaw_matrix(theta_z) 57 | return Rz @ Ry @ Rx 58 | 59 | 60 | def get_calibration_matrix( 61 | f: float, 62 | px: float = 0.0, 63 | py: float = 0.0, 64 | mx: float = 1.0, 65 | my: float = 1.0, 66 | ) -> np.ndarray: 67 | K = np.diag([mx, my, 1]) @ np.array([[f, 0.0, px], [0.0, f, py], [0.0, 0.0, 1.0]]) 68 | return K 69 | 70 | 71 | def get_projection_matrix( 72 | f: float, 73 | px: float = 0.0, 74 | py: float = 0.0, 75 | C: Sequence[float] = (0.0, 0.0, 0.0), 76 | theta_x: float = 0.0, 77 | theta_y: float = 0.0, 78 | theta_z: float = 0.0, 79 | mx: float = 1.0, 80 | my: float = 1.0, 81 | ) -> np.ndarray: 82 | K = get_calibration_matrix(f=f, px=px, py=py, mx=mx, my=my) 83 | R = get_rotation_matrix(theta_x=theta_x, theta_y=theta_y, theta_z=theta_z).T 84 | P = K @ R @ np.c_[np.eye(3), -np.asarray(C)] 85 | return P 86 | -------------------------------------------------------------------------------- /camera_models/_principal_axis.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | from ._utils import draw3d_arrow 8 | 9 | 10 | class PrincipalAxis: 11 | def __init__( 12 | self, camera_center: np.ndarray, camera_dz: np.ndarray, f: float 13 | ) -> None: 14 | self.camera_center = camera_center 15 | self.camera_dz = camera_dz 16 | self.f = f 17 | self.p = camera_center + f * camera_dz 18 | 19 | def draw3d( 20 | self, 21 | head_length: float = 0.3, 22 | color: str = "tab:red", 23 | s: float = 20.0, 24 | ax: Optional[Axes3D] = None, 25 | ) -> Axes3D: 26 | if ax is None: 27 | ax = plt.gca(projection="3d") 28 | 29 | draw3d_arrow( 30 | arrow_location=self.camera_center, 31 | arrow_vector=2.0 * self.f * self.camera_dz, 32 | head_length=head_length, 33 | color=color, 34 | name="Z", 35 | ax=ax, 36 | ) 37 | ax.scatter3D(*self.p, s=s, color=color) 38 | ax.text(*self.p, "p") 39 | return ax 40 | -------------------------------------------------------------------------------- /camera_models/_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | 8 | def draw3d_arrow( 9 | arrow_location: np.ndarray, 10 | arrow_vector: np.ndarray, 11 | head_length: float = 0.3, 12 | color: Optional[str] = None, 13 | name: Optional[str] = None, 14 | ax: Optional[Axes3D] = None, 15 | ) -> Axes3D: 16 | if ax is None: 17 | ax = plt.gca(projection="3d") 18 | 19 | ax.quiver( 20 | *arrow_location, 21 | *arrow_vector, 22 | arrow_length_ratio=head_length / np.linalg.norm(arrow_vector), 23 | color=color, 24 | ) 25 | if name is not None: 26 | ax.text(*(arrow_location + arrow_vector), name) 27 | 28 | return ax 29 | 30 | 31 | def get_plane_from_three_points( 32 | X1: np.ndarray, X2: np.ndarray, X3: np.ndarray 33 | ) -> np.ndarray: 34 | pi = np.hstack([np.cross(X1 - X3, X2 - X3), -X3 @ np.cross(X1, X2)]) 35 | return pi 36 | 37 | 38 | def set_xyzlim3d( 39 | left: Optional[float] = None, 40 | right: Optional[float] = None, 41 | ax: Optional[Axes3D] = None, 42 | ) -> Axes3D: 43 | if ax is None: 44 | ax = plt.gca(projection="3d") 45 | 46 | ax.set_xlim3d(left, right) 47 | ax.set_ylim3d(left, right) 48 | ax.set_zlim3d(left, right) 49 | return ax 50 | 51 | 52 | def set_xyzticks(ticks: List[float], ax: Optional[Axes3D] = None) -> Axes3D: 53 | if ax is None: 54 | ax = plt.gca(projection="3d") 55 | 56 | ax.set_xticks(ticks) 57 | ax.set_yticks(ticks) 58 | ax.set_zticks(ticks) 59 | return ax 60 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # conda env create --prefix ./env --file environment.yml # create the environment 2 | # conda activate ./env # activate the environment 3 | # conda env update --prefix ./env --file environment.yml --prune # update the environment 4 | # conda deactivate # deactivate the environment 5 | channels: 6 | - conda-forge 7 | dependencies: 8 | - black 9 | - flake8 10 | - ipykernel 11 | - ipywidgets 12 | - matplotlib 13 | - mypy 14 | - notebook 15 | - numpy 16 | - python>=3.7 17 | -------------------------------------------------------------------------------- /figures/638px-Yaw_Axis_Corrected.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mnslarcher/camera-models/51d7872bf0fd6ea7be6dbaa5282fdc63f3fb618a/figures/638px-Yaw_Axis_Corrected.svg.png -------------------------------------------------------------------------------- /figures/Pinhole-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 63 | 74 | 85 | 96 | 106 | 117 | 128 | 129 | 149 | 153 | 158 | 163 | 167 | 173 | 179 | 184 | 188 | 192 | 205 | 209 | 213 | 217 | 222 | 227 | 240 | 241 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # E203: black and flake8 disagree on whitespace before ':' 3 | # W503: black and flake8 disagree on how to place operators 4 | ignore = E203, W503 5 | max-line-length = 88 6 | 7 | [mypy] 8 | allow_redefinition = True 9 | ignore_missing_imports = True 10 | --------------------------------------------------------------------------------