├── .gitattributes ├── doc ├── related.md └── rotations.md ├── README.md ├── LICENSE ├── .gitignore └── bundle.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /doc/related.md: -------------------------------------------------------------------------------- 1 | # Related 2 | 3 | * [OpenMVG](http://imagine.enpc.fr/~marletr/publi/RRPR-2016-Moulon-et-al.pdf) 4 | * [Theia](http://www.cs.ucsb.edu/~holl/pubs/Sweeney-2015-ACMMM.pdf) 5 | * Bundler 6 | * Colmap 7 | * OpenSfM 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-sfm 2 | A readable implementation of structure-from-motion in python. 3 | 4 | ## Install 5 | 6 | Depends directly on numpy and autograd, indirectly on scipy. Installable using 7 | conda on Windows or Linux. Other platforms not tested. 8 | 9 | ``` 10 | conda create -n simple-sfm -c conda-forge numpy autograd 11 | ``` 12 | -------------------------------------------------------------------------------- /doc/rotations.md: -------------------------------------------------------------------------------- 1 | 2 | # How to best deal with rotations? 3 | 4 | ## Literature 5 | 6 | - [When is Rotations Averaging Hard?](http://www.cs.cornell.edu/~snavely/publications/papers/rotations_eccv2016.pdf) 7 | Kyle Wilson, David Bindel, and Noah Snavely 8 | - Project [Simplifying 3D Reconstruction: Pose-Free Modeling](http://wiki.cs.purdue.edu/cgvlab/doku.php?id=projects:simplify_reconstruction) 9 | - [Angle Independent Bundle Adjustment Refinement](https://www.cs.purdue.edu/cgvlab/papers/aliaga/3dpvt06.pdf) 10 | Jeffrey Zhang, Daniel G. Aliaga, Mireille Boutin, Robert Insley 11 | - [Pose-Free Structure from Motion Using Depth From Motion Constraints](https://www.cs.purdue.edu/cgvlab/papers/aliaga/tip11.pdf) 12 | Ji Zhang, Mireille Boutin, and Daniel G. Aliaga 13 | - [Averages of Rotations and Orientations in 3-space](http://www.cs.unc.edu/techreports/01-029.pdf) 14 | - [Lie Groups for 2D and 3D Transformations](http://ethaneade.com/lie.pdf) 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zbyněk Winkler 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 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /bundle.py: -------------------------------------------------------------------------------- 1 | from autograd import numpy as np 2 | from math import tan, radians 3 | from autograd import value_and_grad 4 | 5 | # todo: what is the best way to encode orientation? 6 | 7 | def reprojection_error(points, camera, views, observations): 8 | assert len(views) == len(observations) 9 | ret = 0.0 10 | camera_matrix = cam2mat(camera) 11 | for view, (pointids, observed_pixels) in zip(views, observations): 12 | position, orientation = view[:3], view[3:] 13 | camera_centric = rotate(points[pointids], orientation) - position 14 | expected_pixels = project(camera_centric, camera_matrix) 15 | ret = ret + np.linalg.norm(observed_pixels - expected_pixels, axis=1) 16 | return ret 17 | 18 | def rotate(points, q): 19 | rot = quat2mat(q) 20 | return np.dot(points, rot) 21 | 22 | _FLOAT_EPS = np.finfo(np.float).eps 23 | def quat2mat(q): 24 | # ------- https://github.com/matthew-brett/transforms3d BSD ----------- 25 | # https://en.wikipedia.org/wiki/Rotation_matrix#Quaternion 26 | w, x, y, z = q 27 | Nq = w*w + x*x + y*y + z*z 28 | if Nq < _FLOAT_EPS: 29 | return np.eye(3) 30 | s = 2.0/Nq 31 | X = x*s 32 | Y = y*s 33 | Z = z*s 34 | wX = w*X; wY = w*Y; wZ = w*Z 35 | xX = x*X; xY = x*Y; xZ = x*Z 36 | yY = y*Y; yZ = y*Z; zZ = z*Z 37 | return np.array( 38 | [[ 1.0-(yY+zZ), xY-wZ, xZ+wY ], 39 | [ xY+wZ, 1.0-(xX+zZ), yZ-wX ], 40 | [ xZ-wY, yZ+wX, 1.0-(xX+yY) ]]) 41 | 42 | def cam2mat(camera): 43 | # http://docs.opencv.org/trunk/d9/d0c/group__calib3d.html#details 44 | fovh, width, height = camera 45 | f = width/2 * tan(radians(fovh)/2) 46 | cx, cy = width/2, height/2 47 | return np.array( 48 | ((f, 0, cx), 49 | (0, f, cy), 50 | (0, 0, 1))) 51 | 52 | def project(points, camera_matrix): 53 | projected = np.dot(points, camera_matrix.T) 54 | divided = projected / projected[:,2].reshape((len(projected),1)) 55 | assert (divided[:,2] == 1).all() 56 | return divided[:,0:2] 57 | 58 | def test_project(): 59 | w, h = 1280, 962 60 | camera_matrix = cam2mat((90, w, h)) 61 | res = np.array([ 62 | [ w/2, 0, w/2 ], 63 | [ 0, w/2, h/2 ], 64 | [ 0, 0, 1 ]]) 65 | assert np.allclose(camera_matrix, res) 66 | test_set = np.array([ 67 | [ 0, 0,1, w/2, h/2], 68 | [ 1, 0,1, w, h/2], 69 | [-1, 0,1, 0, h/2], 70 | [ 0, h/w,1, w/2, h], 71 | [ 0,-h/w,1, w/2, 0], 72 | ], dtype=float) 73 | points = test_set[:,0:3] 74 | pixels = project(points, camera_matrix) 75 | assert np.allclose(pixels, test_set[:,3:5]) 76 | 77 | def test_rotate(): 78 | points = np.ones((1,3)) 79 | e = np.array([1,0,0,0]) # identity quat 80 | eye = quat2mat(e) 81 | assert np.allclose(np.identity(3), eye) 82 | assert (rotate(points, e) == points).all() 83 | quat = [0, 1, 0, 0] 84 | mat = quat2mat(quat) # 180 degree rotation around x axis 85 | assert np.allclose(mat, np.diag([1, -1, -1])) 86 | rotated = rotate(points, quat) # flips y and z 87 | assert np.allclose(rotated, [[ 1, -1, -1]]) 88 | 89 | def test_reprojection_error(): 90 | points = np.zeros((1,3)) 91 | camera = 60, 640, 480 # fovh, w, h 92 | views = np.array([[0.,0.,-1., 1.,0.,0.,0.]]) # origin, no rotation 93 | observations = [ [[0], np.array([[640/2., 480/2.]])] ] 94 | error = reprojection_error(points, camera, views, observations) 95 | assert np.allclose(error, 0) 96 | 97 | points = np.zeros((1,3)) 98 | points[0][2] += 0.1 # move by 10 cm further from camera, this should not increase error 99 | error = reprojection_error(points, camera, views, observations) 100 | assert np.allclose(error, 0) 101 | 102 | points = np.zeros((1,3)) 103 | points[0][0] += 0.1 # move by 10 cm to the right (x axis), increases error 104 | error = reprojection_error(points, camera, views, observations) 105 | assert error > 0.001 106 | 107 | gfun = value_and_grad(reprojection_error) 108 | error_wrapped, gradient = gfun(points, camera, views, observations) 109 | assert error == error_wrapped # function is properly wrapped 110 | 111 | def test_gradient_descent(): 112 | points = np.zeros((1,3)) 113 | camera = 60, 640, 480 # fovh, w, h 114 | views = np.array([[0.,0.,-1., 1.,0.,0.,0.]]) # origin, no rotation 115 | observations = [ [[0], np.array([[640/2., 480/2.]])] ] 116 | 117 | points[0][0] += 0.1 118 | 119 | gfun = value_and_grad(reprojection_error) 120 | error_before, gradient = gfun(points, camera, views, observations) 121 | points -= gradient/10000 122 | error_after, gradient = gfun(points, camera, views, observations) 123 | assert error_after < error_before # error decreses 124 | assert points[0][0] < 0.1 # point is moved towards zero 125 | 126 | if __name__ == "__main__": 127 | test_project() 128 | test_rotate() 129 | test_reprojection_error() 130 | test_gradient_descent() 131 | --------------------------------------------------------------------------------