├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── bunny.obj ├── killeroo.obj ├── pyobb_demos │ ├── 2d_demo.py │ ├── 3d_demo.py │ ├── __init__.py │ └── objloader.py ├── requirements.txt └── setup.py ├── pyobb ├── __init__.py └── obb.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests └── test_obb.py /.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 | /.idea 91 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | install: 5 | - pip install -e . 6 | - pip install -r requirements.txt 7 | script: pytest 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pedro Boechat 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 | # pyobb 2 | 3 | [![Build Status](https://travis-ci.org/pboechat/pyobb.svg?branch=master)](https://travis-ci.org/pboechat/pyobb) [![PyPI version](https://badge.fury.io/py/pyobb.svg)](https://badge.fury.io/py/pyobb) 4 | 5 | > OBB implementation in Python (using numpy) 6 | 7 | This is basically a port of the code found on [James' Blog](http://jamesgregson.blogspot.com/2011/03/latex-test.html), which in turn is a C++ implementation (using CGAL) of the ideas found in Stefan Gottschalk's [PhD thesis](http://gamma.cs.unc.edu/users/gottschalk/main.pdf). 8 | The central idea of this OBB contruction is to compute a covariance matrix for a point set and then find the eigenvectors of this covariance matrix. 9 | 10 | ---------- 11 | 12 | ### Installation 13 | 14 | Simply run 15 | 16 | pip install pyobb 17 | 18 | 19 | ### Usage 20 | 21 | The *pyobb* package contains a single class: *OBB*. An OBB has the following attributes: 22 | 23 | * *centroid*: the OBB center 24 | * *min*: the OBB point with the smallest XYZ components in the local frame (i.e., -[width/2, height/2, depth/2]) 25 | * *max*: the OBB point with the largest XYZ components in the local frame (i.e., [width/2, height/2, depth/2]) 26 | * *points*: the 8 points of the OBB 27 | * *extents*: the extents of the OBB in the XYZ-axis (i.e., the scaled unit vectors of the global frame) 28 | * *rotation*: the rotation matrix of the OBB 29 | 30 | You have three different ways to build an OBB: using a covariance matrix, using a point set and using a triangle mesh. Those ways are respectively implemented by the methods: 31 | 32 | * *OBB.build_from_covariance_matrix(covariance_matrix, points)*: expects a 3x3 covariance matrix and a set of 3D points 33 | * *OBB.build_from_points(points)*: expects a set of 3D points 34 | * *OBB.build_from_triangles(points, triangles)*: expects a set of 3D points and a flat list of indices refering those points for which every 3-uple would form a triangle 35 | 36 | For instance, you can create an OBB from the points of a lat/lon sphere 37 | 38 | from math import pi, cos, sin, sqrt 39 | from pyobb.obb import OBB 40 | 41 | # creates a lat/lon sphere with a given radius and centered at a given point 42 | def sphere(radius, center, num_slices=30): 43 | theta_step = 2.0 * pi / (num_slices - 1) 44 | phi_step = pi / (num_slices - 1.0) 45 | theta = 0.0 46 | vertices = [] 47 | for i in range(0, num_slices): 48 | cos_theta = cos(theta) 49 | sin_theta = sin(theta) 50 | phi = 0.0 51 | for j in range(0, num_slices): 52 | x = -sin(phi) * cos_theta 53 | y = -cos(phi) 54 | z = -sin(phi) * sin_theta 55 | n = sqrt(x * x + y * y + z * z) 56 | if n < 0.99 or n > 1.01: 57 | x /= n 58 | y /= n 59 | z /= n 60 | vertices.append((x * radius + center[0], 61 | y * radius + center[1], 62 | z * radius + center[2])) 63 | phi += phi_step 64 | theta += theta_step 65 | return vertices 66 | 67 | obb = OBB.build_from_points(sphere(1, (0, 0, 0))) 68 | 69 | Which gives you this OBB: 70 | 71 | ![](http://pedroboechat.com/images/pyobb_0.png) 72 | 73 | You can also create an OBB from the vertices and faces of OBJ models 74 | 75 | from pyobb.obb import OBB 76 | from objloader import OBJ # source: http://www.pygame.org/wiki/OBJFileLoader 77 | 78 | obj = OBJ(filename='bunny.obj') # stanford bunny 79 | # obj = OBJ(filename='killeroo.obj') # killeroo 80 | indices = [] 81 | for face in obj.faces: 82 | indices.append(face[0][0] - 1) 83 | indices.append(face[0][1] - 1) 84 | indices.append(face[0][2] - 1) 85 | obb = OBB.build_from_triangles(obj.vertices, indices) 86 | 87 | Which gives you something like this: 88 | 89 | ![](http://pedroboechat.com/images/pyobb_1.png) 90 | 91 | or this: 92 | 93 | ![](http://pedroboechat.com/images/pyobb_2.png) 94 | -------------------------------------------------------------------------------- /bin/pyobb_demos/2d_demo.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | 3 | from OpenGL.GL import * 4 | from OpenGL.GLU import * 5 | from pygame import * 6 | from pygame.constants import * 7 | 8 | from pyobb.obb import OBB 9 | 10 | 11 | ######################################################################################################################## 12 | # copied from: http://www.pygame.org/wiki/OBJFileLoader 13 | ######################################################################################################################## 14 | def main(): 15 | init() 16 | viewport = (800, 600) 17 | display.set_mode(viewport, OPENGL | DOUBLEBUF) 18 | display.set_caption('pyobb 2D demo') 19 | 20 | clock = time.Clock() 21 | 22 | glMatrixMode(GL_PROJECTION) 23 | glLoadIdentity() 24 | width, height = viewport 25 | gluOrtho2D(0, width, 0, height) 26 | 27 | glMatrixMode(GL_MODELVIEW) 28 | glLoadIdentity() 29 | 30 | points = [] 31 | render_gl_lists = False 32 | while True: 33 | clock.tick(30) 34 | for e in event.get(): 35 | if e.type == QUIT: 36 | exit() 37 | elif e.type == KEYDOWN: 38 | if e.key == K_ESCAPE: 39 | exit() 40 | elif e.key == K_RETURN: 41 | poly_gl_list = glGenLists(1) 42 | glNewList(poly_gl_list, GL_COMPILE) 43 | glColor3fv((0, 0, 1)) 44 | glBegin(GL_POLYGON) 45 | for point in points: 46 | glVertex2fv(point) 47 | glEnd() 48 | glEndList() 49 | 50 | obb = OBB.build_from_points([(point[0], point[1], 0) for point in points]) 51 | 52 | obb_gl_list = glGenLists(1) 53 | glNewList(obb_gl_list, GL_COMPILE) 54 | glBegin(GL_LINES) 55 | glColor3fv((1, 0, 0)) 56 | 57 | def input_vertex(x, y, z): 58 | glVertex3fv(obb.transform((x, y, z))) 59 | 60 | input_vertex(*obb.max) 61 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 62 | 63 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 64 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 65 | 66 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 67 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 68 | 69 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 70 | input_vertex(*obb.max) 71 | 72 | input_vertex(obb.max[0], obb.max[1], obb.max[2]) 73 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 74 | 75 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 76 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 77 | 78 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 79 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 80 | 81 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 82 | input_vertex(obb.min[0], obb.min[1], obb.min[2]) 83 | 84 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 85 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 86 | 87 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 88 | input_vertex(*obb.min) 89 | 90 | input_vertex(*obb.min) 91 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 92 | 93 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 94 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 95 | 96 | glEnd() 97 | glEndList() 98 | render_gl_lists = True 99 | elif e.key == K_BACKSPACE: 100 | points = [] 101 | render_gl_lists = False 102 | elif e.type == MOUSEBUTTONDOWN: 103 | if e.button == 1: 104 | point = mouse.get_pos() 105 | points.append((point[0], height - point[1])) 106 | elif e.button == 2: 107 | points = points[:-1] 108 | 109 | glClear(GL_COLOR_BUFFER_BIT) 110 | glLoadIdentity() 111 | 112 | if render_gl_lists: 113 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 114 | glCallList(poly_gl_list) 115 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 116 | glCallList(obb_gl_list) 117 | glPointSize(6.0) 118 | glColor3fv((0, 1, 0)) 119 | glBegin(GL_POINTS) 120 | for point in points: 121 | glVertex2fv(point) 122 | glEnd() 123 | 124 | display.flip() 125 | 126 | 127 | if __name__ == '__main__': 128 | main() 129 | -------------------------------------------------------------------------------- /bin/pyobb_demos/3d_demo.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | from argparse import ArgumentParser 3 | from pygame import * 4 | from pygame.constants import * 5 | from OpenGL.GL import * 6 | from OpenGL.GLU import * 7 | from pyobb.obb import OBB 8 | 9 | from objloader import OBJ 10 | 11 | 12 | ######################################################################################################################## 13 | # copied from: http://www.pygame.org/wiki/OBJFileLoader 14 | ######################################################################################################################## 15 | def main(): 16 | parser = ArgumentParser() 17 | parser.add_argument('--obj', type=str, required=True, help='OBJ filename') 18 | args = parser.parse_args() 19 | 20 | init() 21 | viewport = (800, 600) 22 | display.set_mode(viewport, OPENGL | DOUBLEBUF) 23 | display.set_caption('pyobb 3D demo') 24 | 25 | glEnable(GL_LIGHTING) 26 | glEnable(GL_LIGHT0) 27 | glLightfv(GL_LIGHT0, GL_POSITION, (0, -1, 0, 0)) 28 | glLightfv(GL_LIGHT0, GL_AMBIENT, (0.2, 0.2, 0.2, 1)) 29 | glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.5, 0.5, 0.5, 1)) 30 | 31 | glEnable(GL_COLOR_MATERIAL) 32 | glEnable(GL_DEPTH_TEST) 33 | glShadeModel(GL_SMOOTH) 34 | 35 | obj = OBJ(filename=args.obj) 36 | indices = [] 37 | for face in obj.faces: 38 | indices.append(face[0][0] - 1) 39 | indices.append(face[0][1] - 1) 40 | indices.append(face[0][2] - 1) 41 | obb = OBB.build_from_triangles(obj.vertices, indices) 42 | 43 | obb_gl_list = glGenLists(1) 44 | glNewList(obb_gl_list, GL_COMPILE) 45 | glBegin(GL_LINES) 46 | glColor3fv((1, 0, 0)) 47 | 48 | def input_vertex(x, y, z): 49 | glVertex3fv(obb.transform((x, y, z))) 50 | 51 | input_vertex(*obb.max) 52 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 53 | 54 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 55 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 56 | 57 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 58 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 59 | 60 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 61 | input_vertex(*obb.max) 62 | 63 | input_vertex(obb.max[0], obb.max[1], obb.max[2]) 64 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 65 | 66 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 67 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 68 | 69 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 70 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 71 | 72 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 73 | input_vertex(obb.min[0], obb.min[1], obb.min[2]) 74 | 75 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 76 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 77 | 78 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 79 | input_vertex(*obb.min) 80 | 81 | input_vertex(*obb.min) 82 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 83 | 84 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 85 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 86 | glEnd() 87 | glEndList() 88 | 89 | clock = time.Clock() 90 | 91 | glMatrixMode(GL_PROJECTION) 92 | glLoadIdentity() 93 | width, height = viewport 94 | gluPerspective(90.0, width / float(height), 0.1, 100.0) 95 | glEnable(GL_DEPTH_TEST) 96 | glMatrixMode(GL_MODELVIEW) 97 | 98 | rotation = [0, 0] 99 | translation = [-obb.centroid[0], -obb.centroid[1], -(obb.centroid[2] + obb.extents[2] * 2)] 100 | rotate = move = False 101 | while True: 102 | clock.tick(30) 103 | for e in event.get(): 104 | if e.type == QUIT: 105 | exit() 106 | elif e.type == KEYDOWN and e.key == K_ESCAPE: 107 | exit() 108 | elif e.type == MOUSEBUTTONDOWN: 109 | if e.button == 4: 110 | translation[2] += 0.1 111 | elif e.button == 5: 112 | translation[2] -= 0.1 113 | elif e.button == 1: 114 | rotate = True 115 | elif e.button == 2: 116 | move = True 117 | elif e.type == MOUSEBUTTONUP: 118 | if e.button == 1: 119 | rotate = False 120 | elif e.button == 2: 121 | move = False 122 | elif e.type == MOUSEMOTION: 123 | i, j = e.rel 124 | if rotate: 125 | rotation[1] += i 126 | rotation[0] += j 127 | if move: 128 | translation[0] += i * .025 129 | translation[1] -= j * .025 130 | 131 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 132 | glLoadIdentity() 133 | 134 | glTranslate(translation[0], translation[1], translation[2]) 135 | glRotate(rotation[0], 1, 0, 0) 136 | glRotate(rotation[1], 0, 1, 0) 137 | 138 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 139 | glCallList(obj.gl_list) 140 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 141 | glCallList(obb_gl_list) 142 | 143 | display.flip() 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /bin/pyobb_demos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pboechat/pyobb/44d1f5fdd76982ee36bb11dc55ac00b28aaa64df/bin/pyobb_demos/__init__.py -------------------------------------------------------------------------------- /bin/pyobb_demos/objloader.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | import pygame 3 | from OpenGL.GL import * 4 | 5 | 6 | ######################################################################################################################## 7 | # copied from: http://www.pygame.org/wiki/OBJFileLoader 8 | ######################################################################################################################## 9 | def MTL(filename): 10 | contents = {} 11 | mtl = None 12 | for line in open(filename, "r"): 13 | if line.startswith('#'): 14 | continue 15 | values = line.split() 16 | if not values: 17 | continue 18 | if values[0] == 'newmtl': 19 | mtl = contents[values[1]] = {} 20 | elif mtl is None: 21 | raise ValueError("mtl file doesn't start with newmtl stmt") 22 | elif values[0] == 'map_Kd': 23 | # load the texture referred to by this declaration 24 | mtl[values[0]] = values[1] 25 | surf = pygame.image.load(mtl['map_Kd']) 26 | image = pygame.image.tostring(surf, 'RGBA', 1) 27 | ix, iy = surf.get_rect().size 28 | texid = mtl['texture_Kd'] = glGenTextures(1) 29 | glBindTexture(GL_TEXTURE_2D, texid) 30 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 31 | GL_LINEAR) 32 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, 33 | GL_LINEAR) 34 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ix, iy, 0, GL_RGBA, 35 | GL_UNSIGNED_BYTE, image) 36 | else: 37 | mtl[values[0]] = map(float, values[1:]) 38 | return contents 39 | 40 | 41 | class OBJ: 42 | def __init__(self, filename, swap_yz=False): 43 | self.vertices = [] 44 | self.normals = [] 45 | self.texcoords = [] 46 | self.faces = [] 47 | self.mtl = {} 48 | 49 | material = None 50 | for line in open(filename, "r"): 51 | if line.startswith('#'): 52 | continue 53 | values = line.split() 54 | if not values: 55 | continue 56 | if values[0] == 'v': 57 | v = list(map(float, values[1:4])) 58 | if swap_yz: 59 | v = v[0], v[2], v[1] 60 | self.vertices.append(v) 61 | elif values[0] == 'vn': 62 | vn = list(map(float, values[1:4])) 63 | if swap_yz: 64 | vn = vn[0], vn[2], vn[1] 65 | self.normals.append(vn) 66 | elif values[0] == 'vt': 67 | vt = list(map(float, values[1:3])) 68 | self.texcoords.append(vt) 69 | elif values[0] in ('usemtl', 'usemat'): 70 | material = values[1] 71 | elif values[0] == 'mtllib': 72 | self.mtl = MTL(values[1]) 73 | elif values[0] == 'f': 74 | vertices = [] 75 | texcoords = [] 76 | norms = [] 77 | face_elements = values[1:] 78 | if len(face_elements) > 3: 79 | raise Exception('unsupported polygonal face') 80 | for face_element in face_elements: 81 | w = face_element.split('/') 82 | vertices.append(int(w[0])) 83 | if len(w) >= 2 and len(w[1]) > 0: 84 | texcoords.append(int(w[1])) 85 | else: 86 | texcoords.append(0) 87 | if len(w) >= 3 and len(w[2]) > 0: 88 | norms.append(int(w[2])) 89 | else: 90 | norms.append(0) 91 | self.faces.append((vertices, norms, texcoords, material)) 92 | self.gl_list = glGenLists(1) 93 | glNewList(self.gl_list, GL_COMPILE) 94 | glEnable(GL_TEXTURE_2D) 95 | glFrontFace(GL_CCW) 96 | for vertices in self.faces: 97 | vertices, normals, texcoords, material = vertices 98 | if material is not None: 99 | mtl = self.mtl[material] 100 | if 'texture_Kd' in mtl: 101 | # use diffuse texmap 102 | glBindTexture(GL_TEXTURE_2D, mtl['texture_Kd']) 103 | else: 104 | # just use diffuse colour 105 | glColor(*mtl['Kd']) 106 | else: 107 | glColor3fv((1, 1, 1)) 108 | glBegin(GL_TRIANGLES) 109 | for i in range(len(vertices)): 110 | if normals[i] > 0: 111 | glNormal3fv(self.normals[normals[i] - 1]) 112 | if texcoords[i] > 0: 113 | glTexCoord2fv(self.texcoords[texcoords[i] - 1]) 114 | glVertex3fv(self.vertices[vertices[i] - 1]) 115 | glEnd() 116 | glDisable(GL_TEXTURE_2D) 117 | glEndList() 118 | -------------------------------------------------------------------------------- /bin/requirements.txt: -------------------------------------------------------------------------------- 1 | pyobb 2 | pygame 3 | PyOpenGL 4 | -------------------------------------------------------------------------------- /bin/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | def get_requirements(): 7 | with open('requirements.txt', 'r') as requirements_file: 8 | lines = requirements_file.readlines() 9 | requirements = [] 10 | for line in lines: 11 | if line.startswith('#'): 12 | continue 13 | requirements.append(line.strip()) 14 | return requirements 15 | 16 | 17 | setup(name='pyobb_demos', 18 | packages=['pyobb_demos'], 19 | version='0.0', 20 | install_requires=get_requirements(), 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'pyobb_2d_demo = pyobb_demos.2d_demo:main', 24 | 'pyobb_3d_demo = pyobb_demos.3d_demo:main' 25 | ] 26 | }, 27 | description='Python OBB Demos', 28 | author='Pedro Boechat', 29 | author_email='pboechat@gmail.com', 30 | url='https://github.com/pboechat/pyobb/pyobb_demos') 31 | -------------------------------------------------------------------------------- /pyobb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pboechat/pyobb/44d1f5fdd76982ee36bb11dc55ac00b28aaa64df/pyobb/__init__.py -------------------------------------------------------------------------------- /pyobb/obb.py: -------------------------------------------------------------------------------- 1 | from numpy import ndarray, array, asarray, dot, cross, cov, array, finfo, min as npmin, max as npmax 2 | from numpy.linalg import eigh, norm 3 | 4 | 5 | ######################################################################################################################## 6 | # adapted from: http://jamesgregson.blogspot.com/2011/03/latex-test.html 7 | ######################################################################################################################## 8 | class OBB: 9 | def __init__(self): 10 | self.rotation = None 11 | self.min = None 12 | self.max = None 13 | 14 | def transform(self, point): 15 | return dot(array(point), self.rotation) 16 | 17 | @property 18 | def centroid(self): 19 | return self.transform((self.min + self.max) / 2.0) 20 | 21 | @property 22 | def extents(self): 23 | return abs(self.transform((self.max - self.min) / 2.0)) 24 | 25 | @property 26 | def points(self): 27 | return [ 28 | # upper cap: ccw order in a right-hand system 29 | # rightmost, topmost, farthest 30 | self.transform((self.max[0], self.max[1], self.min[2])), 31 | # leftmost, topmost, farthest 32 | self.transform((self.min[0], self.max[1], self.min[2])), 33 | # leftmost, topmost, closest 34 | self.transform((self.min[0], self.max[1], self.max[2])), 35 | # rightmost, topmost, closest 36 | self.transform(self.max), 37 | # lower cap: cw order in a right-hand system 38 | # leftmost, bottommost, farthest 39 | self.transform(self.min), 40 | # rightmost, bottommost, farthest 41 | self.transform((self.max[0], self.min[1], self.min[2])), 42 | # rightmost, bottommost, closest 43 | self.transform((self.max[0], self.min[1], self.max[2])), 44 | # leftmost, bottommost, closest 45 | self.transform((self.min[0], self.min[1], self.max[2])), 46 | ] 47 | 48 | @classmethod 49 | def build_from_covariance_matrix(cls, covariance_matrix, points): 50 | if not isinstance(points, ndarray): 51 | points = array(points, dtype=float) 52 | assert points.shape[1] == 3 53 | 54 | obb = OBB() 55 | 56 | _, eigen_vectors = eigh(covariance_matrix) 57 | 58 | def try_to_normalize(v): 59 | n = norm(v) 60 | if n < finfo(float).resolution: 61 | raise ZeroDivisionError 62 | return v / n 63 | 64 | r = try_to_normalize(eigen_vectors[:, 0]) 65 | u = try_to_normalize(eigen_vectors[:, 1]) 66 | f = try_to_normalize(eigen_vectors[:, 2]) 67 | 68 | obb.rotation = array((r, u, f)).T 69 | 70 | # apply the rotation to all the position vectors of the array 71 | # TODO : this operation could be vectorized with tensordot 72 | p_primes = asarray([obb.rotation.dot(p) for p in points]) 73 | obb.min = npmin(p_primes, axis=0) 74 | obb.max = npmax(p_primes, axis=0) 75 | 76 | return obb 77 | 78 | @classmethod 79 | def build_from_triangles(cls, points, triangles): 80 | for point in points: 81 | if len(point) != 3: 82 | raise Exception('points have to have 3-elements') 83 | 84 | weighed_mean = array([0, 0, 0], dtype=float) 85 | area_sum = 0 86 | c00 = c01 = c02 = c11 = c12 = c22 = 0 87 | for i in range(0, len(triangles), 3): 88 | p = array(points[triangles[i]], dtype=float) 89 | q = array(points[triangles[i + 1]], dtype=float) 90 | r = array(points[triangles[i + 2]], dtype=float) 91 | mean = (p + q + r) / 3.0 92 | area = norm(cross((q - p), (r - p))) / 2.0 93 | weighed_mean += mean * area 94 | area_sum += area 95 | c00 += (9.0 * mean[0] * mean[0] + p[0] * p[0] + q[0] * q[0] + r[0] * r[0]) * (area / 12.0) 96 | c01 += (9.0 * mean[0] * mean[1] + p[0] * p[1] + q[0] * q[1] + r[0] * r[1]) * (area / 12.0) 97 | c02 += (9.0 * mean[0] * mean[2] + p[0] * p[2] + q[0] * q[2] + r[0] * r[2]) * (area / 12.0) 98 | c11 += (9.0 * mean[1] * mean[1] + p[1] * p[1] + q[1] * q[1] + r[1] * r[1]) * (area / 12.0) 99 | c12 += (9.0 * mean[1] * mean[2] + p[1] * p[2] + q[1] * q[2] + r[1] * r[2]) * (area / 12.0) 100 | 101 | weighed_mean /= area_sum 102 | c00 /= area_sum 103 | c01 /= area_sum 104 | c02 /= area_sum 105 | c11 /= area_sum 106 | c12 /= area_sum 107 | c22 /= area_sum 108 | 109 | c00 -= weighed_mean[0] * weighed_mean[0] 110 | c01 -= weighed_mean[0] * weighed_mean[1] 111 | c02 -= weighed_mean[0] * weighed_mean[2] 112 | c11 -= weighed_mean[1] * weighed_mean[1] 113 | c12 -= weighed_mean[1] * weighed_mean[2] 114 | c22 -= weighed_mean[2] * weighed_mean[2] 115 | 116 | covariance_matrix = ndarray(shape=(3, 3), dtype=float) 117 | covariance_matrix[0, 0] = c00 118 | covariance_matrix[0, 1] = c01 119 | covariance_matrix[0, 2] = c02 120 | covariance_matrix[1, 0] = c01 121 | covariance_matrix[1, 1] = c11 122 | covariance_matrix[1, 2] = c12 123 | covariance_matrix[2, 0] = c02 124 | covariance_matrix[1, 2] = c12 125 | covariance_matrix[2, 2] = c22 126 | 127 | return OBB.build_from_covariance_matrix(covariance_matrix, points) 128 | 129 | @classmethod 130 | def build_from_points(cls, points): 131 | if not isinstance(points, ndarray): 132 | points = array(points, dtype=float) 133 | assert points.shape[1] == 3, 'points have to have 3-elements' 134 | # no need to store the covariance matrix 135 | return OBB.build_from_covariance_matrix(cov(points, y=None, rowvar=0, bias=1), points) 136 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.12 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup(name='pyobb', 7 | packages=['pyobb'], 8 | version='1.0.2', 9 | install_requires=['numpy>=1.12'], 10 | description='Python OBB Implementation', 11 | author='Pedro Boechat', 12 | author_email='pboechat@gmail.com', 13 | url='https://github.com/pboechat/pyobb', 14 | download_url = 'https://github.com/pboechat/pyobb/archive/1.0.2.tar.gz', 15 | keywords = ['obb', 'computational-geometry', 'oriented-bounding-box']) 16 | -------------------------------------------------------------------------------- /tests/test_obb.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | from math import pi, cos, sin, sqrt, radians 3 | from pyobb.obb import OBB 4 | 5 | EPSILON = 0.025 6 | 7 | 8 | def tpl_cmp(a, b, epsilon=EPSILON): 9 | for aX, bX in zip(a, b): 10 | if abs(aX - bX) > epsilon: 11 | return False 12 | return True 13 | 14 | 15 | def render_to_png(filename, callback, obb, model_matrix=(1, 0, 0, 0, 16 | 0, 1, 0, 0, 17 | 0, 0, 1, 0, 18 | 0, 0, 0, 1)): 19 | from pygame import init, display, quit 20 | from pygame.constants import OPENGL, DOUBLEBUF 21 | from OpenGL.GL import glLightfv, glCullFace, glEnable, glShadeModel, glMatrixMode, glLoadIdentity, glClear, \ 22 | glLoadMatrixf, glPolygonMode, glCallList, glReadPixels, GL_LIGHT0, GL_POSITION, GL_AMBIENT, GL_DIFFUSE, \ 23 | GL_BACK, GL_LIGHTING, GL_COLOR_MATERIAL, GL_DEPTH_TEST, GL_SMOOTH, GL_PROJECTION, GL_MODELVIEW, \ 24 | GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_FRONT_AND_BACK, GL_FILL, GL_LINE, GL_BGR, GL_UNSIGNED_BYTE 25 | from OpenGL.GLU import gluPerspective 26 | from cv2 import imwrite 27 | from numpy import frombuffer, uint8 28 | init() 29 | viewport = (800, 600) 30 | display.set_mode(viewport, OPENGL | DOUBLEBUF) 31 | glLightfv(GL_LIGHT0, GL_POSITION, (0, -1, 0, 0)) 32 | glLightfv(GL_LIGHT0, GL_AMBIENT, (0.2, 0.2, 0.2, 1)) 33 | glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.5, 0.5, 0.5, 1)) 34 | glCullFace(GL_BACK) 35 | glEnable(GL_LIGHT0) 36 | glEnable(GL_LIGHTING) 37 | glEnable(GL_COLOR_MATERIAL) 38 | glEnable(GL_DEPTH_TEST) 39 | glShadeModel(GL_SMOOTH) 40 | glMatrixMode(GL_PROJECTION) 41 | glLoadIdentity() 42 | width, height = viewport 43 | gluPerspective(90.0, width / float(height), 0.1, 100.0) 44 | glEnable(GL_DEPTH_TEST) 45 | glMatrixMode(GL_MODELVIEW) 46 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 47 | glLoadIdentity() 48 | glLoadMatrixf(model_matrix) 49 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 50 | glCallList(callback()) 51 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 52 | glCallList(create_obb_gl_list(obb)) 53 | img_data = glReadPixels(0, 0, width, height, GL_BGR, GL_UNSIGNED_BYTE) 54 | img = frombuffer(img_data, dtype=uint8) 55 | img = img.reshape((height, width, 3)) 56 | imwrite(filename, img) 57 | quit() 58 | 59 | 60 | def create_obb_gl_list(obb): 61 | from OpenGL.GL import glGenLists, glNewList, glFrontFace, glBegin, glEnd, glEndList, glColor3fv, glVertex3fv, \ 62 | GL_CCW, GL_COMPILE, GL_LINES 63 | gl_list = glGenLists(1) 64 | glNewList(gl_list, GL_COMPILE) 65 | glFrontFace(GL_CCW) 66 | glBegin(GL_LINES) 67 | glColor3fv((1, 0, 0)) 68 | 69 | def input_vertex(x, y, z): 70 | glVertex3fv((obb.rotation[0][0] * x + obb.rotation[0][1] * y + obb.rotation[0][2] * z, 71 | obb.rotation[1][0] * x + obb.rotation[1][1] * y + obb.rotation[1][2] * z, 72 | obb.rotation[2][0] * x + obb.rotation[2][1] * y + obb.rotation[2][2] * z)) 73 | 74 | input_vertex(*obb.max) 75 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 76 | 77 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 78 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 79 | 80 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 81 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 82 | 83 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 84 | input_vertex(*obb.max) 85 | 86 | input_vertex(obb.max[0], obb.max[1], obb.max[2]) 87 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 88 | 89 | input_vertex(obb.max[0], obb.min[1], obb.max[2]) 90 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 91 | 92 | input_vertex(obb.min[0], obb.max[1], obb.max[2]) 93 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 94 | 95 | input_vertex(obb.min[0], obb.min[1], obb.max[2]) 96 | input_vertex(obb.min[0], obb.min[1], obb.min[2]) 97 | 98 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 99 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 100 | 101 | input_vertex(obb.max[0], obb.min[1], obb.min[2]) 102 | input_vertex(*obb.min) 103 | 104 | input_vertex(*obb.min) 105 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 106 | 107 | input_vertex(obb.min[0], obb.max[1], obb.min[2]) 108 | input_vertex(obb.max[0], obb.max[1], obb.min[2]) 109 | 110 | glEnd() 111 | glEndList() 112 | 113 | return gl_list 114 | 115 | 116 | def create_gl_list(shape): 117 | from OpenGL.GL import glGenLists, glNewList, glFrontFace, glBegin, glEnd, glEndList, glNormal3fv, glVertex3fv, \ 118 | GL_COMPILE, GL_CCW, GL_TRIANGLES 119 | vertices = shape['vertices'] 120 | normals = shape['normals'] 121 | indices = shape['indices'] 122 | gl_list = glGenLists(1) 123 | glNewList(gl_list, GL_COMPILE) 124 | glFrontFace(GL_CCW) 125 | glBegin(GL_TRIANGLES) 126 | for idx in indices: 127 | glNormal3fv(normals[idx]) 128 | glVertex3fv(vertices[idx]) 129 | glEnd() 130 | glEndList() 131 | return gl_list 132 | 133 | 134 | def sphere(radius, center, num_slices): 135 | theta_step = 2.0 * pi / (num_slices - 1) 136 | phi_step = pi / (num_slices - 1.0) 137 | theta = 0.0 138 | vertices = [] 139 | normals = [] 140 | for i in range(0, num_slices): 141 | cos_theta = cos(theta) 142 | sin_theta = sin(theta) 143 | phi = 0.0 144 | for j in range(0, num_slices): 145 | x = -sin(phi) * cos_theta 146 | y = -cos(phi) 147 | z = -sin(phi) * sin_theta 148 | n = sqrt(x * x + y * y + z * z) 149 | if n < 0.99 or n > 1.01: 150 | x /= n 151 | y /= n 152 | z /= n 153 | normals.append((x, y, z)) 154 | vertices.append((x * radius + center[0], 155 | y * radius + center[1], 156 | z * radius + center[2])) 157 | phi += phi_step 158 | theta += theta_step 159 | indices = [] 160 | for i in range(0, num_slices - 1): 161 | for j in range(0, num_slices - 1): 162 | base_idx = (i * num_slices + j) 163 | indices.append(base_idx) 164 | indices.append(base_idx + num_slices) 165 | indices.append(base_idx + num_slices + 1) 166 | indices.append(base_idx) 167 | indices.append(base_idx + num_slices + 1) 168 | indices.append(base_idx + 1) 169 | 170 | return {'radius': radius, 171 | 'center': center, 172 | 'num_slices': num_slices, 173 | 'vertices': vertices, 174 | 'normals': normals, 175 | 'indices': indices} 176 | 177 | 178 | def cube(size, T, R): 179 | vertices = [] 180 | normals = [] 181 | c1 = cos(radians(R[0])) 182 | c2 = cos(radians(R[1])) 183 | c3 = cos(radians(R[2])) 184 | s1 = sin(radians(R[0])) 185 | s2 = sin(radians(R[1])) 186 | s3 = sin(radians(R[2])) 187 | model = [c2 * c3, -c2 * s3, s2, T[0], 188 | c1 * s3 + c3 * s1 * s2, c1 * c3 - s1 * s2 * s3, -c2 * s1, T[1], 189 | s1 * s3 - c1 * c3 * s2, c3 * s1 + c1 * s2 * s3, c1 * c2, T[2]] 190 | 191 | def new_vertex(x, y, z): 192 | vertices.append((model[0] * x * size + model[1] * y * size + model[2] * z * size + model[3], 193 | model[4] * x * size + model[5] * y * size + model[6] * z * size + model[7], 194 | model[8] * x * size + model[9] * y * size + model[10] * z * size + model[11])) 195 | normals.append((x, y, z)) 196 | 197 | new_vertex(-1.0, -1.0, -1.0) 198 | new_vertex(-1.0, -1.0, 1.0) 199 | new_vertex(-1.0, 1.0, 1.0) 200 | new_vertex(1.0, 1.0, -1.0) 201 | new_vertex(-1.0, -1.0, -1.0) 202 | new_vertex(-1.0, 1.0, -1.0) 203 | new_vertex(1.0, -1.0, 1.0) 204 | new_vertex(-1.0, -1.0, -1.0) 205 | new_vertex(1.0, -1.0, -1.0) 206 | new_vertex(1.0, 1.0, -1.0) 207 | new_vertex(1.0, -1.0, -1.0) 208 | new_vertex(-1.0, -1.0, -1.0) 209 | new_vertex(-1.0, -1.0, -1.0) 210 | new_vertex(-1.0, 1.0, 1.0) 211 | new_vertex(-1.0, 1.0, -1.0) 212 | new_vertex(1.0, -1.0, 1.0) 213 | new_vertex(-1.0, -1.0, 1.0) 214 | new_vertex(-1.0, -1.0, -1.0) 215 | new_vertex(-1.0, 1.0, 1.0) 216 | new_vertex(-1.0, -1.0, 1.0) 217 | new_vertex(1.0, -1.0, 1.0) 218 | new_vertex(1.0, 1.0, 1.0) 219 | new_vertex(1.0, -1.0, -1.0) 220 | new_vertex(1.0, 1.0, -1.0) 221 | new_vertex(1.0, -1.0, -1.0) 222 | new_vertex(1.0, 1.0, 1.0) 223 | new_vertex(1.0, -1.0, 1.0) 224 | new_vertex(1.0, 1.0, 1.0) 225 | new_vertex(1.0, 1.0, -1.0) 226 | new_vertex(-1.0, 1.0, -1.0) 227 | new_vertex(1.0, 1.0, 1.0) 228 | new_vertex(-1.0, 1.0, -1.0) 229 | new_vertex(-1.0, 1.0, 1.0) 230 | new_vertex(1.0, 1.0, 1.0) 231 | new_vertex(-1.0, 1.0, 1.0) 232 | new_vertex(1.0, -1.0, 1.0) 233 | 234 | for i in range(0, len(normals), 3): 235 | v0 = normals[i] 236 | v1 = normals[i + 1] 237 | v2 = normals[i + 2] 238 | a = v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2] 239 | b = v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2] 240 | cross = (a[1] * b[2] - a[2] * b[1], 241 | a[2] * b[0] - a[0] * b[2], 242 | a[0] * b[1] - a[1] * b[0]) 243 | length = sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) 244 | if length > 0: 245 | normal = (cross[0] / length, cross[1] / length, cross[2] / length) 246 | else: 247 | # degenerate normal 248 | normal = (0, 0, 0) 249 | normals[i] = normal 250 | normals[i + 1] = normal 251 | normals[i + 2] = normal 252 | 253 | return {'size': size, 254 | 'T': T, 255 | 'R': R, 256 | 'vertices': vertices, 257 | 'normals': normals, 258 | 'indices': list(range(0, 36))} 259 | 260 | 261 | @mark.parametrize('sphere,idx', [(sphere(1, (0, 0, 0), 30), 0), 262 | (sphere(1, (1, 0, 0), 30), 1), 263 | (sphere(1, (-1, 0, 0), 30), 2), 264 | (sphere(1, (0, 1, 0), 30), 3), 265 | (sphere(1, (0, -1, 0), 30), 4), 266 | (sphere(1, (0, 0, 1), 30), 5), 267 | (sphere(1, (0, 0, -1), 30), 6)]) 268 | def test_obb_center(sphere, idx): 269 | obb = OBB.build_from_points(sphere['vertices']) 270 | # render_to_png('test_obb_center_%d.png' % idx, lambda: create_gl_list(sphere), obb, (1, 0, 0, 0, 271 | # 0, 1, 0, 0, 272 | # 0, 0, 1, 0, 273 | # 0, 0, -5, 1)) 274 | assert tpl_cmp(obb.centroid, sphere['center']) 275 | 276 | 277 | @mark.parametrize('sphere,idx,extents', [(sphere(1, (0, 0, 0), 30), 0, [1, 1, 1]), 278 | (sphere(1, (1, 0, 0), 30), 1, [1, 1, 1]), 279 | (sphere(1, (-1, 0, 0), 30), 2, [1, 1, 1])]) 280 | def test_obb_size(sphere, idx, extents): 281 | obb = OBB.build_from_points(sphere['vertices']) 282 | # render_to_png('test_obb_size_%d.png' % idx, lambda: create_gl_list(sphere), obb, (1, 0, 0, 0, 283 | # 0, 1, 0, 0, 284 | # 0, 0, 1, 0, 285 | # 0, 0, -5, 1)) 286 | assert tpl_cmp(obb.extents, extents) 287 | 288 | # @mark.parametrize('cube,u,v,w,idx', [(cube(1, (0, 0, 0), (0, 0, 0)), (0, 0, 1), (0, -1, 0), (-1, 0, 0), 0), 289 | # (cube(1, (0, 0, 0), (45, 45, 0)), (0, 0, 1), (0, 1, 0), (-1, 0, 0), 1), 290 | # (cube(1, (0, 0, 0), (-45, -45, 0)), (0, 1, 0), (0, 0, 1), (-1, 0, 0), 2)]) 291 | # def test_obb_axis(cube, u, v, w, idx): 292 | # obb = OBB.build_from_triangles(cube['vertices'], cube['indices']) 293 | # # render_to_png('test_obb_axis_%d.png' % idx, lambda: create_gl_list(cube), obb, (1, 0, 0, 0, 294 | # # 0, 1, 0, 0, 295 | # # 0, 0, 1, 0, 296 | # # 0, 0, -5, 1)) 297 | # assert tpl_cmp(obb.rotation[0], u) 298 | # assert tpl_cmp(obb.rotation[1], v) 299 | # assert tpl_cmp(obb.rotation[2], w) 300 | --------------------------------------------------------------------------------