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
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 03:50 +0000
1"""Use factory to generate the precon3d_types"""
3from pathlib import Path
5import precon3d.utility as ut
7# pylint: disable=wildcard-import
8from precon3d.custom_types import *
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 )
20def create_stitching_parameters(data: Dict[str, Any]) -> StitchingParams:
21 """
22 Create an instance of StitchingParams from a dictionary of attributes.
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.
29 Parameters
30 ----------
31 data : dict
32 A dictionary containing the following keys:
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.
39 Each of these dictionaries should contain the necessary keys to
40 instantiate their respective NamedTuples.
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.
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 """
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 )
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
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"])
84 return StitchingParams(
85 fiji_attrs=fiji_attrs,
86 general_attrs=general_attrs,
87 normalization_attrs=normalization_attrs,
88 stitching_attrs=stitching_attrs,
89 )
92def create_shading_parameters(data: Dict[str, Any]) -> ShadingConfig:
93 """
94 Create a ShadingConfig from a dictionary.
96 Parameters
97 ----------
98 data : Dict[str, Any]
99 A dictionary containing the user defined configuration parameters.
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'.
110 """
111 file_extension = data["general_attrs"]["file_extension"]
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 )
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)
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())
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 )
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 )
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}")
171 general_attrs = GeneralAttrs(
172 input_directory=input_dir,
173 file_extension=file_extension,
174 output_directory=output_dir,
175 )
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 # )
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)
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 )
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 )
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 )
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 )
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 )
264 return ShadingConfig(
265 general_attrs=general_attrs, manual_attrs=manual_attrs
266 )
269def create_resampler_parameters(config: dict) -> GeneralAttrs:
270 """Factory to handle user specified precon settings inputted in the yml file"""
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 )
283 # # execute function
284 # save_support_point_info(user_resample_settings)
286 return user_resampler_params