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
« 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
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
17import precon3d.utility as ut
18import precon3d.factory
20# pylint: disable=wildcard-import
21from precon3d.custom_types import *
22from precon3d._my_typer_cli import CustomCLIGroup, CustomCLICommand
24# CLI
25app = typer.Typer(
26 cls=CustomCLIGroup,
27 no_args_is_help=True,
28 short_help="Crop images to regions of interest",
29)
32class CropperAttrs(NamedTuple):
33 """Represents a rectangular subarea defined by its position and dimensions.
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 """
42 subarea_x: int = None
43 subarea_y: int = None
44 subarea_width: int = None
45 subarea_height: int = None
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 )
58def crop_image_stack(config: Dict):
59 """crop a stack of images based on the provided configuration.
61 Args:
62 config (dict): Configuration parameters including image directory, output directory,
63 and downscaling settings.
64 """
66 general_attrs = precon3d.factory.create_general_attrs(config)
67 crop_attrs = create_cropper_attrs(config)
68 scale_factor = float(config["scale_factor"])
70 output_dir = general_attrs.output_directory.joinpath("Cropped")
71 output_dir.mkdir(parents=True, exist_ok=True)
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}")
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"])
88 user_image_list = image_list[user_image_range_start:user_image_range_end]
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 )
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
115def crop_image(
116 img: Image.Image, cropper_attrs: CropperAttrs, scale_factor: float = 1.0
117):
118 """_summary_
120 Args:
121 img (Image.Image): _description_
122 cropper_attrs (CropperAttrs): _description_
123 scale_factor (float, optional): _description_. Defaults to 1.0.
125 Returns:
126 _type_: _description_
127 """
128 # with Image.open(img_filepath) as img:
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)
139 if scale_factor != 1.0:
141 scaled_width = int(cropper_attrs.subarea_width * scale_factor)
142 scaled_height = int(cropper_attrs.subarea_height * scale_factor)
144 img_roi = img_roi.resize((scaled_width, scaled_height))
146 return img_roi
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 """
159 # cast to Path object if not already a Path object
160 if not isinstance(configfile, Path):
161 configfile = Path(configfile)
163 # read yaml content
164 config_dict = ut.read_config(configfile)
166 ut.current_date_and_time()
167 typer.echo(
168 f"*** Starting precon3d_downscale:process_images, {configfile.name} *** \n"
169 )
171 tic = time.perf_counter()
173 crop_image_stack(config_dict)
175 toc = time.perf_counter()
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 )
185@app.callback()
186def callback():
187 """
188 precon3d.cropper crops images to user defined regions of interest
189 """
192if __name__ == "__main__":
193 app()