Coverage for src/precon3d/factory.py: 9%

78 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-23 03:50 +0000

1"""Use factory to generate the precon3d_types""" 

2 

3from pathlib import Path 

4 

5import precon3d.utility as ut 

6 

7# pylint: disable=wildcard-import 

8from precon3d.custom_types import * 

9 

10 

11def create_general_attrs(data: Dict[str, Any]) -> GeneralAttrs: 

12 """Create GeneralAttrs from a dictionary.""" 

13 return GeneralAttrs( 

14 input_directory=Path(data["general_attrs"]["input_directory"]), 

15 file_extension=data["general_attrs"]["file_extension"], 

16 output_directory=Path(data["general_attrs"]["output_directory"]), 

17 ) 

18 

19 

20def create_stitching_parameters(data: Dict[str, Any]) -> StitchingParams: 

21 """ 

22 Create an instance of StitchingParams from a dictionary of attributes. 

23 

24 This function leverages Python's unpacking feature to pass dictionary keys 

25 as keyword arguments to the constructors of various NamedTuples. This 

26 approach allows for a concise and efficient way to create instances of 

27 the NamedTuples directly from a structured dictionary. 

28 

29 Parameters 

30 ---------- 

31 data : dict 

32 A dictionary containing the following keys: 

33 

34 - 'fiji_attrs': A dictionary of attributes for Fiji processing. 

35 - 'general_attrs': A dictionary of general attributes for data processing. 

36 - 'normalization_attrs': A dictionary of normalization attributes. 

37 - 'stitching_attrs': A dictionary of stitching attributes. 

38 

39 Each of these dictionaries should contain the necessary keys to 

40 instantiate their respective NamedTuples. 

41 

42 Returns 

43 ------- 

44 StitchingParams 

45 An instance of StitchingParams containing the constructed NamedTuple 

46 instances for Fiji attributes, general attributes, normalization 

47 attributes, and stitching attributes. 

48 

49 Notes 

50 ----- 

51 The unpacking pattern `**data["key"]` allows the function to extract 

52 the key-value pairs from the dictionary and pass them as keyword 

53 arguments to the NamedTuple constructors. This eliminates the need 

54 for explicitly specifying each argument, making the code cleaner 

55 and more maintainable. 

56 """ 

57 

58 fiji_attrs = FijiAttrs(**data["fiji_attrs"]) 

59 # type hint filepaths/directories with Path 

60 general_attrs = ut.GeneralAttrs( 

61 input_directory=Path(data["general_attrs"]["input_directory"]), 

62 file_extension=data["general_attrs"]["file_extension"], 

63 output_directory=Path(data["general_attrs"]["output_directory"]), 

64 ) 

65 

66 channel_dir = data["normalization_attrs"][ 

67 "channel_flatfields_parent_directory" 

68 ] 

69 if channel_dir is not None: 

70 channel_flatfields_parent_directory = Path(channel_dir) 

71 else: 

72 channel_flatfields_parent_directory = None 

73 

74 normalization_attrs = NormalizationAttrs( 

75 use_flatfield=bool(data["normalization_attrs"]["use_flatfield"]), 

76 channel_flatfields_parent_directory=channel_flatfields_parent_directory, 

77 channel_flatfields_filename=data["normalization_attrs"][ 

78 "channel_flatfields_filename" 

79 ], 

80 ) 

81 # genaric types can be automatically unpacked 

82 stitching_attrs = StitchingAttrs(**data["stitching_attrs"]) 

83 

84 return StitchingParams( 

85 fiji_attrs=fiji_attrs, 

86 general_attrs=general_attrs, 

87 normalization_attrs=normalization_attrs, 

88 stitching_attrs=stitching_attrs, 

89 ) 

90 

91 

92def create_shading_parameters(data: Dict[str, Any]) -> ShadingConfig: 

93 """ 

94 Create a ShadingConfig from a dictionary. 

95 

96 Parameters 

97 ---------- 

98 data : Dict[str, Any] 

99 A dictionary containing the user defined configuration parameters. 

100 

101 Returns 

102 ------- 

103 ShadingConfig 

104 An instance of the ShadingConfig class initialized with the provided data. 

105 Raises 

106 ------ 

107 ValueError 

108 If the file_extension is not '.czi' or '.tif'. 

109 

110 """ 

111 file_extension = data["general_attrs"]["file_extension"] 

112 

113 # Validate file_extension 

114 if file_extension not in [".czi", ".tif"]: 

115 raise ValueError( 

116 f"Invalid file extension: {file_extension}. Only '.czi' and '.tif' are accepted." 

117 ) 

118 

119 # Validate directory paths using pathlib 

120 input_dir = Path(data["general_attrs"]["input_directory"]) 

121 output_dir = Path(data["general_attrs"]["output_directory"]) 

122 if not input_dir.is_dir(): 

123 raise ValueError(f"Invalid directory path: {input_dir}") 

124 # Create output_dir if it doesn't exist 

125 output_dir.mkdir(parents=True, exist_ok=True) 

126 

127 # Count files and subfolders in input_dir 

128 files = list(input_dir.iterdir()) 

129 num_files = sum(1 for f in files if f.is_file()) 

130 num_subfolders = sum(1 for f in files if f.is_dir()) 

131 

132 print( 

133 f"Input Directory '{input_dir}' contains {num_files} files and {num_subfolders} subfolders." 

134 ) 

135 # Validate extract_tiles based on file_extension 

136 extract_tiles = data["manual_attrs"]["extract_tiles"] 

137 if extract_tiles and file_extension != ".czi": 

138 raise ValueError( 

139 "extract_tiles can only be True if file_extension is '.czi'." 

140 ) 

141 

142 # Check if tiles have already been extracted 

143 tiles_output_dir = output_dir.joinpath("Tiles") 

144 if tiles_output_dir.is_dir() and any(tiles_output_dir.iterdir()): 

145 print(f"Tiles have already been extracted to '{tiles_output_dir}'.") 

146 # Print subfolder names and file counts 

147 print("\nContents of the 'Tiles' directory:") 

148 for subfolder in tiles_output_dir.iterdir(): 

149 if subfolder.is_dir(): 

150 file_count = len( 

151 list(subfolder.glob("*")) 

152 ) # Count files in the subfolder 

153 print( 

154 f" - Subfolder '{subfolder.name}' contains {file_count} files." 

155 ) 

156 

157 if extract_tiles: 

158 user_input = ( 

159 input( 

160 "\nTiles already exist. Do you want to continue with extraction? (yes/no): " 

161 ) 

162 .strip() 

163 .lower() 

164 ) 

165 if user_input not in ["yes", "y"]: 

166 print("Extraction aborted by the user.") 

167 extract_tiles = False # Set to False to prevent extraction 

168 # else: 

169 # print(f"'Tiles' folder doesn't exist in {output_dir}") 

170 

171 general_attrs = GeneralAttrs( 

172 input_directory=input_dir, 

173 file_extension=file_extension, 

174 output_directory=output_dir, 

175 ) 

176 

177 # Validate reference image 

178 downselection_reference_image = Path( 

179 data["manual_attrs"]["downselection_reference_image"] 

180 ) 

181 # TODO: pchao, what to do if it is created later... 

182 # if not downselection_reference_image.is_file(): 

183 # raise ValueError( 

184 # f"Invalid reference image path: {downselection_reference_image}" 

185 # ) 

186 

187 # Check for existing channel folders if reorganizing by channels 

188 reorganize_tiles_by_channels = data["manual_attrs"][ 

189 "reorganize_tiles_by_channels" 

190 ] 

191 if reorganize_tiles_by_channels: 

192 existing_folders = [ 

193 f 

194 for f in os.listdir(output_dir) 

195 if os.path.isdir(os.path.join(output_dir, f)) 

196 ] 

197 print(f"\nCheck the existing folders in the {output_dir}:") 

198 mismatches = [] 

199 channel_exists = False 

200 for channel in data["manual_attrs"]["channel_keywords"]: 

201 if channel in existing_folders: 

202 print(f" - {channel} (exists)") 

203 channel_exists = True 

204 else: 

205 print(f" - {channel} (does not exist)") 

206 mismatches.append(channel) 

207 

208 # Prompt user if there are mismatches 

209 # if mismatches: 

210 if channel_exists: 

211 user_input = ( 

212 input( 

213 "Some channel folders already exists. Do you want to continue? (yes/no): " 

214 ) 

215 .strip() 

216 .lower() 

217 ) 

218 if user_input not in ["yes", "y"]: 

219 print("Operation aborted by the user.") 

220 reorganize_tiles_by_channels = ( 

221 False # Set to False to prevent reorganization 

222 ) 

223 

224 # Check if downselection_reference_channel is in channel_keywords 

225 downselection_reference_channel = data["manual_attrs"][ 

226 "downselection_reference_channel" 

227 ] 

228 if ( 

229 downselection_reference_channel 

230 not in data["manual_attrs"]["channel_keywords"] 

231 ): 

232 raise ValueError( 

233 f"downselection_reference_channel '{downselection_reference_channel}' must be one of the channel_keywords." 

234 ) 

235 

236 # Validate downselection_ssim_threshold is between 0 and 1 

237 downselection_ssim_threshold = float( 

238 data["manual_attrs"]["downselection_ssim_threshold"] 

239 ) 

240 if not 0 <= downselection_ssim_threshold <= 1: 

241 raise ValueError( 

242 f"downselection_ssim_threshold '{downselection_ssim_threshold}' must be between 0.0 and 1.0" 

243 ) 

244 

245 # Validate downselection_nxn_subimages is greater than 1, less than 100 

246 downselection_nxn_subimages = int( 

247 data["manual_attrs"]["downselection_nxn_subimages"] 

248 ) 

249 if not 1 <= downselection_nxn_subimages <= 100: 

250 raise ValueError( 

251 f"Currently, downselection_nxn_subimages '{downselection_nxn_subimages}' only accepts values between 1 and 100." 

252 ) 

253 

254 manual_attrs = ManualShadingAttrs( 

255 extract_tiles=extract_tiles, 

256 reorganize_tiles_by_channels=reorganize_tiles_by_channels, 

257 channel_keywords=data["manual_attrs"]["channel_keywords"], 

258 downselection_reference_channel=downselection_reference_channel, 

259 downselection_reference_image=downselection_reference_image, 

260 downselection_ssim_threshold=downselection_ssim_threshold, 

261 downselection_nxn_subimages=downselection_nxn_subimages, 

262 ) 

263 

264 return ShadingConfig( 

265 general_attrs=general_attrs, manual_attrs=manual_attrs 

266 ) 

267 

268 

269def create_resampler_parameters(config: dict) -> GeneralAttrs: 

270 """Factory to handle user specified precon settings inputted in the yml file""" 

271 

272 print("Inspecting czi files for resampling: \n") 

273 user_resampler_params = GeneralAttrs( 

274 input_directory=Path( 

275 config["general_attrs"]["input_directory"] 

276 ).expanduser(), 

277 output_directory=Path( 

278 config["general_attrs"]["output_directory"] 

279 ).expanduser(), 

280 file_extension=config["general_attrs"]["file_extension"], 

281 ) 

282 

283 # # execute function 

284 # save_support_point_info(user_resample_settings) 

285 

286 return user_resampler_params