├── imps ├── ply3.png ├── ply4.png ├── view1.png ├── view2.png ├── view3.png └── view4.png ├── run.sh ├── .gitignore ├── readme.md ├── split_copy_files.py ├── batch_voxelization.py ├── voxelization.py └── binvox_rw.py /imps/ply3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/ply3.png -------------------------------------------------------------------------------- /imps/ply4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/ply4.png -------------------------------------------------------------------------------- /imps/view1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/view1.png -------------------------------------------------------------------------------- /imps/view2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/view2.png -------------------------------------------------------------------------------- /imps/view3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/view3.png -------------------------------------------------------------------------------- /imps/view4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcoder2014/voxelization/HEAD/imps/view4.png -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | python batch_voxelization.py --data_dir ./ply_model/ --json_dir ./voxel_json/ --numpy_dir ./voxel_numpy/ --binvox_dir ./voxel_binvox/ --size0 96 --size1 96 --size2 96 --judeg_coef 0.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ply model 2 | 3 | # model files or export files 4 | ply_model/ 5 | voxel_binvox/ 6 | voxel_json/ 7 | voxel_numpy/ 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | .static_storage/ 64 | .media/ 65 | local_settings.py 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This project's aim is to voxelize the `*.ply` 3D model. 4 | But only for `ply` files, all [assimp support files ]( 5 | http://assimp.sourceforge.net/main_features_formats.html) can be voxelized. 6 | 7 | # Import formats 8 | Autodesk ( .fbx ), Collada ( .dae ), glTF ( .gltf, .glb ), Blender 3D ( .blend ), 9 | 3ds Max 3DS ( .3ds ), 3ds Max ASE ( .ase ), Wavefront Object ( .obj ), 10 | Industry Foundation Classes (IFC/Step) ( .ifc ), XGL ( .xgl,.zgl ), 11 | Stanford Polygon Library ( .ply ), AutoCAD DXF ( .dxf ), LightWave ( .lwo ), 12 | LightWave Scene ( .lws ), Modo ( .lxo ), Stereolithography ( .stl ), 13 | DirectX X ( .x ), AC3D ( .ac ), Milkshape 3D ( .ms3d ), TrueSpace ( .cob,.scn ) 14 | 15 | # Export voxel formats 16 | ## numpy 17 | A numpy array with shape of you appointed.Example: you give a vexel size of (100, 100, 100). 18 | It will return a numpy array with shape (100, 100, 100). 19 | Filename: `imagename.npy` 20 | 21 | When you need to import it, using `numpy.load(filename)`. 22 | 23 | ## JSON 24 | For more convenient usage, it export a json file. Just reshape the numpy array to 25 | (-1, ) . As it shown below: 26 | 27 | ```JSON 28 | '[0,1,0,1]' 29 | ``` 30 | Filename: `iamgename.json` 31 | 32 | ## binvox 33 | A format of [binvox](http://www.patrickmin.com/binvox/), using [binvox-rw-py 34 | ](https://github.com/dimatura/binvox-rw-py) to write out `.binvox` file to floder. 35 | 36 | Using this format, you can easily got the result in eyes with [viewvox](). 37 | 38 | ## Requires 39 | 40 | - [pyassimp 3](https://github.com/assimp/assimp/blob/master/port/PyAssimp/README.md) 41 | - [numpy](http://www.numpy.org/) 42 | - [python 2.7](https://www.python.org/downloads/) 43 | - [pyflann](https://github.com/primetang/pyflann) 44 | 45 | ## Usage 46 | 47 | ``` 48 | >>> import voxelization 49 | >>> voxelization.voxelization("134212_1.ply") 50 | ('Bounding box: ', 350.86337, 268.0675, 62.311089, 140.45639, 59.910782, -137.18449) 51 | ('x_edge: ', 1.0958697001139324, '\ny_edge: ', 1.0841495990753174, '\nz_edge: ', 0.99747787475585936, '\nedge: ', 1.0958697001139324) 52 | ('The mesh ', '0', ' has vertices: ', (53215, 3)) 53 | ('The mesh', '0', ' process successfully in ', 530.8458249568939, 's') 54 | calculate all meshes voxel finished! 55 | array([[[0, 0, 0, ..., 0, 0, 0], 56 | ...... 57 | [0, 0, 0, ..., 0, 0, 0]]], dtype=int8) 58 | >>> 59 | ``` 60 | 61 | The file `run.sh` is the usage of the script `batch_voxelization.py`. You can change your arguments, and run it just use command `bash run.sh` in linux. 62 | 63 | ## Show 64 | There is some works of the app. Those screenshot pictures are using [viewvox](http://www.patrickmin.com/viewvox/) Application. 65 | 66 | ![ply3]( imps/ply3.png ) 67 | ![view3]( imps/view3.png ) 68 | ![ply4]( imps/ply4.png ) 69 | ![view4]( imps/view4.png ) 70 | 71 | 72 | ## Requires Introduction 73 | 74 | ### PyAssimp 75 | PyAssimp can import and export many formats of 3-D models, very useful. 76 | 77 | ### numpy 78 | NumPy is the fundamental package for scientific computing with Python. 79 | 80 | ### PyFlann 81 | FLANN (Fast Library for Approximate Nearest Neighbors) is a library for 82 | performing fast approximate nearest neighbor searches. FLANN is written in 83 | the C++ programming language. FLANN can be easily used in many contexts 84 | through the C, MATLAB and Python bindings provided with the library. 85 | -------------------------------------------------------------------------------- /split_copy_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | split files under one floder into your determined parts. 4 | And copy them to your determined floders. 5 | """ 6 | import argparse 7 | import os 8 | import time 9 | import operator 10 | import math 11 | import shutil 12 | 13 | FLAGS = None 14 | 15 | def main( ): 16 | """ main function 17 | """ 18 | filenames = os.listdir( FLAGS.in_dir ) 19 | file_lists = div_list(filenames, FLAGS.split) 20 | 21 | start_time = time.time() 22 | file_list_length = len(file_lists) 23 | 24 | for part in range(file_list_length): 25 | 26 | part_list = file_lists[part] # part lists 27 | print(part_list) 28 | floder_path = os.path.join(FLAGS.out_dir, FLAGS.prefix + str(part)) 29 | os.makedirs(floder_path) #create_ 30 | 31 | part_time = time.time() 32 | part_list_length = len(part_list) 33 | 34 | for index in range(part_list_length): 35 | 36 | single_time = time.time() 37 | file_old = os.path.join(FLAGS.in_dir, part_list[index]) 38 | file_new = os.path.join(floder_path, part_list[index]) 39 | 40 | shutil.copy(file_old, file_new) 41 | print("File {0} -> {1} finished in {2} Sec. Total:{3}/{4} Part:{5}/{6}".format( 42 | file_old, file_new, time.time() - part_time, 43 | part+1, file_list_length, 44 | index+1, part_list_length )) 45 | 46 | print("Total: {0}/{1} finished, cost {2} Sec.".format( 47 | part + 1, file_list_length, time.time() - part_time )) 48 | 49 | print("All work finished, cost {0} Sec.".format( 50 | time.time() - start_time )) 51 | 52 | #split the arr into N chunks 53 | def div_list(ls,n): 54 | """divide list into n parts 55 | """ 56 | if not isinstance(ls,list) or not isinstance(n,int): 57 | return [] 58 | ls_len = len(ls) 59 | if n<=0 or 0==ls_len: 60 | return [] 61 | if n > ls_len: 62 | return [] 63 | elif n == ls_len: 64 | return [[i] for i in ls] 65 | else: 66 | j = ls_len/n 67 | k = ls_len%n 68 | ### j,j,j,...(前面有n-1个j),j+k 69 | #步长j,次数n-1 70 | ls_return = [] 71 | for i in xrange(0,(n-1)*j,j): 72 | ls_return.append(ls[i:i+j]) 73 | #算上末尾的j+k 74 | ls_return.append(ls[(n-1)*j:]) 75 | return ls_return 76 | 77 | 78 | if __name__ == '__main__': 79 | parser = argparse.ArgumentParser() 80 | parser.add_argument( 81 | '-i', 82 | '--in_dir', 83 | dest = 'in_dir', 84 | type = str, 85 | default='./', 86 | help = 'the input dir') 87 | 88 | parser.add_argument( 89 | '-o', 90 | '--out_dir', 91 | dest = 'out_dir', 92 | type = str, 93 | default='./', 94 | help = 'the output dir') 95 | 96 | parser.add_argument( 97 | '-s', 98 | '--split', 99 | type = int, 100 | dest = "split", 101 | default = 1, 102 | help = 'Split files into xx parts') 103 | 104 | parser.add_argument( 105 | '-p', 106 | '--prefix', 107 | dest = 'prefix', 108 | type = str, 109 | default = '', 110 | help = "the prefix add before each subdirs!" ) 111 | 112 | FLAGS, unparsed = parser.parse_known_args() 113 | print(FLAGS) 114 | main() # run main function 115 | -------------------------------------------------------------------------------- /batch_voxelization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """batch_voxelization 3 | """ 4 | 5 | import argparse 6 | import sys 7 | import os 8 | import voxelization 9 | import thread 10 | import time 11 | import numpy as np 12 | import operator 13 | 14 | FLAGS = None 15 | 16 | def main( ): 17 | """ the main program entrence 18 | """ 19 | 20 | print(FLAGS) 21 | filenames = os.listdir(FLAGS.data_dir) # get the filenames under data_dir 22 | print(filenames) 23 | 24 | #file_lists = div_list(filenames, FLAGS.threads) 25 | #print(file_lists) 26 | # locks = [] 27 | 28 | # for index in range(FLAGS.threads): 29 | #produce(index, file_lists[index]) 30 | # try: 31 | # lock = thread.allocate_lock() 32 | # lock.acquire() 33 | # locks.append(lock) 34 | # thread.start_new_thread( produce, (file_lists[index], id, lock, ) ) 35 | # except: 36 | # print("Error! thread", index, "unable to start") 37 | produce(filenames, "0") 38 | print("All work has finished!") 39 | 40 | def produce( filenames , id): 41 | """function produce 42 | Args: 43 | filenames: a list of filenames 44 | id: thread id 45 | """ 46 | # print("threads", id, filenames, "length", len(filenames)) 47 | print("threads:{0} ,file {1}, length: {2}".format( 48 | id, filenames, len(filenames))) 49 | length = len(filenames) 50 | for index in range(length): 51 | filename = filenames[index] 52 | if not operator.eq("ply", filename[filename.rfind(".")+1:len(filename)]): 53 | #print (filename, operator.eq("ply", filename[filename.rfind("."):len(filename)])) 54 | print (filename) 55 | continue 56 | #print("work in file", filename, "process bar", index,"/",length) 57 | print("work in file: {0}, Process bar: {1}/{2}".format( 58 | filename, index + 1, length )) 59 | voxelization.voxelization( 60 | FLAGS.data_dir + filename, 61 | outputJsonPath = FLAGS.json_dir, 62 | outputNumpyPath = FLAGS.numpy_dir, 63 | outputBinvoxPath = FLAGS.binvox_dir, 64 | coef = FLAGS.judge_coef, 65 | size = (FLAGS.size0, FLAGS.size1, FLAGS.size2)) 66 | 67 | 68 | if __name__ == '__main__': 69 | parser = argparse.ArgumentParser() 70 | parser.add_argument( 71 | '--data_dir', 72 | type = str, 73 | default='./', 74 | help = 'the relative path to the *.ply model files floder') 75 | 76 | parser.add_argument( 77 | '--json_dir', 78 | type = str, 79 | default="./voxel_json/", 80 | help = 'the path to save exported json files' ) 81 | 82 | parser.add_argument( 83 | '--numpy_dir', 84 | type = str, 85 | default = './voxel_numpy/', 86 | help = 'the path to save exported numpy files' ) 87 | 88 | parser.add_argument( 89 | '--binvox_dir', 90 | type = str, 91 | default = './voxel_binvox/', 92 | help = 'the path to save exported binvox files' ) 93 | 94 | parser.add_argument( 95 | "--judge_coef", 96 | type = float, 97 | default = 1.0, 98 | help = "larger the finger is, thicker the voxel model is" ) 99 | 100 | parser.add_argument( 101 | '--size0', 102 | type = int, 103 | default = 192, 104 | help = "the first num of size (192, 192, 200)" ) 105 | 106 | parser.add_argument( 107 | '--size1', 108 | type = int, 109 | default = 192, 110 | help = "the second num of size (192, 192, 200)" ) 111 | 112 | parser.add_argument( 113 | '--size2', 114 | type = int, 115 | default = 200, 116 | help = "the third num of size (192, 192, 200)" ) 117 | 118 | FLAGS, unparsed = parser.parse_known_args() 119 | main() # run main function 120 | -------------------------------------------------------------------------------- /voxelization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions that can voxelization the *.ply files 4 | python 2.7 5 | Author: Chaoqun Jiang 6 | Home Page: http://mcoder.cc 7 | """ 8 | 9 | import pyassimp 10 | import numpy as np 11 | import operator 12 | import pyflann 13 | import json 14 | import binvox_rw 15 | import time 16 | import os 17 | 18 | def voxelization( 19 | filename, 20 | outputJsonPath = '../voxel_json/', 21 | outputNumpyPath = '../voxel_numpy/', 22 | outputBinvoxPath = '../voxel_binvox/', 23 | coef = 1.0, 24 | size = (192,192,200)): 25 | """ function voxelization 26 | This function load a *.ply model file, and convert it into a voxel. 27 | And export in two formats. 28 | 29 | numpy formats: just use numpy import, a array has shape (192, 192, 200) 30 | json format: a numpy format reshape to (-1,) and attribute name is 'array' 31 | Args: 32 | filename: a relative file path to the *.ply file 33 | outputJsonPath: a relative floder path to save voxel in json format 34 | outputNumpyPath: a relative floder path to save voxel in numpy format 35 | Note: The directory should already be created. Or it will throw IOError 36 | outputBinvoxPath: a relative floder path to save in binvox format 37 | coef: used to judge if the point is 1 or 0 38 | size: a tuple with 3 integer, default is (192, 192, 200) 39 | Return: 40 | None: if no voxel has calculated, return None 41 | numpy.ndarray: if the voxel has been calculated, return ndarray 42 | """ 43 | if len(size) != 3: 44 | print("The argument \" size \" should has three integer") 45 | return 46 | 47 | scene = pyassimp.load(filename) # import scene 48 | meshes_count = len(scene.meshes) # the count of meshes 49 | if meshes_count < 1: 50 | print("Error! The model file has no meshes in it") 51 | return 52 | 53 | voxel_width = size[0] 54 | voxel_height = size[1] 55 | voxel_length = size[2] 56 | 57 | voxel = np.zeros( shape = (voxel_width, voxel_height, voxel_length), 58 | dtype = np.int8) # Creat a zeros ndarray 59 | print("Program is manipulating model: ", filename) 60 | print("Program will create voxel in shape", size) 61 | 62 | boundingbox = _getBoundingBox(scene) # get the bounding box of scene 63 | 64 | # calculate each voxel's edge length 65 | center = np.array( [ (boundingbox[0] + boundingbox[3]) / 2, 66 | (boundingbox[1] + boundingbox[4]) / 2, 67 | (boundingbox[2] + boundingbox[5]) /2] ) 68 | 69 | x_edge = (boundingbox[0] - boundingbox[3]) / voxel_width 70 | y_edge = (boundingbox[1] - boundingbox[4]) / voxel_height 71 | z_edge = (boundingbox[2] - boundingbox[5]) / voxel_length 72 | edge = max(x_edge, y_edge, z_edge) # use the max as edge 73 | print ("x_edge: {0}, y_edge: {1}, z_edge: {2}, edge: {3}".format( 74 | x_edge, y_edge, z_edge, edge)) 75 | 76 | # set the (voxel_width // 2, voxel_height // 2, voxel_length // 2)'s 77 | # position is center. So we can get other voxel box's voxel box. 78 | # At here, we calculate the start voxel box's center position. 79 | start = center - np.array([voxel_width // 2 * edge, 80 | voxel_height // 2 * edge, voxel_length // 2 * edge]) 81 | 82 | #print("center", center, "start", start) 83 | print("center: {0}, staet: {1}".format(center, start)) 84 | 85 | for index in range(meshes_count): 86 | _meshVoxel(start, edge, scene.meshes[index], voxel, coef, str(index)) 87 | print("calculate all meshes voxel finished!") 88 | 89 | # save voxel files 90 | _saveVoxel(filename, 91 | outputJsonPath, outputNumpyPath, outputBinvoxPath, voxel) 92 | return voxel 93 | 94 | def _getBoundingBox(scene): 95 | """give a assimp scene, get it bounding box 96 | It will bounding all meshes in the mesh. 97 | Args: 98 | scene: assimp scene 99 | Returns: 100 | bounding box ( xmax, ymax, zmax, xmin, ymin, zmin ) 101 | 6 num represent 6 faces. 102 | """ 103 | if len(scene.meshes) == 0: 104 | print("scene's meshes attribute has no mesh") 105 | return (0,0,0,0,0,0) 106 | 107 | mesh_1 = scene.meshes[0] 108 | xmax, ymax, zmax = np.amax( mesh_1.vertices, axis = 0 ) 109 | xmin, ymin, zmin = np.amin( mesh_1.vertices, axis = 0 ) 110 | 111 | for index in range(1,len(scene.meshes)): 112 | mesh_t = scene.meshes[index] 113 | xmax_t, ymax_t, zmax_t = np.amax( mesh_t.vertices, axis = 0) 114 | xmin_t, ymin_t, zmin_t = np.amin( mesh_t.vertices, axis = 0) 115 | 116 | if xmax_t > xmax: xmax = xmax_t 117 | if ymax_t > ymax: ymax = ymax_t 118 | if zmax_t > zmax: zmax = zmax_t 119 | if xmin_t < xmin: xmin = xmin_t 120 | if ymin_t < ymin: ymin = ymin_t 121 | if zmin_t < zmin: zmin = zmin_t 122 | 123 | # print("Bounding box: ",xmax, ymax, zmax, xmin, ymin, zmin) 124 | print("Bounding box: xmax: {0}, ymax: {1}, zmax:{2}, xmin: {3}, ymin: {4}, zmin: {5}".format( 125 | xmax, ymax, zmax, xmin, ymin, zmin)) 126 | return (xmax, ymax, zmax, xmin, ymin, zmin) 127 | 128 | def _meshVoxel(startpoint, edge, mesh, voxel, coef = 1.0, str = "0"): 129 | """ mesh voxel function 130 | change numpy.ndarray's 0 to 1 acounding to mesh and scene'bounding box 131 | Args: 132 | startpoint: numpy.ndarray with shape of (3,) 133 | edge: the voxel box's edge length 134 | mesh: pyassimp mesh 135 | voxel: numpy.ndarray 136 | coef: used to judge if this point is 1 137 | str: the string you want to split each mesh 138 | """ 139 | vertices = mesh.vertices # np.array n x 3 140 | 141 | #print("The mesh ", str," has vertices: ", vertices.shape) 142 | print("The mesh {0} has vertices {1}".format(str, vertices.shape)) 143 | 144 | # KDtree 145 | flann = pyflann.FLANN() # create a FLANN object 146 | params = flann.build_index(vertices, algorithm = "kdtree", trees = 4) 147 | 148 | # iterate to calculate the voxel value 149 | # if there is a point close to the center, there is 1, otherwise, no changes 150 | width, height, length = voxel.shape 151 | start_time = time.time() 152 | landmark = coef * edge 153 | 154 | for x in range(width): 155 | for y in range(height): 156 | for z in range(length): 157 | # for each voxel center 158 | voxel_center = np.array([[ 159 | startpoint[0] + x * edge, 160 | startpoint[1] + y * edge, 161 | startpoint[2] + z * edge]],dtype = np.float32) 162 | result, dists = flann.nn_index(voxel_center, 1, 163 | checks = params["checks"]) 164 | index = result[0] 165 | vertex = vertices[index,:] # get nearest neighbor 166 | distance = np.sqrt(((vertex - voxel_center) ** 2).sum()) 167 | 168 | if distance <= landmark: 169 | voxel[x,y,z] = 1 170 | 171 | # print("The mesh" + str +" process successfully in " , 172 | # (time.time() - start_time), " s") 173 | print("The mesh {0} process successfully in {1}s".format( 174 | str, (time.time() - start_time))) 175 | 176 | def _saveVoxel(filename, 177 | outputJsonPath, outputNumpyPath, outputBinvoxPath, voxel): 178 | """ save voxel 179 | Save the voxel into file. 180 | Args: 181 | filename: the filename of import. 182 | outputJsonPath: path to save json. 183 | outputNumpyPath: path to save numpy. 184 | outputBinvoxPath: path to save binvox. 185 | voxel: numpy.ndarray 186 | """ 187 | startPoint = 0 188 | if filename.rfind("/") != -1: 189 | startPoint = filename.rfind("/") + 1 190 | 191 | filename = filename[startPoint:filename.rfind('.')] # cut the format end 192 | # save npy 193 | #voxel.tofile(outputNumpyPath + filename + ".numpy") 194 | np.save(os.path.join( outputNumpyPath, filename ) + ".npy", voxel) 195 | 196 | # save binvox 197 | bool_voxel = voxel.astype(np.bool) 198 | binvox = binvox_rw.Voxels( 199 | data = bool_voxel, 200 | dims = list(voxel.shape), 201 | translate = [0.0, 0.0, 0.0], 202 | scale = 1.0, 203 | axis_order = 'xzy') 204 | fp = open(os.path.join( outputBinvoxPath, filename ) + ".binvox", 'wb+') 205 | fp.truncate() 206 | binvox.write(fp) 207 | fp.close() 208 | 209 | # save json 210 | array = voxel.reshape(-1,) 211 | json_str = json.dumps(array.tolist()) 212 | json_file = open(os.path.join( outputJsonPath, filename ) + ".json", "w+") 213 | json_file.truncate() # 清空当前文件的内容 214 | json_file.write(json_str) 215 | json_file.close() 216 | -------------------------------------------------------------------------------- /binvox_rw.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Daniel Maturana 2 | # This file is part of binvox-rw-py. 3 | # 4 | # binvox-rw-py is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # binvox-rw-py is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with binvox-rw-py. If not, see . 16 | # 17 | 18 | """ 19 | Binvox to Numpy and back. 20 | 21 | 22 | >>> import numpy as np 23 | >>> import binvox_rw 24 | >>> with open('chair.binvox', 'rb') as f: 25 | ... m1 = binvox_rw.read_as_3d_array(f) 26 | ... 27 | >>> m1.dims 28 | [32, 32, 32] 29 | >>> m1.scale 30 | 41.133000000000003 31 | >>> m1.translate 32 | [0.0, 0.0, 0.0] 33 | >>> with open('chair_out.binvox', 'wb') as f: 34 | ... m1.write(f) 35 | ... 36 | >>> with open('chair_out.binvox', 'rb') as f: 37 | ... m2 = binvox_rw.read_as_3d_array(f) 38 | ... 39 | >>> m1.dims==m2.dims 40 | True 41 | >>> m1.scale==m2.scale 42 | True 43 | >>> m1.translate==m2.translate 44 | True 45 | >>> np.all(m1.data==m2.data) 46 | True 47 | 48 | >>> with open('chair.binvox', 'rb') as f: 49 | ... md = binvox_rw.read_as_3d_array(f) 50 | ... 51 | >>> with open('chair.binvox', 'rb') as f: 52 | ... ms = binvox_rw.read_as_coord_array(f) 53 | ... 54 | >>> data_ds = binvox_rw.dense_to_sparse(md.data) 55 | >>> data_sd = binvox_rw.sparse_to_dense(ms.data, 32) 56 | >>> np.all(data_sd==md.data) 57 | True 58 | >>> # the ordering of elements returned by numpy.nonzero changes with axis 59 | >>> # ordering, so to compare for equality we first lexically sort the voxels. 60 | >>> np.all(ms.data[:, np.lexsort(ms.data)] == data_ds[:, np.lexsort(data_ds)]) 61 | True 62 | """ 63 | 64 | import numpy as np 65 | 66 | class Voxels(object): 67 | """ Holds a binvox model. 68 | data is either a three-dimensional numpy boolean array (dense representation) 69 | or a two-dimensional numpy float array (coordinate representation). 70 | 71 | dims, translate and scale are the model metadata. 72 | 73 | dims are the voxel dimensions, e.g. [32, 32, 32] for a 32x32x32 model. 74 | 75 | scale and translate relate the voxels to the original model coordinates. 76 | 77 | To translate voxel coordinates i, j, k to original coordinates x, y, z: 78 | 79 | x_n = (i+.5)/dims[0] 80 | y_n = (j+.5)/dims[1] 81 | z_n = (k+.5)/dims[2] 82 | x = scale*x_n + translate[0] 83 | y = scale*y_n + translate[1] 84 | z = scale*z_n + translate[2] 85 | 86 | """ 87 | 88 | def __init__(self, data, dims, translate, scale, axis_order): 89 | self.data = data 90 | self.dims = dims 91 | self.translate = translate 92 | self.scale = scale 93 | assert (axis_order in ('xzy', 'xyz')) 94 | self.axis_order = axis_order 95 | 96 | def clone(self): 97 | data = self.data.copy() 98 | dims = self.dims[:] 99 | translate = self.translate[:] 100 | return Voxels(data, dims, translate, self.scale, self.axis_order) 101 | 102 | def write(self, fp): 103 | write(self, fp) 104 | 105 | def read_header(fp): 106 | """ Read binvox header. Mostly meant for internal use. 107 | """ 108 | line = fp.readline().strip() 109 | if not line.startswith(b'#binvox'): 110 | raise IOError('Not a binvox file') 111 | dims = list(map(int, fp.readline().strip().split(b' ')[1:])) 112 | translate = list(map(float, fp.readline().strip().split(b' ')[1:])) 113 | scale = list(map(float, fp.readline().strip().split(b' ')[1:]))[0] 114 | line = fp.readline() 115 | return dims, translate, scale 116 | 117 | def read_as_3d_array(fp, fix_coords=True): 118 | """ Read binary binvox format as array. 119 | 120 | Returns the model with accompanying metadata. 121 | 122 | Voxels are stored in a three-dimensional numpy array, which is simple and 123 | direct, but may use a lot of memory for large models. (Storage requirements 124 | are 8*(d^3) bytes, where d is the dimensions of the binvox model. Numpy 125 | boolean arrays use a byte per element). 126 | 127 | Doesn't do any checks on input except for the '#binvox' line. 128 | """ 129 | dims, translate, scale = read_header(fp) 130 | raw_data = np.frombuffer(fp.read(), dtype=np.uint8) 131 | # if just using reshape() on the raw data: 132 | # indexing the array as array[i,j,k], the indices map into the 133 | # coords as: 134 | # i -> x 135 | # j -> z 136 | # k -> y 137 | # if fix_coords is true, then data is rearranged so that 138 | # mapping is 139 | # i -> x 140 | # j -> y 141 | # k -> z 142 | values, counts = raw_data[::2], raw_data[1::2] 143 | data = np.repeat(values, counts).astype(np.bool) 144 | data = data.reshape(dims) 145 | if fix_coords: 146 | # xzy to xyz TODO the right thing 147 | data = np.transpose(data, (0, 2, 1)) 148 | axis_order = 'xyz' 149 | else: 150 | axis_order = 'xzy' 151 | return Voxels(data, dims, translate, scale, axis_order) 152 | 153 | def read_as_coord_array(fp, fix_coords=True): 154 | """ Read binary binvox format as coordinates. 155 | 156 | Returns binvox model with voxels in a "coordinate" representation, i.e. an 157 | 3 x N array where N is the number of nonzero voxels. Each column 158 | corresponds to a nonzero voxel and the 3 rows are the (x, z, y) coordinates 159 | of the voxel. (The odd ordering is due to the way binvox format lays out 160 | data). Note that coordinates refer to the binvox voxels, without any 161 | scaling or translation. 162 | 163 | Use this to save memory if your model is very sparse (mostly empty). 164 | 165 | Doesn't do any checks on input except for the '#binvox' line. 166 | """ 167 | dims, translate, scale = read_header(fp) 168 | raw_data = np.frombuffer(fp.read(), dtype=np.uint8) 169 | 170 | values, counts = raw_data[::2], raw_data[1::2] 171 | 172 | sz = np.prod(dims) 173 | index, end_index = 0, 0 174 | end_indices = np.cumsum(counts) 175 | indices = np.concatenate(([0], end_indices[:-1])).astype(end_indices.dtype) 176 | 177 | values = values.astype(np.bool) 178 | indices = indices[values] 179 | end_indices = end_indices[values] 180 | 181 | nz_voxels = [] 182 | for index, end_index in zip(indices, end_indices): 183 | nz_voxels.extend(range(index, end_index)) 184 | nz_voxels = np.array(nz_voxels) 185 | # TODO are these dims correct? 186 | # according to docs, 187 | # index = x * wxh + z * width + y; // wxh = width * height = d * d 188 | 189 | x = nz_voxels / (dims[0]*dims[1]) 190 | zwpy = nz_voxels % (dims[0]*dims[1]) # z*w + y 191 | z = zwpy / dims[0] 192 | y = zwpy % dims[0] 193 | if fix_coords: 194 | data = np.vstack((x, y, z)) 195 | axis_order = 'xyz' 196 | else: 197 | data = np.vstack((x, z, y)) 198 | axis_order = 'xzy' 199 | 200 | #return Voxels(data, dims, translate, scale, axis_order) 201 | return Voxels(np.ascontiguousarray(data), dims, translate, scale, axis_order) 202 | 203 | def dense_to_sparse(voxel_data, dtype=np.int): 204 | """ From dense representation to sparse (coordinate) representation. 205 | No coordinate reordering. 206 | """ 207 | if voxel_data.ndim!=3: 208 | raise ValueError('voxel_data is wrong shape; should be 3D array.') 209 | return np.asarray(np.nonzero(voxel_data), dtype) 210 | 211 | def sparse_to_dense(voxel_data, dims, dtype=np.bool): 212 | if voxel_data.ndim!=2 or voxel_data.shape[0]!=3: 213 | raise ValueError('voxel_data is wrong shape; should be 3xN array.') 214 | if np.isscalar(dims): 215 | dims = [dims]*3 216 | dims = np.atleast_2d(dims).T 217 | # truncate to integers 218 | xyz = voxel_data.astype(np.int) 219 | # discard voxels that fall outside dims 220 | valid_ix = ~np.any((xyz < 0) | (xyz >= dims), 0) 221 | xyz = xyz[:,valid_ix] 222 | out = np.zeros(dims.flatten(), dtype=dtype) 223 | out[tuple(xyz)] = True 224 | return out 225 | 226 | #def get_linear_index(x, y, z, dims): 227 | #""" Assuming xzy order. (y increasing fastest. 228 | #TODO ensure this is right when dims are not all same 229 | #""" 230 | #return x*(dims[1]*dims[2]) + z*dims[1] + y 231 | 232 | def write(voxel_model, fp): 233 | """ Write binary binvox format. 234 | 235 | Note that when saving a model in sparse (coordinate) format, it is first 236 | converted to dense format. 237 | 238 | Doesn't check if the model is 'sane'. 239 | 240 | """ 241 | if voxel_model.data.ndim==2: 242 | # TODO avoid conversion to dense 243 | dense_voxel_data = sparse_to_dense(voxel_model.data, voxel_model.dims) 244 | else: 245 | dense_voxel_data = voxel_model.data 246 | 247 | fp.write('#binvox 1\n') 248 | fp.write('dim '+' '.join(map(str, voxel_model.dims))+'\n') 249 | fp.write('translate '+' '.join(map(str, voxel_model.translate))+'\n') 250 | fp.write('scale '+str(voxel_model.scale)+'\n') 251 | fp.write('data\n') 252 | if not voxel_model.axis_order in ('xzy', 'xyz'): 253 | raise ValueError('Unsupported voxel model axis order') 254 | 255 | if voxel_model.axis_order=='xzy': 256 | voxels_flat = dense_voxel_data.flatten() 257 | elif voxel_model.axis_order=='xyz': 258 | voxels_flat = np.transpose(dense_voxel_data, (0, 2, 1)).flatten() 259 | 260 | # keep a sort of state machine for writing run length encoding 261 | state = voxels_flat[0] 262 | ctr = 0 263 | for c in voxels_flat: 264 | if c==state: 265 | ctr += 1 266 | # if ctr hits max, dump 267 | if ctr==255: 268 | fp.write(chr(state)) 269 | fp.write(chr(ctr)) 270 | ctr = 0 271 | else: 272 | # if switch state, dump 273 | fp.write(chr(state)) 274 | fp.write(chr(ctr)) 275 | state = c 276 | ctr = 1 277 | # flush out remainders 278 | if ctr > 0: 279 | fp.write(chr(state)) 280 | fp.write(chr(ctr)) 281 | 282 | if __name__ == '__main__': 283 | import doctest 284 | doctest.testmod() 285 | --------------------------------------------------------------------------------