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

1"""Converts a semantic segmentation into a finite element mesh.""" 

2 

3import argparse 

4 

5# from importlib.metadata import version 

6from pathlib import Path 

7from typing import NamedTuple 

8 

9import numpy as np 

10import yaml 

11 

12# import automesh as am 

13from automesh import Voxels 

14 

15 

16class AutomeshRecipe(NamedTuple): 

17 """ 

18 Defines the receipe to run automesh directly from its Python API. 

19 

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 """ 

41 

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 

51 

52 

53def validate_recipe(*, recipe: AutomeshRecipe) -> bool: 

54 """ 

55 Validate the given recipe. 

56 

57 Ensures that all values in the Recipe NamedTuple are valid. 

58 

59 Parameters 

60 ---------- 

61 recipe : AutomeshRecipe 

62 The Recipe NamedTuple populated with items from the user input 

63 .yml file. 

64 

65 Returns 

66 ------- 

67 bool 

68 True if the recipe is valid, False otherwise. 

69 

70 Raises 

71 ------ 

72 AssertionError 

73 If any of the validation checks fail. 

74 

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 """ 

91 

92 amr = recipe # automesh recipe (amr) 

93 

94 # Assure the .npy input file can be found 

95 assert amr.npy_input.is_file(), f"Cannot find {amr.npy_input}" 

96 

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}" 

100 

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" 

107 

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" 

111 

112 return True 

113 

114 

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. 

118 

119 Parameters 

120 ---------- 

121 yml_input_file : Path 

122 The .yml recipe that specifies path variables. 

123 

124 Returns 

125 ------- 

126 True if successful, False otherwise. 

127 

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. 

136 

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 """ 

143 

144 print(f"This is {Path(__file__).resolve()}") 

145 

146 fin = yml_input_file.resolve().expanduser() 

147 

148 print(f"Processing file: {fin}") 

149 

150 if not fin.is_file(): 

151 raise FileNotFoundError(f"File not found: {str(fin)}") 

152 

153 file_type = fin.suffix.casefold() 

154 supported_types = (".yaml", ".yml") 

155 

156 if file_type not in supported_types: 

157 raise TypeError("Only file types .yaml, and .yml are supported.") 

158 

159 db = [] 

160 

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 

168 

169 print(f"Success: database created from file: {fin}") 

170 print(db) 

171 

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 ) 

183 

184 validate_recipe(recipe=recipe) 

185 

186 amr = recipe 

187 

188 # Run Sculpt 

189 # cc = [str(recipe.sculpt_binary), "-i", str(path_sculpt_i)] 

190 # result = subprocess.run(cc) 

191 

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}" 

204 

205 print("Running automesh with the following command:") 

206 print(f"{cc}") 

207 

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) 

221 

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}") 

232 

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}") 

244 

245 return True # success 

246 

247 

248def main(): 

249 """ 

250 Runs the module from the command line. 

251 

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. 

254 

255 Parameters 

256 ---------- 

257 None 

258 

259 Returns 

260 ------- 

261 None 

262 

263 Examples 

264 -------- 

265 To run the module, use the following command in the terminal: 

266 $ npy_to_mesh path/to/input.yml 

267 """ 

268 

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() 

274 

275 npy_to_mesh(yml_input_file=input_file) 

276 

277 

278if __name__ == "__main__": 

279 main()