Coverage for src/precon3d/cropper.py: 0%

67 statements  

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

1import os 

2import time 

3import numpy as np 

4from pathlib import Path 

5from PIL import Image 

6 

7Image.MAX_IMAGE_PIXELS = None 

8from typing import NamedTuple, Dict, Any 

9import typer 

10from rich.progress import ( 

11 Progress, 

12 BarColumn, 

13 TextColumn, 

14) # Import Rich progress componentsimport yaml # Assuming the configuration is in YAML format 

15 

16 

17import precon3d.utility as ut 

18import precon3d.factory 

19 

20# pylint: disable=wildcard-import 

21from precon3d.custom_types import * 

22from precon3d._my_typer_cli import CustomCLIGroup, CustomCLICommand 

23 

24# CLI 

25app = typer.Typer( 

26 cls=CustomCLIGroup, 

27 no_args_is_help=True, 

28 short_help="Crop images to regions of interest", 

29) 

30 

31 

32class CropperAttrs(NamedTuple): 

33 """Represents a rectangular subarea defined by its position and dimensions. 

34 

35 Attributes: 

36 subarea_x (int): The x-coordinate of the top-left corner of the subarea. 

37 subarea_y (int): The y-coordinate of the top-left corner of the subarea. 

38 subarea_width (int): The width of the subarea. 

39 subarea_height (int): The height of the subarea. 

40 """ 

41 

42 subarea_x: int = None 

43 subarea_y: int = None 

44 subarea_width: int = None 

45 subarea_height: int = None 

46 

47 

48def create_cropper_attrs(data: Dict[str, Any]) -> CropperAttrs: 

49 """Create CropperAttrs from a dictionary.""" 

50 return CropperAttrs( 

51 subarea_x=int(data["cropper_attrs"]["subarea_x"]), 

52 subarea_y=int(data["cropper_attrs"]["subarea_y"]), 

53 subarea_width=int(data["cropper_attrs"]["subarea_width"]), 

54 subarea_height=int(data["cropper_attrs"]["subarea_height"]), 

55 ) 

56 

57 

58def crop_image_stack(config: Dict): 

59 """crop a stack of images based on the provided configuration. 

60 

61 Args: 

62 config (dict): Configuration parameters including image directory, output directory, 

63 and downscaling settings. 

64 """ 

65 

66 general_attrs = precon3d.factory.create_general_attrs(config) 

67 crop_attrs = create_cropper_attrs(config) 

68 scale_factor = float(config["scale_factor"]) 

69 

70 output_dir = general_attrs.output_directory.joinpath("Cropped") 

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

72 

73 # Get the list of images to process 

74 image_list = ut.sorted_files( 

75 general_attrs.input_directory, general_attrs.file_extension 

76 ) 

77 print(f"Found {len(image_list)} images in {general_attrs.input_directory}") 

78 

79 if config["start_slice"] is None: 

80 user_image_range_start = 0 

81 else: 

82 user_image_range_start = int(config["start_slice"]) 

83 if config["end_slice"] is None: 

84 user_image_range_end = len(image_list) 

85 else: 

86 user_image_range_end = int(config["end_slice"]) 

87 

88 user_image_list = image_list[user_image_range_start:user_image_range_end] 

89 

90 # Use Rich's Progress for displaying progress 

91 with Progress( 

92 TextColumn("[progress.description]{task.description}"), 

93 BarColumn(), 

94 TextColumn("[progress.percentage]{task.percentage:>3.1f}%"), 

95 TextColumn("[bold blue]{task.completed}/{task.total}"), 

96 ) as progress: 

97 task = progress.add_task( 

98 "Cropping Images...", total=len(user_image_list) 

99 ) 

100 

101 # Process each image in the list 

102 for image_file in user_image_list: 

103 # for counter, image_file in enumerate(image_list): 

104 image_filepath_out = output_dir.joinpath(image_file.name) 

105 # if counter % 10 == 0: 

106 # print(f"\t\tCropping image {counter + 1} of {len(image_list)}") 

107 with Image.open(image_file) as image_input: 

108 image_cropped = crop_image( 

109 image_input, crop_attrs, scale_factor 

110 ) 

111 image_cropped.save(image_filepath_out) 

112 progress.update(task, advance=1) # Update the progress bar 

113 

114 

115def crop_image( 

116 img: Image.Image, cropper_attrs: CropperAttrs, scale_factor: float = 1.0 

117): 

118 """_summary_ 

119 

120 Args: 

121 img (Image.Image): _description_ 

122 cropper_attrs (CropperAttrs): _description_ 

123 scale_factor (float, optional): _description_. Defaults to 1.0. 

124 

125 Returns: 

126 _type_: _description_ 

127 """ 

128 # with Image.open(img_filepath) as img: 

129 

130 # Define the cropping box 

131 crop_box = ( 

132 cropper_attrs.subarea_x, 

133 cropper_attrs.subarea_y, 

134 cropper_attrs.subarea_x + cropper_attrs.subarea_width, 

135 cropper_attrs.subarea_y + cropper_attrs.subarea_height, 

136 ) 

137 img_roi = img.crop(crop_box) 

138 

139 if scale_factor != 1.0: 

140 

141 scaled_width = int(cropper_attrs.subarea_width * scale_factor) 

142 scaled_height = int(cropper_attrs.subarea_height * scale_factor) 

143 

144 img_roi = img_roi.resize((scaled_width, scaled_height)) 

145 

146 return img_roi 

147 

148 

149@app.command( 

150 cls=CustomCLICommand, 

151 name="process_images", 

152 short_help="crop images from user config", 

153) 

154def process_images(configfile: Path): 

155 """ 

156 crop images from user config 

157 """ 

158 

159 # cast to Path object if not already a Path object 

160 if not isinstance(configfile, Path): 

161 configfile = Path(configfile) 

162 

163 # read yaml content 

164 config_dict = ut.read_config(configfile) 

165 

166 ut.current_date_and_time() 

167 typer.echo( 

168 f"*** Starting precon3d_downscale:process_images, {configfile.name} *** \n" 

169 ) 

170 

171 tic = time.perf_counter() 

172 

173 crop_image_stack(config_dict) 

174 

175 toc = time.perf_counter() 

176 

177 typer.echo( 

178 f"""total time:  

179 {toc - tic:0.2f} seconds  

180 {(toc - tic)/60:0.2f} minutes  

181 {(toc - tic)/3600:0.2f} hours""" 

182 ) 

183 

184 

185@app.callback() 

186def callback(): 

187 """ 

188 precon3d.cropper crops images to user defined regions of interest 

189 """ 

190 

191 

192if __name__ == "__main__": 

193 app()