Re-structuring package layout

这个提交包含在:
Craig Warren
2022-11-09 09:29:23 +00:00
父节点 50d17f33ef
当前提交 16df30968e
共有 269 个文件被更改,包括 14 次插入261 次删除

查看文件

@@ -0,0 +1,70 @@
User libraries is a sub-package where useful Python modules contributed by users are stored.
**********
STLtoVoxel
**********
Information
===========
**Author/Contact**: Kartik Bansal (kartikbn21000@gmail.com)
This package provides the ability to directly model real objects without having to build their geometries manually using geometry primitives such as the ``#edge``, ``#plate``, ``#box`` etc.. commands. It specifically provides a tool to convert a `STL file <https://en.wikipedia.org/wiki/STL_(file_format)>`_, which can be produced by many CAD software packages, to a voxelised mesh (FDTD Yee cells) which is saved as a geometry file in HDF5 format suitable for directly importing into gprMax.
This package was created as part of the `Google Summer of Code <https://summerofcode.withgoogle.com/>`_ programme 2021 which gprMax participated. The package uses the `stl-to-voxel <https://github.com/cpederkoff/stl-to-voxel>`_ Python library by Christian Pederkoff.
**License**: `Creative Commons Attribution-ShareAlike 4.0 International License <http://creativecommons.org/licenses/by-sa/4.0/>`_
**Attribution/cite**: TBC
Package contents
================
* ``STLtoVoxel.py`` is the main script which should be executed to convert a STL file to a voxelised mesh which is saved as a geometry file in HDF5 format suitable for directly importing into gprMax.
* ``examples`` is a folder containing example STL files as well as gprMax input files that can be used to import the resulting HDF5 geometry files.
* ``convert.py``, ``perimeter.py``, ``slice.py`` are modules adapted from the `stl-to-voxel <https://github.com/cpederkoff/stl-to-voxel>`_ Python library by Christian Pederkoff.
* ``license.md`` is the license for the `stl-to-voxel <https://github.com/cpederkoff/stl-to-voxel>`_ Python library by Christian Pederkoff.
How to use the package
======================
The main script is ``stltovoxel.py`` which should be run at the command line and takes three arguments:
* ``stlfilename`` is name of STL file to convert including the path.
* ``-matindex`` is an integer which represents the index of the material to be used from the materials file which will accompany the generated geometry file (HDF5 format).
* ``-dxdyz`` is the spatial discretisation of the generated voxelised mesh. It should be given as three floating point numbers.
The physical dimensions of the voxelised object will depend on the size of the object in the original STL file and the spatial discretisation chosen.
Example
-------
To create a voxelised mesh (HDF5 geometry file) from the ubiquitous `Stanford bunny <https://en.wikipedia.org/wiki/Stanford_bunny>`_ STL file, using a spatial discretisation of 1mm and selecting material index 2 from a materials file:
.. code-block:: none
python -m user_libs.STLtoVoxel.stltovoxel user_libs/STLtoVoxel/examples/stl/Stanford_Bunny.stl -matindex 2 -dxdydz 0.001 0.001 0.001
Since the number of voxels are 108 x 88 108 and the spatial discretisation chosen is 1mm, the physical dimensions of the Stanford bunny when imported into gprMax will be 0.108 x 0.088 x 0.108mm.
The following is an example of a ``materials.txt`` file that can be used with the generated geometry file (HDF5 format) when importing into gprMax. Since ``-matindex`` is set to 2 the material with name ``hdpe``, i.e. a plastic, will be used.
.. literalinclude:: examples/materials.txt
:language: none
:linenos:
The following Python script (using the gprMax API) can be used to import the generated geometry file ``Stanford_Bunny.h5`` and materials file ``materials.txt`` into a gprMax model:
.. literalinclude:: examples/bunny.py
:language: python
:linenos:
.. figure:: ../../images_shared/stanford_bunny_stl.png
:width: 600 px
Image of the Stanford bunny STL file
.. figure:: ../../images_shared/stanford_bunny.png
:width: 600 px
FDTD geometry mesh showing the Stanford bunny

查看文件

查看文件

@@ -0,0 +1,33 @@
import numpy as np
from stl import mesh
from . import slice
def convert_meshes(meshes, discretization, parallel=True):
scale, shift, shape = slice.calculate_scale_shift(meshes, discretization)
vol = np.zeros(shape[::-1], dtype=np.int32)
for mesh_ind, org_mesh in enumerate(meshes):
slice.scale_and_shift_mesh(org_mesh, scale, shift)
cur_vol = slice.mesh_to_plane(org_mesh, shape, parallel)
vol[cur_vol] = mesh_ind + 1
return vol, scale, shift
def convert_file(input_file_path, discretization, pad=1, parallel=False):
return convert_files([input_file_path], discretization, pad=pad, parallel=parallel)
def convert_files(input_file_paths, discretization, colors=[(0, 0, 0)], pad=1, parallel=False):
meshes = []
for input_file_path in input_file_paths:
mesh_obj = mesh.Mesh.from_file(input_file_path)
org_mesh = np.hstack((mesh_obj.v0[:, np.newaxis], mesh_obj.v1[:, np.newaxis], mesh_obj.v2[:, np.newaxis]))
meshes.append(org_mesh)
vol, scale, shift = convert_meshes(meshes, discretization, parallel)
vol = np.transpose(vol)
return vol

查看文件

@@ -0,0 +1,37 @@
from pathlib import Path
import gprMax
# File path for output
fn = Path(__file__)
# Discretisation
dl = 0.001
# Domain
x = 0.148
y = 0.128
z = 0.148
scene = gprMax.Scene()
title = gprMax.Title(name=fn.with_suffix('').name)
domain = gprMax.Domain(p1=(x, y, z))
dxdydz = gprMax.Discretisation(p1=(dl, dl, dl))
time_window = gprMax.TimeWindow(time=10e-9)
scene.add(title)
scene.add(domain)
scene.add(dxdydz)
scene.add(time_window)
go = gprMax.GeometryObjectsRead(p1=(0.020, 0.020, 0.020), geofile='stl/Stanford_Bunny.h5', matfile= 'materials.txt')
gv = gprMax.GeometryView(p1=(0, 0, 0), p2=domain.props.p1,
dl=(dl, dl, dl), filename=fn.with_suffix('').name,
output_type='n')
scene.add(go)
scene.add(gv)
gprMax.run(scenes=[scene], geometry_only=True, outputfile=fn)

二进制文件未显示。

查看文件

@@ -0,0 +1,4 @@
#material: 3 0 1 0 sand
#material: 5 0.001 1 0 concrete
#material: 2.35 0 1 0 hdpe
#material: 3 0 1 0 pcb

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

查看文件

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Christian Pederkoff
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看文件

@@ -0,0 +1,68 @@
from functools import reduce
def lines_to_voxels(line_list, pixels):
current_line_indices = set()
x = 0
for (event_x, status, line_ind) in generate_line_events(line_list):
while event_x - x >= 0:
lines = reduce(lambda acc, cur: acc + [line_list[cur]], current_line_indices, [])
paint_y_axis(lines, pixels, x)
x += 1
if status == 'start':
assert line_ind not in current_line_indices
current_line_indices.add(line_ind)
elif status == 'end':
assert line_ind in current_line_indices
current_line_indices.remove(line_ind)
def slope_intercept(p1, p2):
x1, y1 = p1[:2]
x2, y2 = p2[:2]
slope = (y2 - y1) / (x2 - x1)
intercept = y1 - slope * x1
return slope, intercept
def generate_y(p1, p2, x):
slope, intercept = slope_intercept(p1, p2)
y = slope * x + intercept
return y
def paint_y_axis(lines, pixels, x):
is_black = False
target_ys = list(map(lambda line: int(generate_y(line[0], line[1], x)), lines))
target_ys.sort()
if len(target_ys) % 2:
distances = []
for i in range(len(target_ys) - 1):
distances.append(target_ys[i+1] - target_ys[i])
# https://stackoverflow.com/a/17952763
min_idx = -min((x, -i) for i, x in enumerate(distances))[1]
del target_ys[min_idx]
yi = 0
for target_y in target_ys:
if is_black:
# Bulk assign all pixels between yi -> target_y
pixels[yi:target_y, x] = True
pixels[target_y][x] = True
is_black = not is_black
yi = target_y
assert is_black is False, 'an error has occured at x%s' % x
def generate_line_events(line_list):
events = []
for i, line in enumerate(line_list):
first, second = sorted(line, key=lambda pt: pt[0])
events.append((first[0], 'start', i))
events.append((second[0], 'end', i))
return sorted(events, key=lambda tup: tup[0])

查看文件

@@ -0,0 +1,148 @@
import multiprocessing as mp
import sys
import numpy as np
from gprMax.utilities.utilities import get_terminal_width
from tqdm import tqdm
from . import perimeter
def mesh_to_plane(mesh, bounding_box, parallel):
if parallel:
pool = mp.Pool(mp.cpu_count())
result_ids = []
vol = np.zeros(bounding_box[::-1], dtype=bool)
current_mesh_indices = set()
z = 0
with tqdm(total=bounding_box[2], desc="Processing Layers", ncols=get_terminal_width() - 1, file=sys.stdout) as pbar:
for event_z, status, tri_ind in generate_tri_events(mesh):
while event_z - z >= 0:
mesh_subset = [mesh[ind] for ind in current_mesh_indices]
if parallel:
pass
else:
pbar.update(1)
_, pixels = paint_z_plane(mesh_subset, z, bounding_box[1::-1])
vol[z]=pixels
z += 1
if status == 'start':
assert tri_ind not in current_mesh_indices
current_mesh_indices.add(tri_ind)
elif status == 'end':
assert tri_ind in current_mesh_indices
current_mesh_indices.remove(tri_ind)
if parallel:
results = [r.get() for r in result_ids]
for z, pixels in results:
vol[z] = pixels
pool.close()
pool.join()
return vol
def paint_z_plane(mesh, height, plane_shape):
pixels = np.zeros(plane_shape, dtype=bool)
lines = []
for triangle in mesh:
triangle_to_intersecting_lines(triangle, height, pixels, lines)
perimeter.lines_to_voxels(lines, pixels)
return height, pixels
def linear_interpolation(p1, p2, distance):
'''
:param p1: Point 1
:param p2: Point 2
:param distance: Between 0 and 1, Lower numbers return points closer to p1.
:return: A point on the line between p1 and p2
'''
return p1 * (1-distance) + p2 * distance
def triangle_to_intersecting_lines(triangle, height, pixels, lines):
assert (len(triangle) == 3)
above = list(filter(lambda pt: pt[2] > height, triangle))
below = list(filter(lambda pt: pt[2] < height, triangle))
same = list(filter(lambda pt: pt[2] == height, triangle))
if len(same) == 3:
for i in range(0, len(same) - 1):
for j in range(i + 1, len(same)):
lines.append((same[i], same[j]))
elif len(same) == 2:
lines.append((same[0], same[1]))
elif len(same) == 1:
if above and below:
side1 = where_line_crosses_z(above[0], below[0], height)
lines.append((side1, same[0]))
else:
x = int(same[0][0])
y = int(same[0][1])
pixels[y][x] = True
else:
cross_lines = []
for a in above:
for b in below:
cross_lines.append((b, a))
side1 = where_line_crosses_z(cross_lines[0][0], cross_lines[0][1], height)
side2 = where_line_crosses_z(cross_lines[1][0], cross_lines[1][1], height)
lines.append((side1, side2))
def where_line_crosses_z(p1, p2, z):
if (p1[2] > p2[2]):
p1, p2 = p2, p1
# now p1 is below p2 in z
if p2[2] == p1[2]:
distance = 0
else:
distance = (z - p1[2]) / (p2[2] - p1[2])
return linear_interpolation(p1, p2, distance)
def calculate_scale_shift(meshes, discretization):
mesh_min = meshes[0].min(axis=(0, 1))
mesh_max = meshes[0].max(axis=(0, 1))
for mesh in meshes[1:]:
mesh_min = np.minimum(mesh_min, mesh.min(axis=(0, 1)))
mesh_max = np.maximum(mesh_max, mesh.max(axis=(0, 1)))
amplitude = mesh_max - mesh_min
#Standard Unit of STL is mm
vx=discretization[0]*1000
vy=discretization[1]*1000
vz=discretization[2]*1000
bx=int(amplitude[0]/vx)
by=int(amplitude[1]/vy)
bz=int(amplitude[2]/vz)
bounding_box = [bx+1, by+1, bz+1]
return max(1/vx,1/vy,1/vz), mesh_min, bounding_box
def scale_and_shift_mesh(mesh, scale, shift):
for i, dim_shift in enumerate(shift):
mesh[..., i] = (mesh[..., i] - dim_shift) * scale
def generate_tri_events(mesh):
# Create data structure for plane sweep
events = []
for i, tri in enumerate(mesh):
bottom, middle, top = sorted(tri, key=lambda pt: pt[2])
events.append((bottom[2], 'start', i))
events.append((top[2], 'end', i))
return sorted(events, key=lambda tup: tup[0])

查看文件

@@ -0,0 +1,39 @@
import argparse
import logging
from pathlib import Path
import h5py
from .convert import convert_file
logger = logging.getLogger(__name__)
logging.basicConfig(format='%(message)s', level=logging.INFO)
if __name__ == "__main__":
# Parse command line arguments
parser = argparse.ArgumentParser(description='Allows the user to convert a STL files to voxelized mesh.', usage='cd gprMax; python -m user_libs.STLtoVoxel.stltovoxel stlfilename matindex dx_dy_dz')
parser.add_argument('stlfilename', help='name of STL file to convert including path')
parser.add_argument('-matindex', type=int, required=True,
help='index of material to extract from STL file')
parser.add_argument('-dxdydz', nargs='+', type=float, required=True,
help='discretisation to use in voxelisation process')
args = parser.parse_args()
filename_stl = Path(args.stlfilename)
dxdydz = tuple(args.dxdydz)
logger.info(f'\nConverting STL file: {filename_stl.name}')
model_array = convert_file(filename_stl, dxdydz)
model_array[model_array==0] = -1
model_array[model_array==1] = args.matindex
logger.info(f'Number of voxels: {model_array.shape[0]} x {model_array.shape[1]} x {model_array.shape[2]}')
logger.info(f'Spatial discretisation: {dxdydz[0]} x {dxdydz[1]} x {dxdydz[2]}m')
# Write HDF5 file for gprMax using voxels
filename_hdf5 = filename_stl.with_suffix('.h5')
with h5py.File(filename_hdf5, 'w') as f:
f.create_dataset('data', data=model_array)
f.attrs['dx_dy_dz'] = (dxdydz[0], dxdydz[1], dxdydz[2])
logger.info(f'Written geometry object file: {filename_hdf5.name}')