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

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

2""" 

3 

4import argparse 

5 

6# from importlib.metadata import version 

7from pathlib import Path 

8from typing import NamedTuple 

9 

10import numpy as np 

11import yaml 

12 

13# import automesh as am 

14from automesh import Voxels 

15 

16 

17class AutomeshRecipe(NamedTuple): 

18 """ 

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

20 

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

42 

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 

52 

53 

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

55 """ 

56 Validate the given recipe. 

57 

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

59 

60 Parameters 

61 ---------- 

62 recipe : AutomeshRecipe 

63 The Recipe NamedTuple populated with items from the user input 

64 .yml file. 

65 

66 Returns 

67 ------- 

68 bool 

69 True if the recipe is valid, False otherwise. 

70 

71 Raises 

72 ------ 

73 AssertionError 

74 If any of the validation checks fail. 

75 

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

92 

93 amr = recipe 

94 

95 # Assure the .npy input file can be found 

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

97 

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

101 

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" 

108 

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" 

112 

113 return True 

114 

115 

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. 

119 

120 Parameters 

121 ---------- 

122 yml_input_file : Path 

123 The .yml recipe that specifies path variables. 

124 

125 Returns 

126 ------- 

127 True if successful, False otherwise. 

128 

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. 

137 

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

144 

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

146 

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

148 

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

150 

151 if not fin.is_file(): 

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

153 

154 file_type = fin.suffix.casefold() 

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

156 

157 if file_type not in supported_types: 

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

159 

160 db = [] 

161 

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 

169 

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

171 print(db) 

172 

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 ) 

184 

185 validate_recipe(recipe=recipe) 

186 

187 amr = recipe 

188 

189 # Run Sculpt 

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

191 # result = subprocess.run(cc) 

192 

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

205 

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

207 print(f"{cc}") 

208 

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) 

222 

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

233 

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

245 

246 return True # success 

247 

248 

249def main(): 

250 """ 

251 Runs the module from the command line. 

252 

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. 

255 

256 Parameters 

257 ---------- 

258 None 

259 

260 Returns 

261 ------- 

262 None 

263 

264 Examples 

265 -------- 

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

267 $ npy_to_mesh path/to/input.yml 

268 """ 

269 

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

275 

276 voxel_to_mesh(yml_input_file=input_file) 

277 

278 

279if __name__ == "__main__": 

280 main()