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