diff --git a/__init__.py b/__init__.py index e7cecae..e85f5f8 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ """BlenderPhotonics - a Blender addon for 3-D mesh generation and Monte Carlo simulation * Authors: (c) 2021-2022 Qianqian Fang - (c) 2021 Yuxuan Zhang + (c) 2021 Yuxuan Zhang * License: GNU General Public License V3 or later (GPLv3) * Version: v2022 (v0.6.0) * Website: http://mcx.space/bp @@ -22,7 +22,7 @@ users to create sophisticated optical simulations needed for a wide range of biophotonics applications. -Installing this module via Blender menu "Edit\Preference\Add-ons\Install..." +Installing this module via Blender menu "Edit\\Preference\\Add-ons\\Install..." enables the BlenderPhotonics panel. The BlenderPhotonics panel contains the following 4 submodules: @@ -38,7 +38,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -54,6 +55,17 @@ """ +import bpy +from .ui import BlenderPhotonics_UI +from .blender2mesh import scene2mesh +from .mesh2blender import mesh2scene +from .obj2surf import object2surf +from .runmmc import runmmc +from .niifile import niifile +from .nii2mesh import nii2mesh +from bpy.props import PointerProperty + + bl_info = { "name": "BlenderPhotonics", "author": "(c) 2021 Yuxuan (Victor) Zhang, (c) 2021 Qianqian Fang", @@ -61,20 +73,13 @@ "blender": (2, 82, 0), # min blender version "location": "Layout,UI", "description": "An integrated 3D mesh generation and Monte Carlo photon transport simulation environment", - "warning": "This plug-in requires the preinstallation of Iso2Mesh (http://iso2mesh.sf.net) and MMCLAB (http://mcx.space)", + "warning": "This plug-in requires the preinstallation of Iso2Mesh (http://iso2mesh.sf.net) and " + "MMCLAB (http://mcx.space)", "doc_url": "https://github.com/COTILab/BlenderPhotonics", "tracker_url": "https://github.com/COTILab/BlenderPhotonics/issues", "category": "User Interface", } -import bpy -from .ui import BlenderPhotonics_UI -from .blender2mesh import scene2mesh -from .mesh2blender import mesh2scene -from .obj2surf import object2surf -from .runmmc import runmmc -from .niifile import niifile -from .nii2mesh import nii2mesh -from bpy.props import PointerProperty + def register(): print("Registering BlenderPhotonics") @@ -98,4 +103,3 @@ def unregister(): bpy.utils.unregister_class(runmmc) bpy.utils.unregister_class(BlenderPhotonics_UI) del bpy.types.Scene.blender_photonics - diff --git a/backend.py b/backend.py new file mode 100644 index 0000000..f191045 --- /dev/null +++ b/backend.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod, abstractproperty +from collections import namedtuple +from pathlib import Path + + +PackageBrief = namedtuple('PackageBrief', ['name', 'probe_fun']) + + +M_SCRIPTS = Path(__file__).parent / 'script' +OUTPUT_DIR = Path(__file__).parent / 'out' +GATEWAY_DIR = Path(__file__).parent / 'gateway' +PACKAGES = [PackageBrief(name='jsonlab', probe_fun='loadjson'), + PackageBrief(name='iso2mesh', probe_fun='s2m')] + + +class BackendABC(ABC): + @abstractmethod + def __init__(self, engine_type: str): + pass + + @ + +class OctaveBackend(BackendABC): + def __init__(self, engine_type: str): + self.engine_type = engine_type.lower() + if self.engine_type == 'octave': + self.EngineError2 = self.EngineError1 = __import__('oct2py.utils', globals(), locals(), ['Oct2PyError'], 0) + self.engine = __import__('oct2py', globals(), locals(), ['Oct2Py'], 0) + else: + matlab_stuff = __import__('matlab.engine', globals(), locals(), ['MatlabEngine', + 'MatlabExecutionError', + 'RejectedExecutionError'], 0) + self.EngineError1 = matlab_stuff.MatlabExecutionError + self.EngineError1 = matlab_stuff.RejectedExecutionError + self.engine = matlab_stuff.MatlabEngine + + def __getattr__(self, item): + def inner(*args, nout: int = 0): + with self.engine() as engine: + # import + engine.addpath(M_SCRIPTS) + try: + packages_to_load = filter(lambda p: p['name'] in PACKAGES, engine.pkg('list').tolist()[0]) + for package in packages_to_load: + engine.addpath(str(Path(package['dir']))) + except IndexError: + raise ImportError('No local packages installed for Octave') # TODO: another exception type + try: + return engine.feval(item, *args, nargout=nout) + except self.EngineError1: + return engine.feval(item, *args, nout=nout) + return inner + + +def get_backend(engine_type='octave'): + return Backend(engine_type) diff --git a/blender2mesh.py b/blender2mesh.py index 604592a..ae1c33c 100644 --- a/blender2mesh.py +++ b/blender2mesh.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -26,87 +27,93 @@ import jdata as jd import os from bpy.utils import register_class, unregister_class +from .backend import get_backend from .utils import * -g_maxvol=1.0 -g_keepratio=1.0 -g_mergetol=0 -g_dorepair=False -g_onlysurf=False -g_convtri=True -g_endstep='9' -g_tetgenopt="" -enum_endstep=[('1','Step 1: Convert objects to mesh','Convert objects to mesh'), - ('2','Step 2: Join all objects','Join all objects'), - ('3','Step 3: Intersect objects','Intersect objects'), - ('4','Step 4: Convert to triangles','Merge all visible objects, perform intersection and convert to N-gon or triangular mesh'), - ('5','Step 5: Export to JMesh','Export the scene to a human-readable universal data exchange file encoded in the JSON format based on the JMesh specification (see http://neurojson.org)'), - ('6','Step 6: Run Iso2Mesh and load mesh','Output tetrahedral mesh using Iso2Mesh (http://iso2mesh.sf.net)'), - ('9','Run all steps','Create 3-D tetrahedral meshes using Iso2Mesh and Octave (please save your Blender session first!)')] +G_MAXVOL = 1.0 +G_KEEPRATIO = 1.0 +G_MERGETOL = 0 +G_DOREPAIR = False +G_ONLYSURF = False +G_CONVTRI = True +G_ENDSTEP = '9' +G_TETGENOPT = "" +ENUM_ENDSTEP = [('1', 'Step 1: Convert objects to mesh', 'Convert objects to mesh'), + ('2', 'Step 2: Join all objects', 'Join all objects'), + ('3', 'Step 3: Intersect objects', 'Intersect objects'), + ('4', 'Step 4: Convert to triangles', + 'Merge all visible objects, perform intersection and convert to N-gon or triangular mesh'), + ('5', 'Step 5: Export to JMesh', + 'Export the scene to a human-readable universal data exchange file encoded in the JSON format based ' + 'on the JMesh specification (see http://neurojson.org)'), + ('6', 'Step 6: Run Iso2Mesh and load mesh', + 'Output tetrahedral mesh using Iso2Mesh (http://iso2mesh.sf.net)'), + ('9', 'Run all steps', + 'Create 3-D tetrahedral meshes using Iso2Mesh and Octave (please save your Blender session first!)')] + class scene2mesh(bpy.types.Operator): bl_label = 'Convert scene to tetra mesh' bl_description = "Create 3-D tetrahedral meshes using Iso2Mesh and Octave (please save your Blender session first!)" bl_idname = 'blenderphotonics.create3dmesh' - # creat a interface to set uesrs' model parameter. + # create an interface to set user's model parameter. bl_options = {"REGISTER", "UNDO"} - maxvol: bpy.props.FloatProperty(default=g_maxvol, name="Maximum tet volume") - keepratio: bpy.props.FloatProperty(default=g_keepratio,name="Fraction edge kept (0-1)") - mergetol: bpy.props.FloatProperty(default=g_mergetol,name="Tolerance to merge nodes (0 to disable)") - dorepair: bpy.props.BoolProperty(default=g_dorepair,name="Repair mesh (single object only)") - onlysurf: bpy.props.BoolProperty(default=g_onlysurf,name="Return triangular surface mesh only (no tetrahedral mesh)") - convtri: bpy.props.BoolProperty(default=g_convtri,name="Convert to triangular mesh first)") - endstep: bpy.props.EnumProperty(default=g_endstep, name="Run through step", items = enum_endstep) - tetgenopt: bpy.props.StringProperty(default=g_tetgenopt,name="Additional tetgen flags") + + maxvol: bpy.props.FloatProperty(default=G_MAXVOL, name="Maximum tet volume") + keepratio: bpy.props.FloatProperty(default=G_KEEPRATIO, name="Fraction edge kept (0-1)") + mergetol: bpy.props.FloatProperty(default=G_MERGETOL, name="Tolerance to merge nodes (0 to disable)") + dorepair: bpy.props.BoolProperty(default=G_DOREPAIR, name="Repair mesh (single object only)") + onlysurf: bpy.props.BoolProperty(default=G_ONLYSURF, + name="Return triangular surface mesh only (no tetrahedral mesh)") + convtri: bpy.props.BoolProperty(default=G_CONVTRI, name="Convert to triangular mesh first)") + endstep: bpy.props.EnumProperty(default=G_ENDSTEP, name="Run through step", items=ENUM_ENDSTEP) + tetgenopt: bpy.props.StringProperty(default=G_TETGENOPT, name="Additional tetgen flags") @classmethod def description(cls, context, properties): - hints={} - for item in enum_endstep: - hints[item[0]]=item[2] - return hints[properties.endstep] + return [desc for idx, _, desc in ENUM_ENDSTEP if idx == properties.endstep][0] def func(self): - outputdir = GetBPWorkFolder(); + outputdir = get_bp_work_folder() if not os.path.isdir(outputdir): os.makedirs(outputdir) - if(os.path.exists(os.path.join(outputdir,'regionmesh.jmsh'))): - os.remove(os.path.join(outputdir,'regionmesh.jmsh')) - if(os.path.exists(os.path.join(outputdir,'volumemesh.jmsh'))): - os.remove(os.path.join(outputdir,'volumemesh.jmsh')) + if os.path.exists(os.path.join(outputdir, 'regionmesh.jmsh')): + os.remove(os.path.join(outputdir, 'regionmesh.jmsh')) + if os.path.exists(os.path.join(outputdir, 'volumemesh.jmsh')): + os.remove(os.path.join(outputdir, 'volumemesh.jmsh')) - #remove camera and source + # remove camera and source for ob in bpy.context.scene.objects: ob.select_set(False) print(ob.type) - if ob.type == 'CAMERA' or ob.type == 'LIGHT' or ob.type == 'EMPTY' or ob.type == 'LAMP' or ob.type == 'SPEAKER': + if ob.type in ('CAMERA', 'LIGHT', 'EMPTY', 'LAMP', 'SPEAKER'): ob.select_set(True) bpy.ops.object.delete() obj = bpy.context.view_layer.objects.active - if(not self.convtri): + if not self.convtri: bpy.ops.object.select_by_type(type='MESH') bpy.ops.object.select_all(action='INVERT') else: bpy.ops.object.select_all(action='SELECT') - if len(bpy.context.selected_objects)>=1: + if len(bpy.context.selected_objects) >= 1: bpy.ops.object.convert(target='MESH') # at this point, objects are converted to mesh if possible - if(int(self.endstep)<2): + if self.endstep < '2': return bpy.ops.object.select_all(action='SELECT') - if len(bpy.context.selected_objects)>=2: + if len(bpy.context.selected_objects) >= 2: bpy.ops.object.join() # at this point, objects are jointed - if(int(self.endstep)<3): + if self.endstep < '3': return bpy.ops.object.select_all(action='DESELECT') @@ -115,88 +122,79 @@ def func(self): try: bpy.ops.mesh.intersect(mode='SELECT', separate_mode='NONE', solver='EXACT') print("use exact intersection solver") - except: + except RuntimeError: bpy.ops.mesh.intersect(mode='SELECT', separate_mode='NONE') print("use fast intersection solver") # at this point, overlapping objects are intersected - if(int(self.endstep)<4): + if self.endstep < '4': return - if(self.convtri): + if self.convtri: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') # at this point, if enabled, surfaces are converted to triangular meshes - if(int(self.endstep)<5): + if self.endstep < '5': return - #output mesh data to Octave + # output mesh data to Octave # this works only in object mode, bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='SELECT') obj = bpy.context.view_layer.objects.active - verts = [] - for n in range(len(obj.data.vertices)): - vert = obj.data.vertices[n].co - v_global = obj.matrix_world @ vert - verts.append(v_global) + verts = [obj.matrix_world @ vert.co for vert in obj.data.vertices] edges = [edge.vertices[:] for edge in obj.data.edges] - faces = [(np.array(face.vertices[:])+1).tolist() for face in obj.data.polygons] + faces = [(np.array(face.vertices[:]) + 1).tolist() for face in obj.data.polygons] v = np.array(verts) - if(self.convtri): + if self.convtri: f = np.array(faces) else: f = faces # Save file - meshdata={'_DataInfo_': {'JMeshVersion': '0.5', 'Comment':'Created by BlenderPhotonics (http:\/\/mcx.space\/BlenderPhotonics)'}, - 'MeshVertex3':v, 'MeshPoly':f, 'param':{'keepratio':self.keepratio, 'maxvol':self.maxvol, 'mergetol':self.mergetol, 'dorepair':self.dorepair, 'tetgenopt':self.tetgenopt}} - jd.save(meshdata,os.path.join(outputdir,'blendermesh.jmsh')) - - if(int(self.endstep)==5): + meshdata = {'_DataInfo_': {'JMeshVersion': '0.5', + 'Comment': 'Created by BlenderPhotonics (http://mcx.space/BlenderPhotonics)'}, + 'MeshVertex3': v, 'MeshPoly': f, + 'param': {'keepratio': self.keepratio, 'maxvol': self.maxvol, 'mergetol': self.mergetol, + 'dorepair': self.dorepair, 'tetgenopt': self.tetgenopt}} + jd.save(meshdata, os.path.join(outputdir, 'blendermesh.jmsh')) + + if self.endstep == '5': bpy.ops.blender2mesh.invoke_saveas('INVOKE_DEFAULT') # at this point, all mesh objects are saved to a jmesh file under work-dir as blendermesh.json - if(int(self.endstep)<6): + if self.endstep < '6': return - try: - if(bpy.context.scene.blender_photonics.backend == "octave"): - import oct2py as op - oc = op.Oct2Py() - else: - import matlab.engine as op - oc = op.start_matlab() - except ImportError: - raise ImportError('To run this feature, you must install the oct2py or matlab.engine Python modulem first, based on your choice of the backend') - - oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),'script')) + backend = get_backend(bpy.context.scene.blender_photonics.backend) + backend.blender2mesh(os.path.join(outputdir, 'blendermesh.jmsh')) - oc.feval('blender2mesh',os.path.join(outputdir,'blendermesh.jmsh'), nargout=0) - - # import volum mesh to blender(just for user to check the result) + # import volume mesh to blender(just for user to check the result) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() - if(not self.onlysurf): - outputmesh=jd.load(os.path.join(outputdir,'volumemesh.jmsh')) - LoadTetMesh(outputmesh,'Iso2Mesh') - bpy.context.view_layer.objects.active=bpy.data.objects['Iso2Mesh'] + if not self.onlysurf: + outputmesh = jd.load(os.path.join(outputdir, 'volumemesh.jmsh')) + load_tet_mesh(outputmesh, 'Iso2Mesh') + bpy.context.view_layer.objects.active = bpy.data.objects['Iso2Mesh'] else: - regiondata=jd.load(os.path.join(outputdir,'regionmesh.jmsh')) - if(len(regiondata.keys())>0): - LoadReginalMesh(regiondata,'region_') - bpy.context.view_layer.objects.active=bpy.data.objects['region_1'] - + regiondata = jd.load(os.path.join(outputdir, 'regionmesh.jmsh')) + if len(regiondata.keys()) > 0: + load_regional_mesh(regiondata, 'region_') + bpy.context.view_layer.objects.active = bpy.data.objects['region_1'] + bpy.context.space_data.shading.type = 'WIREFRAME' # at this point, if successful, iso2mesh generated mesh objects are imported into blender - if(int(self.endstep)<7): + if self.endstep < '7': return - ShowMessageBox("Mesh generation is complete. The combined tetrahedral mesh is imported for inspection. To set optical properties for each region, please click 'Load mesh and setup simulation'", "BlenderPhotonics") + show_message_box( + "Mesh generation is complete. The combined tetrahedral mesh is imported for inspection. To set optical " + "properties for each region, please click 'Load mesh and setup simulation'", + "BlenderPhotonics") def execute(self, context): print("begin to generate mesh") @@ -216,9 +214,10 @@ class setmeshingprop(bpy.types.Panel): bl_region_type = "UI" def draw(self, context): - global g_maxvol, g_keepratio, g_mergetol, g_dorepair, onlysurf, g_convtri, g_tetgenopt, g_endstep + global G_MAXVOL, G_KEEPRATIO, G_MERGETOL, G_DOREPAIR, G_ONLYSURF, G_CONVTRI, G_TETGENOPT, G_ENDSTEP self.layout.operator("object.dialog_operator") + # This operator will open Blender's file chooser when invoked # and store the selected filepath in self.filepath and print it # to the console using window_manager.fileselect_add() @@ -226,19 +225,20 @@ class BLENDER2MESH_OT_invoke_saveas(bpy.types.Operator): bl_idname = "blender2mesh.invoke_saveas" bl_label = "Export scene in a JMesh/JSON universal exchange file" - filepath: bpy.props.StringProperty(default='',subtype='DIR_PATH') + filepath: bpy.props.StringProperty(default='', subtype='DIR_PATH') def execute(self, context): print(self.filepath) - if(not (self.filepath == "")): + if not (self.filepath == ""): if os.name == 'nt': - os.popen("copy '"+os.path.join(GetBPWorkFolder(),'blendermesh.jmsh')+"' '"+self.filepath+"'"); + os.popen("copy '" + os.path.join(get_bp_work_folder(), 'blendermesh.jmsh') + "' '" + self.filepath + "'") else: - os.popen("cp '"+os.path.join(GetBPWorkFolder(),'blendermesh.jmsh')+"' '"+self.filepath+"'"); + os.popen("cp '" + os.path.join(get_bp_work_folder(), 'blendermesh.jmsh') + "' '" + self.filepath + "'") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - + + register_class(BLENDER2MESH_OT_invoke_saveas) diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..6585337 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,14 @@ +class BlenderPhotonicsError(Exception): + pass + + +class BlenderPhotonicsDependencyError(BlenderPhotonicsError): + """Errors concerning missing system or Python utilities/modules/packages""" + def __init__(self, dep: str, *args, **kwargs): + self.missing_dep = dep + super().__init__(f'Required dependency not found: {dep}', *args) + + +class BlenderPhotonicsMeshingError(BlenderPhotonicsError): + """Meshing failure, typically a forwarding of some backend error""" + pass diff --git a/mesh2blender.py b/mesh2blender.py index 5c45b22..5b5cea1 100644 --- a/mesh2blender.py +++ b/mesh2blender.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -27,11 +28,13 @@ import os from .utils import * + class mesh2scene(bpy.types.Operator): bl_label = 'Load mesh and setup simulation' - bl_description = 'Import mesh to Blender. If one needs to run MMC photon simulations, please remember to set the optical properties to each region' + bl_description = 'Import mesh to Blender. If one needs to run MMC photon simulations, please remember to set the ' \ + 'optical properties to each region ' bl_idname = 'blenderphotonics.meshtoscene' - + def importmesh(self): # clear all object bpy.ops.object.mode_set(mode='OBJECT') @@ -39,25 +42,26 @@ def importmesh(self): bpy.ops.object.delete() # folder path for importing .jmsh files - outputdir = GetBPWorkFolder(); - - regiondata=jd.load(os.path.join(outputdir,'regionmesh.jmsh')) - bbx=LoadReginalMesh(regiondata,'region_') + outputdir = get_bp_work_folder() + + regiondata = jd.load(os.path.join(outputdir, 'regionmesh.jmsh')) + bbx = load_regional_mesh(regiondata, 'region_') - ## add properties + # add properties for obj in bpy.data.objects: obj["mua"] = 0.001 obj["mus"] = 0.1 obj["g"] = 0.0 obj["n"] = 1.37 - ## add source + # add source light_data = bpy.data.lights.new(name="source", type='SPOT') light_object = bpy.data.objects.new(name="source", object_data=light_data) bpy.context.collection.objects.link(light_object) bpy.context.view_layer.objects.active = light_object - if((not np.any(np.isinf(bbx['min']))) and (not np.any(np.isinf(bbx['max'])))): - light_object.location = ((bbx['min'][0]+bbx['max'][0])*0.5, (bbx['min'][1]+bbx['max'][1])*0.5, bbx['max'][2]+0.1*(bbx['max'][2]-bbx['min'][2])) + if (not np.any(np.isinf(bbx['min']))) and (not np.any(np.isinf(bbx['max']))): + light_object.location = ((bbx['min'][0] + bbx['max'][0]) * 0.5, (bbx['min'][1] + bbx['max'][1]) * 0.5, + bbx['max'][2] + 0.1 * (bbx['max'][2] - bbx['min'][2])) else: light_object.location = (0, 0, 5) dg = bpy.context.evaluated_depsgraph_get() diff --git a/nii2mesh.py b/nii2mesh.py index 62af1ff..6db47b7 100644 --- a/nii2mesh.py +++ b/nii2mesh.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -21,87 +22,107 @@ } """ - import bpy import numpy as np import jdata as jd import os from .utils import * -g_maxvol=100 -g_radbound=10 -g_distbound=1.0 -g_isovalue=0.5 -g_imagetype="multi-label" -g_method="auto" +G_MAXVOL = 100 +G_RADBOUND = 10 +G_DISTBOUND = 1.0 +G_ISOVALUE = 0.5 +G_IMAGETYPE = "multi-label" +G_METHOD = "auto" + class nii2mesh(bpy.types.Operator): bl_label = 'Convert 3-D image file to mesh' - bl_description = "Click this button to convert a 3D volume stored in JNIfTI (.jnii/.bnii, see http://neurojson.org) or NIfTI (.nii/.nii.gz) or .mat file to a mesh" + bl_description = "Click this button to convert a 3D volume stored in JNIfTI (.jnii/.bnii, " \ + "see http://neurojson.org) or NIfTI (.nii/.nii.gz) or .mat file to a mesh " bl_idname = 'blenderphotonics.creatregion' - # creat a interface to set uesrs' model parameter. + # create an interface to set user's model parameter. bl_options = {"REGISTER", "UNDO"} - maxvol: bpy.props.FloatProperty(default=g_maxvol, name="Maximum tetrahedron volume") - radbound: bpy.props.FloatProperty(default=g_radbound,name="Surface triangle maximum diameter") - distbound: bpy.props.FloatProperty(default=g_distbound,name="Maximum deviation from true boundary") - isovalue: bpy.props.FloatProperty(default=g_isovalue,name="Isovalue to create surface") - imagetype: bpy.props.EnumProperty(name="Volume type", items = [('multi-label','multi-label','multi-label'), ('binary','binary','binary'), ('grayscale','grayscale','grayscale')]) - method: bpy.props.EnumProperty(name="Mesh extraction method", items = [('auto','auto','auto'),('cgalmesh','cgalmesh','cgalmesh'), ('cgalsurf','cgalsurf','cgalsurf'), ('simplify','simplify','simplify')]) - + + maxvol: bpy.props.FloatProperty(default=G_MAXVOL, name="Maximum tetrahedron volume") + radbound: bpy.props.FloatProperty(default=G_RADBOUND, name="Surface triangle maximum diameter") + distbound: bpy.props.FloatProperty(default=G_DISTBOUND, name="Maximum deviation from true boundary") + isovalue: bpy.props.FloatProperty(default=G_ISOVALUE, name="Isovalue to create surface") + imagetype: bpy.props.EnumProperty(name="Volume type", items=[('multi-label', 'multi-label', 'multi-label'), + ('binary', 'binary', 'binary'), + ('grayscale', 'grayscale', 'grayscale')]) + method: bpy.props.EnumProperty(name="Mesh extraction method", + items=[('auto', 'auto', 'auto'), ('cgalmesh', 'cgalmesh', 'cgalmesh'), + ('cgalsurf', 'cgalsurf', 'cgalsurf'), ('simplify', 'simplify', 'simplify')]) + def vol2mesh(self): # Remove last .jmsh file - outputdir = GetBPWorkFolder() + outputdir = get_bp_work_folder() if not os.path.isdir(outputdir): os.makedirs(outputdir) - if(os.path.exists(os.path.join(outputdir,'regionmesh.jmsh'))): - os.remove(os.path.join(outputdir,'regionmesh.jmsh')) - if(os.path.exists(os.path.join(outputdir,'volumemesh.jmsh'))): - os.remove(os.path.join(outputdir,'volumemesh.jmsh')) + if os.path.exists(os.path.join(outputdir, 'regionmesh.jmsh')): + os.remove(os.path.join(outputdir, 'regionmesh.jmsh')) + if os.path.exists(os.path.join(outputdir, 'volumemesh.jmsh')): + os.remove(os.path.join(outputdir, 'volumemesh.jmsh')) # nii to mesh niipath = bpy.context.scene.blender_photonics.path print(niipath) - if (len(niipath)==0): + if len(niipath) == 0: return - jd.save({'niipath':niipath, 'maxvol':self.maxvol, 'radbound':self.radbound,'distbound':self.distbound, 'isovalue':self.isovalue,'imagetype':self.imagetype,'method':self.method},os.path.join(outputdir,'niipath.json')); + jd.save({'niipath': niipath, 'maxvol': self.maxvol, 'radbound': self.radbound, 'distbound': self.distbound, + 'isovalue': self.isovalue, 'imagetype': self.imagetype, 'method': self.method}, + os.path.join(outputdir, 'niipath.json')) - #run MMC + # run MMC try: - if(bpy.context.scene.blender_photonics.backend == "octave"): + if bpy.context.scene.blender_photonics.backend == "octave": import oct2py as op + from oct2py.utils import Oct2PyError as OcError1 + OcError2 = OcError1 oc = op.Oct2Py() else: import matlab.engine as op + from matlab.engine import MatlabExecutionError as OcError1, RejectedExecutionError as OcError2 oc = op.start_matlab() except ImportError: - raise ImportError('To run this feature, you must install the `oct2py` or `matlab.engine` Python module first, based on your choice of the backend') + raise ImportError( + 'To run this feature, you must install the `oct2py` or `matlab.engine` Python module first, ' + 'based on your choice of the backend') - oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),'script')) - oc.feval('nii2mesh',os.path.join(outputdir,'niipath.json'), nargout=0) + oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'script')) + try: + oc.feval('nii2mesh', os.path.join(outputdir, 'niipath.json'), nargout=0) + except OcError1 as e: + if 'too many outputs' in e.args[0]: + oc.feval('nii2mesh', os.path.join(outputdir, 'niipath.json'), nout=0) + else: + raise # import volum mesh to blender(just for user to check the result) bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() - regiondata=jd.load(os.path.join(outputdir,'regionmesh.jmsh')); - regiondata=JMeshFallback(regiondata) - n=len(regiondata.keys())-1 + regiondata = jd.load(os.path.join(outputdir, 'regionmesh.jmsh')) + regiondata = jmesh_fallback(regiondata) + n = len(regiondata.keys()) - 1 # To import mesh.ply in batches - for i in range (0,n): - surfkey='MeshTri3('+str(i+1)+')' - if(n==1): - surfkey='MeshTri3' - if (not isinstance(regiondata[surfkey], np.ndarray)): - regiondata[surfkey]=np.asarray(regiondata[surfkey],dtype=np.uint32); - regiondata[surfkey]-=1 - AddMeshFromNodeFace(regiondata['MeshVertex3'],regiondata[surfkey].tolist(),'region_'+str(i+1)); + for i in range(n): + surfkey = 'MeshTri3' if n == 1 else f'MeshTri3({i + 1})' + if not isinstance(regiondata[surfkey], np.ndarray): + regiondata[surfkey] = np.asarray(regiondata[surfkey], dtype=np.uint32) + regiondata[surfkey] -= 1 + add_mesh_from_node_face(regiondata['MeshVertex3'], regiondata[surfkey].tolist(), 'region_' + str(i + 1)) bpy.context.space_data.shading.type = 'WIREFRAME' - ShowMessageBox("Mesh generation is complete. The combined tetrahedral mesh is imported for inspection. To set optical properties for each region, please click 'Load mesh and setup simulation'", "BlenderPhotonics") + show_message_box( + "Mesh generation is complete. The combined tetrahedral mesh is imported for inspection. To set optical " + "properties for each region, please click 'Load mesh and setup simulation'", + "BlenderPhotonics") def execute(self, context): self.vol2mesh() @@ -110,6 +131,7 @@ def execute(self, context): def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) + # # Dialog to set meshing properties # @@ -119,5 +141,5 @@ class setmeshingprop(bpy.types.Panel): bl_region_type = "UI" def draw(self, context): - global g_maxvol, g_radbound, g_distbound, g_imagetype, g_method - self.layout.operator("object.dialog_operator") \ No newline at end of file + global G_MAXVOL, G_RADBOUND, G_DISTBOUND, G_IMAGETYPE, G_METHOD + self.layout.operator("object.dialog_operator") diff --git a/niifile.py b/niifile.py index 610fb56..c39f350 100644 --- a/niifile.py +++ b/niifile.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -25,24 +26,28 @@ from bpy.props import StringProperty, EnumProperty from bpy.types import PropertyGroup + class niifile(PropertyGroup): path: StringProperty( - name = "JNIfTI File", - description="Accept NIfTI (.nii/.nii.gz), JSON based JNIfTI (.jnii/.bnii, see http://neurojson.org) and MATLAB .mat file (read the first 3D array object)", + name="JNIfTI File", + description="Accept NIfTI (.nii/.nii.gz), JSON based JNIfTI (.jnii/.bnii, see http://neurojson.org) and " + "MATLAB .mat file (read the first 3D array object)", default="", maxlen=2048, subtype='FILE_PATH' - ) + ) surffile: StringProperty( - name = "JMesh File", - description="Accept triangular surfaces stored in JSON-based JMesh (.jmsh/.bmsh, see http://neurojson.org), OFF, STL, ASC, SMF, and GTS", + name="JMesh File", + description="Accept triangular surfaces stored in JSON-based JMesh (.jmsh/.bmsh, see http://neurojson.org), " + "OFF, STL, ASC, SMF, and GTS", default="", maxlen=2048, subtype='FILE_PATH' - ) + ) backend: EnumProperty( - name = "Backend", + name="Backend", description="Select either Octave or MATLAB as the backend to run Iso2Mesh and MMCLAB", default="octave", - items = (('octave','Octave','Use oct2py to call Iso2Mesh/MMCLAB from GNU Octave'),('matlab','MATLAB','Use matlab.engine to call Iso2Mesh/MMCLAB from MATLAB')) - ) + items=(('octave', 'Octave', 'Use oct2py to call Iso2Mesh/MMCLAB from GNU Octave'), + ('matlab', 'MATLAB', 'Use matlab.engine to call Iso2Mesh/MMCLAB from MATLAB')) + ) diff --git a/obj2surf.py b/obj2surf.py index 0a2f287..566799a 100644 --- a/obj2surf.py +++ b/obj2surf.py @@ -8,7 +8,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -28,128 +29,148 @@ from bpy.utils import register_class, unregister_class from .utils import * -g_action='repair' -g_actionparam=1.0 -g_convtri=True -enum_action=[('import','Import surface mesh from file','Import surface mesh from JMesh/STL/OFF/SMF/ASC/MEDIT/GTS to Blender'), - ('export','Export selected to JSON/JMesh','Export selected objects to JSON/JMesh exchange file'), - ('boolean-resolve','Boolean-resolve: Two meshes slice each other','Output both objects, with each surface intersected by the other'), - ('boolean-first','Boolean-first: 1st mesh sliced by the 2nd','Return the 1st object but sliced by the 2nd object'), - ('boolean-second','Boolean-second: 2nd mesh sliced by the 1st','Return the 2nd object but sliced by the 1st object'), - ('boolean-diff','Boolean-diff: 1st mesh subtract 2nd','Return the 1st object subtracted by the 2nd'), - ('boolean-and','Boolean-and: Space in both objects','Return the surface of the overlapping region'), - ('boolean-or','Boolean-or: Space for joint/union space','Return the outer surface of the merged object'), - ('boolean-decouple','Boolean-decouple: Decouple two shell meshes','Insert a small gap between two touching objects'), - ('simplify','Surface simplification', 'Simplifing a surface by decimating edges'), - ('remesh','Remesh surface', 'Remesh surface and remove badly shaped triangles'), - ('smooth','Smooth selected objects','Smooth selected mesh object'), - ('reorient','Reorient all triangles','Reorient all triangles in counter-clockwise direction'), - ('repair','Fix self-intersection and holes','Fix self-intersection and fill holes of a closed object')] +G_ACTION = 'repair' +G_ACTIONPARAM = 1.0 +G_CONVTRI = True +ENUM_ACTION = [ + ('import', 'Import surface mesh from file', 'Import surface mesh from JMesh/STL/OFF/SMF/ASC/MEDIT/GTS to Blender'), + ('export', 'Export selected to JSON/JMesh', 'Export selected objects to JSON/JMesh exchange file'), + ('boolean-resolve', 'Boolean-resolve: Two meshes slice each other', + 'Output both objects, with each surface intersected by the other'), + ( + 'boolean-first', 'Boolean-first: 1st mesh sliced by the 2nd', + 'Return the 1st object but sliced by the 2nd object'), + ('boolean-second', 'Boolean-second: 2nd mesh sliced by the 1st', + 'Return the 2nd object but sliced by the 1st object'), + ('boolean-diff', 'Boolean-diff: 1st mesh subtract 2nd', 'Return the 1st object subtracted by the 2nd'), + ('boolean-and', 'Boolean-and: Space in both objects', 'Return the surface of the overlapping region'), + ('boolean-or', 'Boolean-or: Space for joint/union space', 'Return the outer surface of the merged object'), + ('boolean-decouple', 'Boolean-decouple: Decouple two shell meshes', + 'Insert a small gap between two touching objects'), + ('simplify', 'Surface simplification', 'Simplifing a surface by decimating edges'), + ('remesh', 'Remesh surface', 'Remesh surface and remove badly shaped triangles'), + ('smooth', 'Smooth selected objects', 'Smooth selected mesh object'), + ('reorient', 'Reorient all triangles', 'Reorient all triangles in counter-clockwise direction'), + ('repair', 'Fix self-intersection and holes', 'Fix self-intersection and fill holes of a closed object')] + class object2surf(bpy.types.Operator): bl_label = 'Process selected object surfaces' - bl_description = "Create surface meshes from selected objects (smoothing, refine, Boolean, repairing, simplification, ...)" + bl_description = "Create surface meshes from selected objects (smoothing, refine, Boolean, repairing, " \ + "simplification, ...) " bl_idname = 'blenderphotonics.blender2surf' - # creat a interface to set uesrs' model parameter. + # create an interface to set user's model parameter. bl_options = {"REGISTER", "UNDO"} - action: bpy.props.EnumProperty(default=g_action, name="Operation", items = enum_action) - actionparam: bpy.props.FloatProperty(default=g_actionparam, name="Operation parameter") - convtri: bpy.props.BoolProperty(default=g_convtri,name="Convert to triangular mesh first") + + action: bpy.props.EnumProperty(default=G_ACTION, name="Operation", items=ENUM_ACTION) + actionparam: bpy.props.FloatProperty(default=G_ACTIONPARAM, name="Operation parameter") + convtri: bpy.props.BoolProperty(default=G_CONVTRI, name="Convert to triangular mesh first") @classmethod def description(cls, context, properties): - hints={} - for item in enum_action: - hints[item[0]]=item[2] - return hints[properties.action] + return [desc for idx, _, desc in ENUM_ACTION if idx == properties.action][0] def func(self): - outputdir = GetBPWorkFolder(); + outputdir = get_bp_work_folder() if not os.path.isdir(outputdir): os.makedirs(outputdir) - if(os.path.exists(os.path.join(outputdir,'surfacemesh.jmsh'))): - os.remove(os.path.join(outputdir,'surfacemesh.jmsh')) + if os.path.exists(os.path.join(outputdir, 'surfacemesh.jmsh')): + os.remove(os.path.join(outputdir, 'surfacemesh.jmsh')) - if len(bpy.context.selected_objects)<1: - ShowMessageBox("Must select at least one object (for Boolean operations, select two)", "BlenderPhotonics") - return; + if len(bpy.context.selected_objects) < 1: + show_message_box("Must select at least one object (for Boolean operations, select two)", "BlenderPhotonics") + return - #remove camera and light objects + # remove camera and light objects for ob in bpy.context.selected_objects: print(ob.type) - if ob.type == 'CAMERA' or ob.type == 'LIGHT' or ob.type == 'EMPTY' or ob.type == 'LAMP' or ob.type == 'SPEAKER': + if ob.type in ('CAMERA', 'LIGHT', 'EMPTY', 'LAMP', 'SPEAKER'): ob.select_set(False) bpy.ops.object.convert(target='MESH') bpy.ops.object.mode_set(mode='EDIT') - if(self.convtri): + if self.convtri: bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') - if len(bpy.context.selected_objects)<1: - ShowMessageBox("No mesh-like object was selected, skip", "BlenderPhotonics") - return; + if len(bpy.context.selected_objects) < 1: + show_message_box("No mesh-like object was selected, skip", "BlenderPhotonics") + return bpy.ops.object.mode_set(mode='OBJECT') - surfdata={'_DataInfo_': {'JMeshVersion': '0.5', 'Comment':'Object surface mesh created by BlenderPhotonics (http:\/\/mcx.space\/BlenderPhotonics)'}} - surfdata['MeshGroup']=[] + surfdata = {'_DataInfo_': {'JMeshVersion': '0.5', + 'Comment': 'Object surface mesh created by BlenderPhotonics (' + 'http://mcx.space/BlenderPhotonics)'}, + 'MeshGroup': []} for ob in bpy.context.selected_objects: - objsurf=GetNodeFacefromObject(ob, self.convtri) + objsurf = get_node_face_from_object(ob, self.convtri) surfdata['MeshGroup'].append(objsurf) - surfdata['param']={'action':self.action, 'level':self.actionparam} + surfdata['param'] = {'action': self.action, 'level': self.actionparam} - jd.save(surfdata,os.path.join(outputdir,'blendersurf.jmsh')) + jd.save(surfdata, os.path.join(outputdir, 'blendersurf.jmsh')) # at this point, objects are converted to mesh if possible - if(self.action == 'export'): + if self.action == 'export': bpy.ops.object2surf.invoke_export('INVOKE_DEFAULT') return try: - if(bpy.context.scene.blender_photonics.backend == "octave"): + if bpy.context.scene.blender_photonics.backend == "octave": import oct2py as op + from oct2py.utils import Oct2PyError as OcError1 + OcError2 = OcError1 oc = op.Oct2Py() else: import matlab.engine as op + from matlab.engine import MatlabExecutionError as OcError1, RejectedExecutionError as OcError2 oc = op.start_matlab() except ImportError: - raise ImportError('To run this feature, you must install the oct2py or matlab.engine Python modulem first, based on your choice of the backend') + raise ImportError( + 'To run this feature, you must install the oct2py or matlab.engine Python modulem first, based on ' + 'your choice of the backend') - oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),'script')) + oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'script')) - oc.feval('blender2surf',os.path.join(outputdir,'blendersurf.jmsh'), nargout=0) + try: + oc.feval('blender2surf', os.path.join(outputdir, 'blendersurf.jmsh'), nargout=0) + except OcError1 as e: + if 'too many outputs' in e.args[0]: + oc.feval('blender2surf', os.path.join(outputdir, 'blendersurf.jmsh'), nout=0) + else: + raise # import volum mesh to blender(just for user to check the result) - if len(bpy.context.selected_objects)>=1: + if len(bpy.context.selected_objects) >= 1: bpy.ops.object.delete() - surfdata=jd.load(os.path.join(outputdir,'surfacemesh.jmsh')) - idx=1 - if(len(surfdata['MeshGroup'])>0): - ob=surfdata['MeshGroup'] - objname='surf_'+str(idx) - if('MeshVertex3' in ob): - if(('_DataInfo_' in ob) and ('BlenderObjectName' in ob['_DataInfo_'])): - objname=ob['_DataInfo_']['BlenderObjectName'] - AddMeshFromNodeFace(ob['MeshVertex3'],(np.array(ob['MeshTri3'])-1).tolist(),objname) - bpy.context.view_layer.objects.active=bpy.data.objects[objname] + surfdata = jd.load(os.path.join(outputdir, 'surfacemesh.jmsh')) + idx = 1 + if len(surfdata['MeshGroup']) > 0: + ob = surfdata['MeshGroup'] + objname = 'surf_' + str(idx) + if 'MeshVertex3' in ob: + if ('_DataInfo_' in ob) and ('BlenderObjectName' in ob['_DataInfo_']): + objname = ob['_DataInfo_']['BlenderObjectName'] + add_mesh_from_node_face(ob['MeshVertex3'], (np.array(ob['MeshTri3']) - 1).tolist(), objname) + bpy.context.view_layer.objects.active = bpy.data.objects[objname] else: for ob in surfdata['MeshGroup']: - objname='surf_'+str(idx) - if(('_DataInfo_' in ob) and ('BlenderObjectName' in ob['_DataInfo_'])): - objname=ob['_DataInfo_']['BlenderObjectName'] - AddMeshFromNodeFace(ob['MeshVertex3'],(np.array(ob['MeshTri3'])-1).tolist(),objname) - bpy.context.view_layer.objects.active=bpy.data.objects[objname] - idx+=1 + objname = 'surf_' + str(idx) + if ('_DataInfo_' in ob) and ('BlenderObjectName' in ob['_DataInfo_']): + objname = ob['_DataInfo_']['BlenderObjectName'] + add_mesh_from_node_face(ob['MeshVertex3'], (np.array(ob['MeshTri3']) - 1).tolist(), objname) + bpy.context.view_layer.objects.active = bpy.data.objects[objname] + idx += 1 bpy.context.space_data.shading.type = 'WIREFRAME' - ShowMessageBox("Mesh generation is complete. The combined surface mesh is imported for inspection.", "BlenderPhotonics") + show_message_box("Mesh generation is complete. The combined surface mesh is imported for inspection.", + "BlenderPhotonics") def execute(self, context): print("begin to process object surface mesh") @@ -157,10 +178,10 @@ def execute(self, context): return {"FINISHED"} def invoke(self, context, event): - if(not self.action == 'import'): - return context.window_manager.invoke_props_dialog(self) + if not self.action == 'import': + return context.window_manager.invoke_props_dialog(self) else: - return bpy.ops.object2surf.invoke_import('INVOKE_DEFAULT') + return bpy.ops.object2surf.invoke_import('INVOKE_DEFAULT') # @@ -172,9 +193,10 @@ class setmeshingprop(bpy.types.Panel): bl_region_type = "UI" def draw(self, context): - global g_action, g_actionparam, g_convtri + global G_ACTION, G_ACTIONPARAM, G_CONVTRI self.layout.operator("object.dialog_operator") + # This operator will open Blender's file chooser when invoked # and store the selected filepath in self.filepath and print it # to the console using window_manager.fileselect_add() @@ -183,51 +205,64 @@ class OBJECT2SURF_OT_invoke_export(bpy.types.Operator): bl_label = "Export to JMesh" bl_description = "Export mesh in the JSON/JMesh format" - filepath: bpy.props.StringProperty(default='',subtype='DIR_PATH') + filepath: bpy.props.StringProperty(default='', subtype='DIR_PATH') def execute(self, context): print(self.filepath) - if(not (self.filepath == "")): + if not (self.filepath == ""): if os.name == 'nt': - os.popen("copy '"+os.path.join(GetBPWorkFolder(),'blendersurf.jmsh')+"' '"+self.filepath+"'") + os.popen("copy '" + os.path.join(get_bp_work_folder(), 'blendersurf.jmsh') + "' '" + self.filepath + "'") else: - os.popen("cp '"+os.path.join(GetBPWorkFolder(),'blendersurf.jmsh')+"' '"+self.filepath+"'") + os.popen("cp '" + os.path.join(get_bp_work_folder(), 'blendersurf.jmsh') + "' '" + self.filepath + "'") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} + register_class(OBJECT2SURF_OT_invoke_export) # This operator will open Blender's file chooser when invoked # and store the selected filepath in self.filepath and print it # to the console using window_manager.fileselect_add() -class OBJECT2SURF_OT_invoke_import(bpy.types.Operator,ImportHelper): +class OBJECT2SURF_OT_invoke_import(bpy.types.Operator, ImportHelper): bl_idname = "object2surf.invoke_import" bl_label = "Import Mesh" bl_description = "Import triangular surfaces in .json,.jmsh,.bmsh,.off,.medit,.stl,.smf,.gts" - filename_ext: "*.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts" - filepath: bpy.props.StringProperty(default='',subtype='DIR_PATH') + filename_ext: bpy.props.StringProperty(default="*.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts") + filepath: bpy.props.StringProperty(default='', subtype='DIR_PATH') filter_glob: bpy.props.StringProperty( - default="*.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts", - options={'HIDDEN'}, - description="Reading triangular surface mesh from *.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts", - maxlen=2048, # Max internal buffer length, longer would be clamped. - ) + default="*.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts", + options={'HIDDEN'}, + description="Reading triangular surface mesh from *.json;*.jmsh;*.bmsh;*.off;*.medit;*.stl;*.smf;*.gts", + maxlen=2048, # Max internal buffer length, longer would be clamped. + ) def execute(self, context): - oc = op.Oct2Py() - oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),'script')) - surfdata=oc.feval('surf2jmesh',self.filepath) - AddMeshFromNodeFace(surfdata['MeshVertex3'],(np.array(surfdata['MeshTri3'])-1).tolist(),'importedsurf') + try: + if bpy.context.scene.blender_photonics.backend == "octave": + import oct2py as op + oc = op.Oct2Py() + else: + import matlab.engine as op + oc = op.start_matlab() + except ImportError: + raise ImportError( + 'To run this feature, you must install the oct2py or matlab.engine Python modulem first, based on ' + 'your choice of the backend') + + oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'script')) + surfdata = oc.feval('surf2jmesh', self.filepath) + add_mesh_from_node_face(surfdata['MeshVertex3'], (np.array(surfdata['MeshTri3']) - 1).tolist(), 'importedsurf') return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - + + register_class(OBJECT2SURF_OT_invoke_import) diff --git a/runmmc.py b/runmmc.py index 153f47b..b24418d 100644 --- a/runmmc.py +++ b/runmmc.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -27,16 +28,16 @@ import os from .utils import * -g_nphoton=10000 -g_tend=5e-9 -g_tstep=5e-9 -g_method="elem" -g_outputtype="flux" -g_isreflect=True -g_isnormalized=True -g_basisorder=1 -g_debuglevel="TP" -g_gpuid="1" +G_NPHOTON = 10000 +G_TEND = 5e-9 +G_TSTEP = 5e-9 +G_METHOD = "elem" +G_OUTPUTTYPE = "flux" +G_ISREFLECT = True +G_ISNORMALIZED = True +G_BASISORDER = 1 +G_DEBUGLEVEL = "TP" +G_GPUID = "1" class runmmc(bpy.types.Operator): @@ -44,100 +45,118 @@ class runmmc(bpy.types.Operator): bl_description = "Run mesh-based Monte Carlo simulation" bl_idname = 'blenderphotonics.runmmc' - # creat a interface to set uesrs' model parameter. + # create an interface to set user's model parameter. bl_options = {"REGISTER", "UNDO"} - nphoton: bpy.props.FloatProperty(default=g_nphoton, name="Photon number") - tend: bpy.props.FloatProperty(default=g_tend,name="Time gate width (s)") - tstep: bpy.props.FloatProperty(default=g_tstep,name="Time gate step (s)") - isreflect: bpy.props.BoolProperty(default=g_isreflect,name="Do reflection") - isnormalized: bpy.props.BoolProperty(default=g_isnormalized,name="Normalize output") - basisorder: bpy.props.IntProperty(default=g_basisorder,step=1,name="Basis order (0 or 1)") - method: bpy.props.EnumProperty(default=g_method, name="Raytracer (use elem)", items = [('elem','elem: Saving weight on elements','Saving weight on elements'),('grid','grid: Dual-grid MMC (not supported)','Dual-grid MMC')]) - outputtype: bpy.props.EnumProperty(default=g_outputtype, name="Output quantity", items = [('flux','flux: fluence rate','fluence rate (J/mm^2/s)'),('fluence','fluence: fluence (J/mm^2)','fluence in J/mm^2'),('energy','energy: energy density J/mm^3','energy density J/mm^3')]) - gpuid: bpy.props.StringProperty(default=g_gpuid,name="GPU ID (01 mask,-1=CPU)") - debuglevel: bpy.props.StringProperty(default=g_debuglevel,name="Debug flag [MCBWDIOXATRPE]") + + nphoton: bpy.props.FloatProperty(default=G_NPHOTON, name="Photon number") + tend: bpy.props.FloatProperty(default=G_TEND, name="Time gate width (s)") + tstep: bpy.props.FloatProperty(default=G_TSTEP, name="Time gate step (s)") + isreflect: bpy.props.BoolProperty(default=G_ISREFLECT, name="Do reflection") + isnormalized: bpy.props.BoolProperty(default=G_ISNORMALIZED, name="Normalize output") + basisorder: bpy.props.IntProperty(default=G_BASISORDER, step=1, name="Basis order (0 or 1)") + method: bpy.props.EnumProperty(default=G_METHOD, name="Raytracer (use elem)", + items=[('elem', 'elem: Saving weight on elements', 'Saving weight on elements'), + ('grid', 'grid: Dual-grid MMC (not supported)', 'Dual-grid MMC')]) + outputtype: bpy.props.EnumProperty(default=G_OUTPUTTYPE, name="Output quantity", + items=[('flux', 'flux= fluence rate', 'fluence rate (J/mm^2/s)'), + ('fluence', 'fluence: fluence (J/mm^2)', 'fluence in J/mm^2'), + ('energy', 'energy: energy density J/mm^3', 'energy density J/mm^3')]) + gpuid: bpy.props.StringProperty(default=G_GPUID, name="GPU ID (01 mask,-1=CPU)") + debuglevel: bpy.props.StringProperty(default=G_DEBUGLEVEL, name="Debug flag [MCBWDIOXATRPE]") def preparemmc(self): - ## save optical parameters and source source information - parameters = [] # mu_a, mu_s, n, g - cfg = [] # location, direction, photon number, Type, + # save optical parameters and source source information + parameters = [] # mu_a, mu_s, n, g + cfg = [] # location, direction, photon number, Type, for obj in bpy.data.objects[0:-1]: - if(not ("mua" in obj)): + if not ("mua" in obj): continue - parameters.append([obj["mua"],obj["mus"],obj["g"],obj["n"]]) + parameters.append([obj["mua"], obj["mus"], obj["g"], obj["n"]]) obj = bpy.data.objects['source'] - location = np.array(obj.location).tolist(); + location = np.array(obj.location).tolist() bpy.context.object.rotation_mode = 'QUATERNION' - direction = np.array(bpy.context.object.rotation_quaternion).tolist(); - srcparam1=[val for val in obj['srcparam1']] - srcparam2=[val for val in obj['srcparam2']] - cfg={'srctype':obj['srctype'],'srcpos':location, 'srcdir':direction,'srcparam1':srcparam1, - 'srcparam2':srcparam2,'nphoton': self.nphoton, 'srctype':obj["srctype"], 'unitinmm': obj['unitinmm'], - 'tend':self.tend, 'tstep':self.tstep, 'isreflect':self.isreflect, 'isnormalized':self.isnormalized, - 'method':self.method, 'outputtype':self.outputtype,'basisorder':self.basisorder, 'debuglevel':self.debuglevel, 'gpuid':self.gpuid} + direction = np.array(bpy.context.object.rotation_quaternion).tolist() + srcparam1 = [val for val in obj['srcparam1']] + srcparam2 = [val for val in obj['srcparam2']] + cfg = {'srctype': obj['srctype'], 'srcpos': location, 'srcdir': direction, 'srcparam1': srcparam1, + 'srcparam2': srcparam2, 'nphoton': self.nphoton, 'unitinmm': obj['unitinmm'], + 'tend': self.tend, 'tstep': self.tstep, 'isreflect': self.isreflect, 'isnormalized': self.isnormalized, + 'method': self.method, 'outputtype': self.outputtype, 'basisorder': self.basisorder, + 'debuglevel': self.debuglevel, 'gpuid': self.gpuid} print(obj['srctype']) - outputdir = GetBPWorkFolder(); + outputdir = get_bp_work_folder() if not os.path.isdir(outputdir): os.makedirs(outputdir) # Save MMC information - jd.save({'prop':parameters,'cfg':cfg}, os.path.join(outputdir,'mmcinfo.json')); + jd.save({'prop': parameters, 'cfg': cfg}, os.path.join(outputdir, 'mmcinfo.json')) - #run MMC + # run MMC try: - if(bpy.context.scene.blender_photonics.backend == "octave"): + if bpy.context.scene.blender_photonics.backend == "octave": import oct2py as op + from oct2py.utils import Oct2PyError as OcError1 + OcError2 = OcError1 oc = op.Oct2Py() else: import matlab.engine as op + from matlab.engine import MatlabExecutionError as OcError1, RejectedExecutionError as OcError2 oc = op.start_matlab() except ImportError: - raise ImportError('To run this feature, you must install the oct2py or matlab.engine Python modulem first, based on your choice of the backend') + raise ImportError( + 'To run this feature, you must install the oct2py or matlab.engine Python modules first, based on ' + 'your choice of the backend') - oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),'script')) + oc.addpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'script')) - oc.feval('blendermmc',os.path.join(outputdir,'mmcinfo.json'), os.path.join(outputdir,'meshdata.mat'), nargout=0) + try: + oc.feval('blendermmc', os.path.join(outputdir, 'mmcinfo.json'), os.path.join(outputdir, 'meshdata.mat'), + nargout=0) + except OcError1 as e: + if 'too many outputs' in e.args[0]: + oc.feval('blendermmc', os.path.join(outputdir, 'mmcinfo.json'), os.path.join(outputdir, 'meshdata.mat'), + nout=0) + else: + raise - #remove all object and import all region as one object + # remove all object and import all region as one object bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() - outputmesh=jd.load(os.path.join(outputdir,'volumemesh.jmsh')); - outputmesh=JMeshFallback(outputmesh) - if (not isinstance(outputmesh['MeshTri3'], np.ndarray)): - outputmesh['MeshTri3']=np.asarray(outputmesh['MeshTri3'],dtype=np.uint32); - outputmesh['MeshTri3']-=1 - AddMeshFromNodeFace(outputmesh['MeshVertex3'],outputmesh['MeshTri3'].tolist(),"Iso2Mesh"); - - #add color to blender model - obj = bpy.data.objects['Iso2Mesh'] - mmcoutput=jd.load(os.path.join(outputdir,'mmcoutput.json')); - mmcoutput['logflux']=np.asarray(mmcoutput['logflux'], dtype='float32'); + outputmesh = jd.load(os.path.join(outputdir, 'volumemesh.jmsh')) + outputmesh = jmesh_fallback(outputmesh) + if not isinstance(outputmesh['MeshTri3'], np.ndarray): + outputmesh['MeshTri3'] = np.asarray(outputmesh['MeshTri3'], dtype=np.uint32) + outputmesh['MeshTri3'] -= 1 + add_mesh_from_node_face(outputmesh['MeshVertex3'], outputmesh['MeshTri3'].tolist(), "Iso2Mesh") - def normalize(x,max,min): - x=(x-min)/(max-min); - return(x) + # add color to blender model + obj = bpy.data.objects['Iso2Mesh'] + mmcoutput = jd.load(os.path.join(outputdir, 'mmcoutput.json')) + mmcoutput['logflux'] = np.asarray(mmcoutput['logflux'], dtype='float32') - colorbit=10 - colorkind=2**colorbit-1 - weight_data = normalize(mmcoutput['logflux'], np.max(mmcoutput['logflux']),np.min(mmcoutput['logflux'])) - weight_data_test =np.rint(weight_data*(colorkind)) + colorbit = 10 + colorkind = 2 ** colorbit - 1 + weight_data = normalize(mmcoutput['logflux']) + weight_data_test = np.rint(weight_data * colorkind) new_vertex_group = obj.vertex_groups.new(name='weight') - for i in range(colorkind+1): - ind=np.array(np.where(weight_data_test==i)).tolist() - new_vertex_group.add(ind[0], i/colorkind, 'ADD') + for i in range(colorkind + 1): + ind = np.array(np.where(weight_data_test == i)).tolist() + new_vertex_group.add(ind[0], i / colorkind, 'ADD') - bpy.context.view_layer.objects.active=obj + bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='WEIGHT_PAINT') bpy.context.space_data.shading.type = 'SOLID' print('Finshed!, Please change intereaction mode to Weight Paint to see result!') - print('''If you prefer a perspective effect,please go to edit mode and make sure shading 'Vertex Group Weight' is on.''') + print( + '''If you prefer a perspective effect,please go to edit mode and make sure shading 'Vertex Group Weight' + is on.''') def execute(self, context): print("Begin to run MMC source transport simulation ...") @@ -147,6 +166,7 @@ def execute(self, context): def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) + # # Dialog to set meshing properties # @@ -156,5 +176,5 @@ class setmmcprop(bpy.types.Panel): bl_region_type = "UI" def draw(self, context): - global g_nphoton, g_tend, g_tstep, g_method,g_outputtype, g_isreflect, g_isnormalized, g_basisorder, g_debuglevel, g_gpuid + global G_NPHOTON, G_TEND, G_TSTEP, G_METHOD, G_OUTPUTTYPE, G_ISREFLECT, G_ISNORMALIZED, G_BASISORDER, G_DEBUGLEVEL, G_GPUID self.layout.operator("object.dialog_operator") diff --git a/script/blender2mesh.m b/script/blender2mesh.m index 0b7b93d..ce6e8f3 100644 --- a/script/blender2mesh.m +++ b/script/blender2mesh.m @@ -50,4 +50,4 @@ function blender2mesh(filename) save('-v7',bpmwpath('meshdata.mat'),'node','elem'); disp(['begin to save region mesh']) -blendersavemesh(node,elem); \ No newline at end of file +blendersavemesh(node,elem,regionmesh_fname, volumemesh_fname); \ No newline at end of file diff --git a/script/blendersavemesh.m b/script/blendersavemesh.m index 942ced2..b244ff6 100644 --- a/script/blendersavemesh.m +++ b/script/blendersavemesh.m @@ -1,4 +1,4 @@ -function blendersavemesh(node,elem) +function blendersavemesh(node,elem,regionmesh_fname,volumemesh_fname) % % blendersavemesh(node,elem) % @@ -9,6 +9,8 @@ function blendersavemesh(node,elem) % input: % node: the node coordinate list of a tetrahedral mesh (nn x 3) % elem: the tetrahedral element list of the mesh (ne x 4) +% regionmesh_fname: file name to save the region mesh +% volumemesh_fname: file name to save the volume mesh % % output: % two JMesh files are saved under the temporary folder bpmwpath('') @@ -59,9 +61,9 @@ function blendersavemesh(node,elem) outputmesh=rmfield(outputmesh,encodevarname('MeshTri3(1)')); end disp(['begin to save whole volumic mesh.']) -savejson('',outputmesh,'FileName',bpmwpath('regionmesh.jmsh'),'ArrayIndent',0); +savejson('',outputmesh,'FileName','regionmesh.jmsh','ArrayIndent',0); faces = meshface(elem(:,1:4)); meshdata.MeshTri3=faces; -savejson('',meshdata,'FileName',bpmwpath('volumemesh.jmsh'),'ArrayIndent',0); +savejson('',meshdata,'FileName','volumemesh.jmsh','ArrayIndent',0); disp(['saving complete.']) diff --git a/ui.py b/ui.py index c24ed5e..edde392 100644 --- a/ui.py +++ b/ui.py @@ -9,7 +9,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -29,6 +30,7 @@ from .nii2mesh import nii2mesh from .obj2surf import object2surf + class BlenderPhotonics_UI(bpy.types.Panel): bl_label = 'BlenderPhotonics v2022' bl_idname = 'BLENDERPHOTONICS_PT_UI' @@ -37,8 +39,8 @@ class BlenderPhotonics_UI(bpy.types.Panel): bl_category = "BlenderPhotonics" @classmethod - def poll(self,context): - return context.mode in {'EDIT_MESH','OBJECT','PAINT_WEIGHT'} + def poll(cls, context): + return context.mode in {'EDIT_MESH', 'OBJECT', 'PAINT_WEIGHT'} def draw(self, context): layout = self.layout @@ -50,49 +52,50 @@ def draw(self, context): layout.label(text="Blender2Mesh", icon='SHADING_SOLID') colb2m = layout.column() - colb2m.operator(scene2mesh.bl_idname,icon='MESH_ICOSPHERE').endstep='9' - colb2m.operator(scene2mesh.bl_idname,text='Export scene to JSON/JMesh',icon='FILE_TICK').endstep='5' - colb2m.operator(scene2mesh.bl_idname,text='Preview surface tesselation',icon='MOD_BOOLEAN').endstep='4' + colb2m.operator(scene2mesh.bl_idname, icon='MESH_ICOSPHERE').endstep = '9' + colb2m.operator(scene2mesh.bl_idname, text='Export scene to JSON/JMesh', icon='FILE_TICK').endstep = '5' + colb2m.operator(scene2mesh.bl_idname, text='Preview surface tesselation', icon='MOD_BOOLEAN').endstep = '4' layout.separator() layout.label(text="Volume2Mesh", icon='SHADING_SOLID') layout.prop(bp, "path") colv2m = layout.column() - colv2m.operator(nii2mesh.bl_idname,icon='MESH_GRID') + colv2m.operator(nii2mesh.bl_idname, icon='MESH_GRID') layout.separator() layout.label(text="Surface2Mesh", icon='SHADING_SOLID') cols2m = layout.column() - cols2m.operator(object2surf.bl_idname,text='Import surface mesh',icon='IMPORT').action='import' - cols2m.operator(object2surf.bl_idname,text='Export object to JSON/JMesh', icon='EXPORT').action='export' - cols2m.operator(object2surf.bl_idname,text='Repair and close triangular mesh',icon='MOD_SUBSURF').action='repair' + cols2m.operator(object2surf.bl_idname, text='Import surface mesh', icon='IMPORT').action = 'import' + cols2m.operator(object2surf.bl_idname, text='Export object to JSON/JMesh', icon='EXPORT').action = 'export' + cols2m.operator(object2surf.bl_idname, text='Repair and close triangular mesh', + icon='MOD_SUBSURF').action = 'repair' rowbool = layout.row() rowbool.label(text='Boolean') - rowbool.operator(object2surf.bl_idname,text='and',icon='SELECT_INTERSECT').action='boolean-and' - rowbool.operator(object2surf.bl_idname,text='or',icon='SELECT_EXTEND').action='boolean-or' - rowbool.operator(object2surf.bl_idname,text='xor',icon='XRAY').action='boolean-resolve' + rowbool.operator(object2surf.bl_idname, text='and', icon='SELECT_INTERSECT').action = 'boolean-and' + rowbool.operator(object2surf.bl_idname, text='or', icon='SELECT_EXTEND').action = 'boolean-or' + rowbool.operator(object2surf.bl_idname, text='xor', icon='XRAY').action = 'boolean-resolve' rowbool2 = layout.row() - rowbool2.operator(object2surf.bl_idname,text='diff',icon='SELECT_SUBTRACT').action='boolean-diff' - rowbool2.operator(object2surf.bl_idname,text='1st',icon='OVERLAY').action='boolean-first' - rowbool2.operator(object2surf.bl_idname,text='2nd',icon='MOD_MASK').action='boolean-second' - rowbool2.operator(object2surf.bl_idname,text='simplify',icon='MOD_SIMPLIFY').action='simplify' + rowbool2.operator(object2surf.bl_idname, text='diff', icon='SELECT_SUBTRACT').action = 'boolean-diff' + rowbool2.operator(object2surf.bl_idname, text='1st', icon='OVERLAY').action = 'boolean-first' + rowbool2.operator(object2surf.bl_idname, text='2nd', icon='MOD_MASK').action = 'boolean-second' + rowbool2.operator(object2surf.bl_idname, text='simplify', icon='MOD_SIMPLIFY').action = 'simplify' layout.separator() layout.label(text="Multiphysics Simulation", icon='SHADING_SOLID') colmmc = layout.column() - colmmc.operator(mesh2scene.bl_idname,icon='EDITMODE_HLT') - colmmc.operator(runmmc.bl_idname,icon='LIGHT_AREA') + colmmc.operator(mesh2scene.bl_idname, icon='EDITMODE_HLT') + colmmc.operator(runmmc.bl_idname, icon='LIGHT_AREA') layout.separator() layout.label(text="Tutorials and Websites", icon='SHADING_SOLID') colurl = layout.row() - op=colurl.operator('wm.url_open', text='Iso2Mesh',icon='URL') - op.url='http://iso2mesh.sf.net' - op=colurl.operator('wm.url_open', text='JMesh spec',icon='URL') - op.url='https://github.com/NeuroJSON/jmesh/blob/master/JMesh_specification.md' + op = colurl.operator('wm.url_open', text='Iso2Mesh', icon='URL') + op.url = 'http://iso2mesh.sf.net' + op = colurl.operator('wm.url_open', text='JMesh spec', icon='URL') + op.url = 'https://github.com/NeuroJSON/jmesh/blob/master/JMesh_specification.md' colurl2 = layout.row() - op=colurl2.operator('wm.url_open', text='MMC wiki',icon='URL') - op.url='http://mcx.space/wiki/?Learn#mmc' - op=colurl2.operator('wm.url_open', text='Brain2Mesh',icon='URL') - op.url='http://mcx.space/brain2mesh' + op = colurl2.operator('wm.url_open', text='MMC wiki', icon='URL') + op.url = 'http://mcx.space/wiki/?Learn#mmc' + op = colurl2.operator('wm.url_open', text='Brain2Mesh', icon='URL') + op.url = 'http://mcx.space/brain2mesh' layout.label(text="Funded by NIH R01-GM114365 & U24-NS124027", icon='HEART') diff --git a/utils.py b/utils.py index 80fe066..18dae8e 100644 --- a/utils.py +++ b/utils.py @@ -8,7 +8,8 @@ @article{BlenderPhotonics2022, author = {Yuxuan Zhang and Qianqian Fang}, - title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon simulations in complex tissues}}, + title = {{BlenderPhotonics: an integrated open-source software environment for three-dimensional meshing and photon + simulations in complex tissues}}, volume = {27}, journal = {Journal of Biomedical Optics}, number = {8}, @@ -22,84 +23,106 @@ import bpy import os -import tempfile import numpy as np -def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'): +from bpy.types import Object as BlenderObject +from getpass import getuser +from tempfile import gettempdir +from typing import Any, Iterable + + +def show_message_box(message: str = "", title: str = "Message Box", icon: str = "INFO"): def draw(self, context): self.layout.label(text=message) - bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) - -def GetNodeFacefromObject(obj, istrimesh=True): - verts = [] - for n in range(len(obj.data.vertices)): - vert = obj.data.vertices[n].co - v_global = obj.matrix_world @ vert - verts.append(v_global) - #edges = [edge.vertices[:] for edge in obj.data.edges] - faces = [(np.array(face.vertices[:])+1).tolist() for face in obj.data.polygons] - v = np.array(verts) + bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) + + +def get_node_face_from_object(obj: BlenderObject, istrimesh: bool = True): + vertices = [obj.matrix_world @ vertex.co for vertex in obj.data.vertices] + # edges = [edge.vertices[:] for edge in obj.data.edges] + faces = [(np.array(face.vertices[:]) + 1).tolist() for face in obj.data.polygons] + v = np.array(vertices) try: f = np.array(faces) - return {'MeshVertex3':v, 'MeshTri3':f} - except: + return {"MeshVertex3": v, "MeshTri3": f} + except ValueError: f = faces - return {'_DataInfo_':{'BlenderObjectName',obj.name},'MeshVertex3':v, 'MeshPoly':f} + return {"_DataInfo_": {"BlenderObjectName", obj.name}, "MeshVertex3": v, "MeshPoly": f} -def AddMeshFromNodeFace(node,face,name): +def add_mesh_from_node_face(node: Iterable, face: Iterable, name: str): # Create mesh and related object - my_mesh=bpy.data.meshes.new(name) - my_obj=bpy.data.objects.new(name,my_mesh) + my_mesh = bpy.data.meshes.new(name) + my_obj = bpy.data.objects.new(name, my_mesh) # Set object location in 3D space - my_obj.location = bpy.context.scene.cursor.location + my_obj.location = bpy.context.scene.cursor.location # make collection - rootcoll=bpy.context.scene.collection.children.get("Collection") + root_coll = bpy.context.scene.collection.children.get("Collection") # Link object to the scene collection - rootcoll.objects.link(my_obj) + root_coll.objects.link(my_obj) # Create object using blender function - my_mesh.from_pydata(node,[],face) + my_mesh.from_pydata(node, [], face) my_mesh.update(calc_edges=True) -def GetBPWorkFolder(): - if os.name == 'nt': - return os.path.join(tempfile.gettempdir(),'iso2mesh-'+os.environ.get('UserName'),'blenderphotonics') - else: - return os.path.join(tempfile.gettempdir(),'iso2mesh-'+os.environ.get('USER'),'blenderphotonics') -def LoadReginalMesh(meshdata, name): - n=len(meshdata.keys())-1 +def get_bp_work_folder(): + return os.path.join(gettempdir(), 'iso2mesh-', getuser(), 'blenderphotonics') + +def load_regional_mesh(mesh_data, name: str): # To import mesh.ply in batches - bbx={'min': np.array([np.inf, np.inf, np.inf]), 'max': np.array([-np.inf, -np.inf, -np.inf])} - for i in range (0,n): - surfkey='MeshTri3('+str(i+1)+')' - if(n==1): - surfkey='MeshTri3' - if (not isinstance(meshdata[surfkey], np.ndarray)): - meshdata[surfkey]=np.asarray(meshdata[surfkey],dtype=np.uint32); - meshdata[surfkey]-=1 - bbx['min']=np.amin(np.vstack((bbx['min'], np.amin(meshdata['MeshVertex3'],axis=0))), axis=0) - bbx['max']=np.amax(np.vstack((bbx['max'], np.amax(meshdata['MeshVertex3'],axis=0))), axis=0) - AddMeshFromNodeFace(meshdata['MeshVertex3'],meshdata[surfkey].tolist(),name+str(i+1)) + bbx = {'min': np.array([np.inf, np.inf, np.inf]), 'max': np.array([-np.inf, -np.inf, -np.inf])} + if len(mesh_data.keys()) == 2: + surf_keys = ['MeshTri3'] + else: + surf_keys = [f'MeshTri3({i + 1})' for i, _ in enumerate(mesh_data.keys())] + + for i, surf_key in enumerate(surf_keys): + if not isinstance(mesh_data[surf_key], np.ndarray): + mesh_data[surf_key] = np.asarray(mesh_data[surf_key], dtype=np.uint32) + mesh_data[surf_key] -= 1 + bbx['min'] = np.amin(np.vstack((bbx['min'], np.amin(mesh_data['MeshVertex3'], axis=0))), axis=0) + bbx['max'] = np.amax(np.vstack((bbx['max'], np.amax(mesh_data['MeshVertex3'], axis=0))), axis=0) + add_mesh_from_node_face(mesh_data['MeshVertex3'], mesh_data[surf_key].tolist(), f'{name}{i + 1}') print(bbx) return bbx -def LoadTetMesh(meshdata,name): - if (not isinstance(meshdata['MeshTri3'], np.ndarray)): - meshdata['MeshTri3']=np.asarray(meshdata['MeshTri3'],dtype=np.uint32); - meshdata['MeshTri3']-=1 - AddMeshFromNodeFace(meshdata['MeshVertex3'],meshdata['MeshTri3'].tolist(),name); - -def JMeshFallback(meshobj): - if('MeshSurf' in meshobj) and (not ('MeshTri3' in meshobj)): - meshobj['MeshTri3']=meshobj.pop('MeshSurf') - if('MeshNode' in meshobj) and (not ('MeshVertex3' in meshobj)): - meshobj['MeshVertex3']=meshobj.pop('MeshNode') - return meshobj + +def load_tet_mesh(mesh_data, name): + if not isinstance(mesh_data['MeshTri3'], np.ndarray): + mesh_data['MeshTri3'] = np.asarray(mesh_data['MeshTri3'], dtype=np.uint32) + mesh_data['MeshTri3'] -= 1 + add_mesh_from_node_face(mesh_data['MeshVertex3'], mesh_data['MeshTri3'].tolist(), name) + + +def jmesh_fallback(mesh_obj): + if ('MeshSurf' in mesh_obj) and (not ('MeshTri3' in mesh_obj)): + mesh_obj['MeshTri3'] = mesh_obj.pop('MeshSurf') + if ('MeshNode' in mesh_obj) and (not ('MeshVertex3' in mesh_obj)): + mesh_obj['MeshVertex3'] = mesh_obj.pop('MeshNode') + return mesh_obj + + +def normalize(x: Iterable, minimum: Any = None, maximum: Any = None, inplace: bool = False): + if minimum is None: + minimum = np.min(x) + if maximum is None: + maximum = np.max(x) + if not inplace: + try: + return (x - minimum) / (maximum - minimum) + except TypeError: + tmp_x = np.array(x) + return (tmp_x - minimum) / (maximum - minimum) + try: + x = (x - minimum) / (maximum - minimum) + except TypeError: + x = (np.array(x) - minimum) / (maximum - minimum) + + return x