Coverage for src/recon3d/voxel_to_mesh.py: 80%
96 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-02 00:06 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-02 00:06 +0000
1"""Converts a semantic segmentation into a finite element mesh.
2"""
4import argparse
6# from importlib.metadata import version
7from pathlib import Path
8from typing import NamedTuple
10import numpy as np
11import yaml
13# import automesh as am
14from automesh import Voxels
17class AutomeshRecipe(NamedTuple):
18 """
19 Defines the receipe to run automesh directly from its Python API.
21 Attributes
22 ----------
23 npy_input : Path
24 The path to the numpy input file.
25 output_file: Path
26 The path to the mesh output file.
27 remove: List[int]
28 Voxel IDs to remove from the mesh [default: [0,]].
29 scale_x : float
30 The scaling factor along the x-axis [default: 1.0].
31 scale_y : float
32 The scaling factor along the y-axis [default: 1.0].
33 scale_z : float
34 The scaling factor along the z-axis [default: 1.0].
35 translate_x : float
36 The translation along the x-axis [default: 0.0].
37 translate_y : float
38 The translation along the y-axis [default: 0.0].
39 translate_z : float
40 The translation along the z-axis [default: 0.0].
41 """
43 npy_input: Path
44 output_file: Path
45 remove: list[int]
46 scale_x: float = 1.0
47 scale_y: float = 1.0
48 scale_z: float = 1.0
49 translate_x: float = 0.0
50 translate_y: float = 0.0
51 translate_z: float = 0.0
54def validate_recipe(*, recipe: AutomeshRecipe) -> bool:
55 """
56 Validate the given recipe.
58 Ensures that all values in the Recipe NamedTuple are valid.
60 Parameters
61 ----------
62 recipe : AutomeshRecipe
63 The Recipe NamedTuple populated with items from the user input
64 .yml file.
66 Returns
67 -------
68 bool
69 True if the recipe is valid, False otherwise.
71 Raises
72 ------
73 AssertionError
74 If any of the validation checks fail.
76 Examples
77 --------
78 >>> recipe = AutomeshRecipe(
79 ... npy_input=Path("/path/to/input.npy"),
80 ... output_file=Path("/path/to/output.inp"),
81 ... remove=[0,],
82 ... scale_x=1.0,
83 ... scale_y=1.0,
84 ... scale_z=1.0,
85 ... translate_x=0.0,
86 ... translate_y=0.0,
87 ... translate_z=0.0,
88 ... )
89 >>> validate_recipe(recipe=recipe)
90 True
91 """
93 amr = recipe
95 # Assure the .npy input file can be found
96 assert amr.npy_input.is_file(), f"Cannot find {amr.npy_input}"
98 # Assure the folder that will contain the output file can be found
99 output_path = amr.output_file.parent
100 assert output_path.is_dir(), f"Cannot find output path {output_path}"
102 removes = amr.remove
103 assert isinstance(removes, list), f"{removes} must be a list"
104 all_ints = [isinstance(x, int) for x in removes]
105 all_nonneg = [x >= 0 for x in removes]
106 assert all(all_ints), f"{removes} must be a list of integers"
107 assert all(all_nonneg), f"{removes} must be a list of non-negative integers"
109 assert amr.scale_x > 0.0, f"{amr.scale_x} must be > 0.0"
110 assert amr.scale_y > 0.0, f"{amr.scale_y} must be > 0.0"
111 assert amr.scale_z > 0.0, f"{amr.scale_z} must be > 0.0"
113 return True
116def voxel_to_mesh(*, yml_input_file: Path) -> bool:
117 """
118 Convert a .npy file specified in a yml recipe to a finite element mesh.
120 Parameters
121 ----------
122 yml_input_file : Path
123 The .yml recipe that specifies path variables.
125 Returns
126 -------
127 True if successful, False otherwise.
129 Raises
130 ------
131 FileNotFoundError
132 If the input .yml file is not found.
133 TypeError
134 If the input file type is not supported.
135 OSError
136 If there is an error with the yml module.
138 Examples
139 --------
140 >>> yml_input_file = Path("/path/to/recipe.yml")
141 >>> voxel_to_mesh(yml_input_file=yml_input_file)
142 0
143 """
145 print(f"This is {Path(__file__).resolve()}")
147 fin = yml_input_file.resolve().expanduser()
149 print(f"Processing file: {fin}")
151 if not fin.is_file():
152 raise FileNotFoundError(f"File not found: {str(fin)}")
154 file_type = fin.suffix.casefold()
155 supported_types = (".yaml", ".yml")
157 if file_type not in supported_types:
158 raise TypeError("Only file types .yaml, and .yml are supported.")
160 db = []
162 try:
163 with open(file=fin, mode="r", encoding="utf-8") as stream:
164 db = yaml.load(stream, Loader=yaml.SafeLoader) # overwrite
165 except yaml.YAMLError as error:
166 print(f"Error with yml module: {error}")
167 print(f"Could not open or decode: {fin}")
168 raise OSError from error
170 print(f"Success: database created from file: {fin}")
171 print(db)
173 recipe = AutomeshRecipe(
174 npy_input=Path(db["npy_input"]).expanduser(),
175 output_file=Path(db["output_file"]).expanduser(),
176 remove=db["remove"],
177 scale_x=db["scale_x"],
178 scale_y=db["scale_y"],
179 scale_z=db["scale_z"],
180 translate_x=db["translate_x"],
181 translate_y=db["translate_y"],
182 translate_z=db["translate_z"],
183 )
185 validate_recipe(recipe=recipe)
187 amr = recipe
189 # Run Sculpt
190 # cc = [str(recipe.sculpt_binary), "-i", str(path_sculpt_i)]
191 # result = subprocess.run(cc)
193 # run automesh as a subprocess
194 cc = "mesh "
195 cc += f"-i {amr.npy_input} "
196 cc += f"-o {amr.output_file} "
197 for ri in amr.remove:
198 cc += f"-r {ri} "
199 cc += f"--xscale {amr.scale_x} "
200 cc += f"--yscale {amr.scale_y} "
201 cc += f"--zscale {amr.scale_z} "
202 cc += f"--xtranslate {amr.translate_x} "
203 cc += f"--ytranslate {amr.translate_y} "
204 cc += f"--ztranslate {amr.translate_z}"
206 print("Running automesh with the following command:")
207 print(f"{cc}")
209 # The automesh library stores .npy arrays as [x[y[z]]], whereas the
210 # image processing convention we wish to use is [z[y[x]]]. So, save
211 # temporary .npy file that is reordered, and feel that to automesh
212 # instead of the original .npy file.
213 aa = np.load(str(amr.npy_input))
214 bb = amr.npy_input.stem + "_xyz.npy"
215 # temp_npy = amr.npy_input.parent.joinpath("temp.npy"
216 temp_npy = amr.npy_input.parent.joinpath(bb)
217 print(f"Created temporary file in xyz order for automesh: {temp_npy}")
218 cc = aa.transpose(2, 1, 0)
219 # TODO: Check if the z-order needs to be reveresed based on the CT serial
220 # section data.
221 np.save(temp_npy, cc)
223 # voxels = am.Voxels.from_npy
224 # voxels = Voxels.from_npy(str(amr.npy_input))
225 voxels = Voxels.from_npy(str(temp_npy))
226 elements = voxels.as_finite_elements(
227 remove=amr.remove,
228 scale=[amr.scale_x, amr.scale_y, amr.scale_z],
229 translate=[amr.translate_x, amr.translate_y, amr.translate_z],
230 )
231 elements.write_exo(str(amr.output_file))
232 print(f"Wrote output file: {amr.output_file}")
234 # clean up the temporary file:
235 try:
236 # Delete the file
237 temp_npy.unlink()
238 print(f"Temporary file successfully deleted: {temp_npy}")
239 except FileNotFoundError:
240 print(f"Temporary file does not exist: {temp_npy}")
241 except PermissionError:
242 print(f"Permission denied: Unable to delete tempoary file: {temp_npy}")
243 except Exception as e:
244 print(f"An error occurred: {e}")
246 return True # success
249def main():
250 """
251 Runs the module from the command line.
253 This function serves as the entry point for terminal-based access to the
254 module. It processes a YAML input file to a finite element mesh.
256 Parameters
257 ----------
258 None
260 Returns
261 -------
262 None
264 Examples
265 --------
266 To run the module, use the following command in the terminal:
267 $ npy_to_mesh path/to/input.yml
268 """
270 parser = argparse.ArgumentParser()
271 parser.add_argument("input_file", help="the .yml npy to mesh recipe")
272 args = parser.parse_args()
273 input_file = args.input_file
274 input_file = Path(input_file).expanduser()
276 voxel_to_mesh(yml_input_file=input_file)
279if __name__ == "__main__":
280 main()