Skip to content

Commit 2f3ead5

Browse files
committed
Added comments, doc strings and type annotations
1 parent 8c85e9b commit 2f3ead5

4 files changed

Lines changed: 291 additions & 132 deletions

File tree

partinet/process_utils/guided_denoiser.py

Lines changed: 101 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,91 +7,154 @@
77
from numpy.fft import fft2, ifft2
88
from scipy.signal import gaussian
99

10-
def transform(image):
10+
11+
def transform(image: np.ndarray) -> np.ndarray:
12+
"""
13+
Normalize and scale an image to 8-bit range (0-255).
14+
15+
Args:
16+
image (np.ndarray): Input image array.
17+
18+
Returns:
19+
np.ndarray: Normalized and scaled 8-bit image.
20+
"""
1121
i_min = image.min()
1222
i_max = image.max()
13-
14-
image = ((image - i_min)/(i_max - i_min)) * 255
23+
image = ((image - i_min) / (i_max - i_min)) * 255
1524
return image.astype(np.uint8)
1625

1726

18-
def standard_scaler(image):
27+
def standard_scaler(image: np.ndarray) -> np.ndarray:
28+
"""
29+
Apply Gaussian blur and standardize the image to have zero mean and unit variance.
30+
31+
Args:
32+
image (np.ndarray): Input image array.
33+
34+
Returns:
35+
np.ndarray: Scaled and transformed image.
36+
"""
1937
kernel_size = 9
2038
image = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
2139
mu = np.mean(image)
2240
sigma = np.std(image)
23-
image = (image - mu)/sigma
24-
image = transform(image).astype(np.uint8)
25-
return image
41+
image = (image - mu) / sigma
42+
return transform(image).astype(np.uint8)
2643

27-
def contrast_enhancement(image):
28-
enhanced_image = cv2.fastNlMeansDenoising(image, None, h=10, templateWindowSize=7, searchWindowSize=21)
29-
30-
return enhanced_image
3144

45+
def contrast_enhancement(image: np.ndarray) -> np.ndarray:
46+
"""
47+
Enhance the contrast of the image using Non-Local Means denoising.
3248
33-
def gaussian_kernel(kernel_size = 3):
49+
Args:
50+
image (np.ndarray): Input image array.
51+
52+
Returns:
53+
np.ndarray: Contrast-enhanced image.
54+
"""
55+
return cv2.fastNlMeansDenoising(image, None, h=10, templateWindowSize=7, searchWindowSize=21)
56+
57+
58+
def gaussian_kernel(kernel_size: int = 3) -> np.ndarray:
59+
"""
60+
Generate a 2D Gaussian kernel.
61+
62+
Args:
63+
kernel_size (int, optional): Size of the kernel. Defaults to 3.
64+
65+
Returns:
66+
np.ndarray: Gaussian kernel.
67+
"""
3468
h = gaussian(kernel_size, kernel_size / 3).reshape(kernel_size, 1)
3569
h = np.dot(h, h.transpose())
3670
h /= np.sum(h)
3771
return h
3872

39-
def wiener_filter(img, kernel, K):
73+
74+
def wiener_filter(img: np.ndarray, kernel: np.ndarray, K: float) -> np.ndarray:
75+
"""
76+
Apply Wiener filtering to the image.
77+
78+
Args:
79+
img (np.ndarray): Input image array.
80+
kernel (np.ndarray): Convolution kernel.
81+
K (float): Noise-to-signal power ratio.
82+
83+
Returns:
84+
np.ndarray: Filtered image.
85+
"""
4086
kernel /= np.sum(kernel)
41-
dummy = np.copy(img)
42-
dummy = fft2(dummy)
43-
kernel = fft2(kernel, s = img.shape)
87+
dummy = fft2(img)
88+
kernel = fft2(kernel, s=img.shape)
4489
kernel = np.conj(kernel) / (np.abs(kernel) ** 2 + K)
45-
dummy = dummy * kernel
46-
dummy = np.abs(ifft2(dummy))
47-
return dummy
90+
dummy *= kernel
91+
return np.abs(ifft2(dummy))
92+
93+
94+
def clahe(image: np.ndarray) -> np.ndarray:
95+
"""
96+
Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to the image.
97+
98+
Args:
99+
image (np.ndarray): Input image array.
100+
101+
Returns:
102+
np.ndarray: CLAHE-processed image.
103+
"""
104+
clahe_instance = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(16, 16))
105+
return clahe_instance.apply(transform(image))
48106

49-
def clahe(image):
50-
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(16,16))
51107

52-
# Apply CLAHE to the image
53-
img_equalized = clahe.apply(transform(image))
54-
return img_equalized
108+
def guided_filter(input_image: np.ndarray, guidance_image: np.ndarray, radius: int = 20, epsilon: float = 0.1) -> np.ndarray:
109+
"""
110+
Apply guided filtering to refine the image using a guidance image.
55111
56-
def guided_filter(input_image, guidance_image, radius=20, epsilon=0.1):
57-
# Convert images to float32
112+
Args:
113+
input_image (np.ndarray): Input image array.
114+
guidance_image (np.ndarray): Guidance image array.
115+
radius (int, optional): Radius of the filter. Defaults to 20.
116+
epsilon (float, optional): Regularization parameter. Defaults to 0.1.
117+
118+
Returns:
119+
np.ndarray: Filtered image.
120+
"""
58121
input_image = input_image.astype(np.float32) / 255.0
59122
guidance_image = guidance_image.astype(np.float32) / 255.0
60123

61-
# Compute mean values of the guidance image and input image
62124
mean_guidance = cv2.boxFilter(guidance_image, -1, (radius, radius))
63125
mean_input = cv2.boxFilter(input_image, -1, (radius, radius))
64-
65-
# Compute correlation and covariance of the guidance and input images
66126
mean_guidance_input = cv2.boxFilter(guidance_image * input_image, -1, (radius, radius))
67127
covariance_guidance_input = mean_guidance_input - mean_guidance * mean_input
68128

69-
# Compute squared mean of the guidance image
70129
mean_guidance_sq = cv2.boxFilter(guidance_image * guidance_image, -1, (radius, radius))
71130
variance_guidance = mean_guidance_sq - mean_guidance * mean_guidance
72131

73-
# Compute weights and mean of the weights
74132
a = covariance_guidance_input / (variance_guidance + epsilon)
75133
b = mean_input - a * mean_guidance
76134
mean_a = cv2.boxFilter(a, -1, (radius, radius))
77135
mean_b = cv2.boxFilter(b, -1, (radius, radius))
78136

79-
# Compute the filtered image
80137
output_image = mean_a * guidance_image + mean_b
81-
82138
return transform(output_image)
83139

84140

85-
def denoise(image_path):
86-
kernel = gaussian_kernel(kernel_size = 9)
141+
def denoise(image_path: str) -> np.ndarray:
142+
"""
143+
Apply a sequence of denoising operations to an input image.
144+
145+
Args:
146+
image_path (str): Path to the input .mrc image file.
147+
148+
Returns:
149+
np.ndarray: Fully denoised image.
150+
"""
151+
kernel = gaussian_kernel(kernel_size=9)
87152
image = mrcfile.read(image_path)
88153
image = image.T
89154
image = np.rot90(image)
90155
normalized_image = standard_scaler(np.array(image))
91156
contrast_enhanced_image = contrast_enhancement(normalized_image)
92-
weiner_filtered_image = wiener_filter(contrast_enhanced_image, kernel, K = 30)
157+
weiner_filtered_image = wiener_filter(contrast_enhanced_image, kernel, K=30)
93158
clahe_image = clahe(weiner_filtered_image)
94159
guided_filter_image = guided_filter(clahe_image, weiner_filtered_image)
95-
96160
return guided_filter_image
97-

partinet/process_utils/pooled_denoise_proc.py

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,54 @@
77
import argparse
88
import gc
99
from concurrent.futures import ProcessPoolExecutor
10+
from typing import List, Tuple
1011

11-
# MAX_WORKERS = max(1, multiprocessing.cpu_count() // 2)
12+
# Function to perform CLAHE-based denoising
1213

13-
def clahe_denoise(args):
14+
def clahe_denoise(args: Tuple[str, str]) -> None:
15+
"""
16+
Applies the guided denoising algorithm to an input image and saves the denoised result.
17+
18+
Args:
19+
args (Tuple[str, str]):
20+
- src_path: Path to the source micrograph file.
21+
- dest_path: Path to save the denoised image.
22+
23+
Raises:
24+
Exception: Logs any exceptions that occur during processing.
25+
"""
1426
try:
1527
src_path, dest_path = args
28+
# Perform denoising
1629
denoised = denoise(src_path)
30+
# Save the denoised image
1731
cv2.imwrite(dest_path, denoised)
1832
logging.info(f"Processed image {src_path} to dest. {dest_path}")
1933
del denoised
2034
gc.collect()
2135
except Exception as e:
2236
logging.error(f"Failed to process {src_path}: {str(e)}")
2337

24-
def process_directory(micrographs_dir, clahe_denoised_dir,MAX_WORKERS):
38+
# Function to process all files in a directory
39+
40+
def process_directory(micrographs_dir: str, clahe_denoised_dir: str, max_workers: int) -> None:
41+
"""
42+
Processes all `.mrc` files in the given directory using parallel workers for denoising.
43+
44+
Args:
45+
micrographs_dir (str): Path to the directory containing raw micrograph files.
46+
clahe_denoised_dir (str): Path to the directory where denoised images will be saved.
47+
max_workers (int): Number of worker processes to use for parallel processing.
48+
49+
Notes:
50+
- Only `.mrc` files are processed.
51+
- Existing denoised images are skipped.
52+
"""
2553
os.makedirs(clahe_denoised_dir, exist_ok=True)
2654
logging.info(f"Directory ready: {clahe_denoised_dir}")
2755

28-
tasks = []
56+
tasks: List[Tuple[str, str]] = []
57+
# Iterate through files in the directory
2958
for file_name in os.listdir(micrographs_dir):
3059
if file_name.endswith(".mrc"):
3160
src_path = os.path.join(micrographs_dir, file_name)
@@ -35,47 +64,74 @@ def process_directory(micrographs_dir, clahe_denoised_dir,MAX_WORKERS):
3564
else:
3665
tasks.append((src_path, dest_path))
3766

38-
with ProcessPoolExecutor(max_workers=MAX_WORKERS) as executor:
67+
# Parallel processing of tasks
68+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
3969
futures = [executor.submit(clahe_denoise, task) for task in tasks]
4070
for future in futures:
41-
future.result()
71+
future.result()
4272
gc.collect()
4373

44-
def main(source_dir, project_dir, ncpu):
74+
# Main function
75+
76+
def main(source_dir: str, project_dir: str, ncpu: int) -> None:
77+
"""
78+
Main function to set up logging, determine available CPUs, and start the denoising process.
79+
80+
Args:
81+
source_dir (str): Path to the directory containing raw micrographs.
82+
project_dir (str): Path to the project directory where denoised images will be saved.
83+
ncpu (int): Number of CPUs to use for parallel processing. Defaults to half the available CPUs.
84+
85+
Notes:
86+
- Logging is configured to write messages to a log file and the console.
87+
- Ensures at least one CPU is used for processing.
88+
"""
89+
# Configure logging
4590
logging.basicConfig(filename="partinet_denoise.log", level=logging.INFO, format='%(asctime)s - %(message)s')
4691
logging.getLogger().addHandler(logging.StreamHandler())
4792

48-
denoise_dir = os.path.join(project_dir,"denoised")
93+
# Prepare output directory and log file
94+
denoise_dir = os.path.join(project_dir, "denoised")
4995
logger_name = project_dir + "/partinet_denoise.log"
5096
logging.basicConfig(filename=logger_name, level=logging.INFO, format='%(asctime)s - %(message)s')
5197

98+
# Determine number of available CPUs
5299
max_available_cpus = multiprocessing.cpu_count()
53100
max_workers = max(1, max_available_cpus // 2)
54101

102+
# Adjust number of CPUs to use
55103
if ncpu is not None:
56104
ncpu = min(ncpu, max_workers)
57105
else:
58106
ncpu = max_workers
59107

60108
logging.info(f"Using {ncpu} workers out of {max_available_cpus} available CPUs.")
61-
62-
# num_cpus = max(multiprocessing.cpu_count(),ncpu)
63-
# logging.info(f"Number of available CPUs: {num_cpus}")
64-
65109
logging.info(f"Processing raw micrographs in {source_dir}")
66110
logging.info(f"Saving denoised micrographs in {denoise_dir}")
67-
68-
# process_directory(source_dir, denoise_dir,ncpu)
69111

70-
def parse_args():
112+
# Process the directory
113+
process_directory(source_dir, denoise_dir, ncpu)
114+
115+
# Command-line argument parsing
116+
117+
def parse_args() -> argparse.Namespace:
118+
"""
119+
Parses command-line arguments for the script.
120+
121+
Returns:
122+
argparse.Namespace: Parsed arguments with the following attributes:
123+
- raw: Path to raw micrographs.
124+
- project: Path to the project directory.
125+
- ncpu: Number of CPUs to use.
126+
"""
71127
parser = argparse.ArgumentParser(description="Denoise micrographs with guided CryoSegNet-style filter")
72128
parser.add_argument("--raw", required=True, help="Path to raw micrographs")
73129
parser.add_argument("--project", required=True, help="Denoised micrographs saved in project/denoised")
74130
parser.add_argument('--ncpu', type=int, default=None, help='Number of CPUs to use')
75-
76-
77131
return parser.parse_args()
78132

133+
# Entry point for the script
134+
79135
if __name__ == "__main__":
80136
args = parse_args()
81-
main(args.raw, args.project, args.ncpu)
137+
main(args.raw, args.project, args.ncpu)

0 commit comments

Comments
 (0)