Skip to content

Commit ea50fe0

Browse files
authored
Further CLEM bug fixes (#252)
* Load images as 'np.float32' before normalising to 8-bit (np.uin8) * Save LIF master metadata one level further down * Run 'process_lif_subimage' in a ProcessPoolExecutor class, and add logic to determine number of workers and threads to use * Update CPU resources requested by CLEM LIF service * Use 'tifffile.imread' instead of 'cv2.imread' to open TIFF files * Added 'imagecodecs' as an explicit dependency; needed for opening certain TIFF files * Clip after normalising instead of before * Updated tests
1 parent 93f3199 commit ea50fe0

6 files changed

Lines changed: 53 additions & 42 deletions

File tree

Helm/values.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ clem_process_raw_lifs:
4747
replicas: 0
4848
image: gcr.io/image/path
4949
command: cryoemservices.service -s CLEMProcessRawLIFs -c /cryoemservices/config/cryoemservices_config.yaml
50-
cpuRequest: "4"
51-
cpuLimit: "4"
52-
memoryLimit: 16Gi
50+
cpuRequest: "16"
51+
cpuLimit: "16"
52+
memoryLimit: 32Gi
5353
scaleOnQueueLength: true
5454
queueLengthTrigger: "4"
5555
minReplicaCount: 0

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"graypy",
3535
"healpy",
3636
"icebreaker-em",
37+
"imagecodecs", # CLEM workflow
3738
"ispyb>=11.1.2",
3839
"mrcfile",
3940
"numpy",

src/cryoemservices/util/image_processing.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import cv2
1818
import numpy as np
1919
import SimpleITK as sitk
20+
import tifffile as tf
2021
from readlif.reader import LifFile
21-
from tifffile import imwrite
2222

2323
# Create logger object to output messages with
2424
logger = logging.getLogger("cryoemservices.util.image_processing")
@@ -61,7 +61,7 @@ class TIFFImageLoader(ImageLoader):
6161
tiff_file: Path
6262

6363
def load(self) -> np.ndarray:
64-
return np.asarray(cv2.imread(self.tiff_file, flags=cv2.IMREAD_UNCHANGED))
64+
return tf.imread(self.tiff_file)
6565

6666

6767
@dataclass(frozen=True)
@@ -239,7 +239,7 @@ def load_and_convert_image(
239239
"""
240240

241241
try:
242-
arr = image_loader.load()
242+
arr = image_loader.load().astype(np.float32)
243243
if new_shape:
244244
new_y, new_x = new_shape
245245
arr = cv2.resize(
@@ -248,9 +248,9 @@ def load_and_convert_image(
248248
interpolation=cv2.INTER_AREA,
249249
)
250250
scale = 255 / ((vmax - vmin) or 1) # Downscale to 8-bit
251-
np.clip(arr, a_min=vmin, a_max=vmax, out=arr)
252-
np.subtract(arr, vmin, out=arr, casting="unsafe")
253-
np.multiply(arr, scale, out=arr, casting="unsafe")
251+
np.subtract(arr, vmin, out=arr)
252+
np.multiply(arr, scale, out=arr)
253+
np.clip(arr, a_min=0, a_max=255, out=arr)
254254
return LoadImageResult(
255255
data=arr.astype(np.uint8),
256256
frame_num=frame_num,
@@ -342,21 +342,20 @@ def load_and_resize_tile(
342342
pos_y = int(round((y0 - py0) / (py1 - py0) * parent_y_pixels))
343343

344344
# Load image and resize
345-
img = image_loader.load()
345+
img = image_loader.load().astype(np.float32)
346346
resized = cv2.resize(
347347
img,
348348
dsize=(tile_x_pixels, tile_y_pixels),
349349
interpolation=cv2.INTER_AREA,
350350
)
351351
# Normalise to 8-bit
352352
scale = 255 / ((vmax - vmin) or 1)
353-
np.clip(resized, vmin, vmax, out=resized)
354-
np.subtract(resized, vmin, out=resized, casting="unsafe")
355-
np.multiply(resized, scale, out=resized, casting="unsafe")
356-
resized = resized.astype(np.uint8)
353+
np.subtract(resized, vmin, out=resized)
354+
np.multiply(resized, scale, out=resized)
355+
np.clip(resized, a_min=0, a_max=255, out=resized)
357356

358357
return ResizeTileResult(
359-
data=resized,
358+
data=resized.astype(np.uint8),
360359
frame_num=frame_num,
361360
x0=pos_x,
362361
x1=pos_x + tile_x_pixels,
@@ -498,7 +497,7 @@ def write_stack_to_tiff(
498497
save_name = save_dir.joinpath(file_name + ".tiff")
499498

500499
# With 'bigtiff=True', they have to be pure Python class instances
501-
imwrite(
500+
tf.imwrite(
502501
save_name,
503502
array,
504503
bigtiff=use_bigtiff,

src/cryoemservices/wrappers/clem_process_raw_lifs.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import json
1010
import logging
1111
import time
12-
from concurrent.futures import ThreadPoolExecutor, as_completed
12+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
1313
from pathlib import Path
1414
from typing import Optional
1515
from xml.etree import ElementTree as ET
@@ -429,11 +429,12 @@ def process_lif_file(
429429
raw_xml_dir = (
430430
processed_dir
431431
/ "/".join(file.relative_to(processed_dir.parent).parts[1:-1])
432+
/ file.stem.replace(" ", "_")
432433
/ "metadata"
433434
)
434435
for folder in (processed_dir, raw_xml_dir):
435436
folder.mkdir(parents=True, exist_ok=True)
436-
logger.info("Created processing directory and folder to store raw metadata in")
437+
logger.info("Created processing directory and folder to store raw metadata in")
437438

438439
# Load LIF file as a LifFile class
439440
logger.info(f"Loading {file.name!r}")
@@ -464,17 +465,22 @@ def process_lif_file(
464465
logger.info(f"Examining subimages in {file.name!r}")
465466

466467
# Iterate across the series in the pool
467-
results = [
468-
process_lif_subimage(
469-
file,
470-
i,
471-
metadata,
472-
processed_dir,
473-
series_path,
474-
num_procs,
475-
)
476-
for i, (series_path, metadata) in enumerate(metadata_dict.items())
477-
]
468+
num_workers = 4 if num_procs > 4 else num_procs
469+
num_threads = (num_procs // num_workers) or 1
470+
with ProcessPoolExecutor(num_workers) as pool:
471+
futures = [
472+
pool.submit(
473+
process_lif_subimage,
474+
file,
475+
i,
476+
metadata,
477+
processed_dir,
478+
series_path,
479+
num_threads,
480+
)
481+
for i, (series_path, metadata) in enumerate(metadata_dict.items())
482+
]
483+
results = [future.result() for future in futures]
478484

479485
end_time = time.perf_counter()
480486
logger.debug(f"Processed LIF file {file} in {end_time - start_time}s")
@@ -493,7 +499,7 @@ class ProcessRawLIFsParameters(BaseModel):
493499

494500
lif_file: Path
495501
root_folder: str # The root folder under which all LIF files are saved
496-
num_procs: int = 20 # Number of processing threads to run
502+
num_procs: int = 16 # Number of processing threads to run
497503

498504

499505
class ProcessRawLIFsWrapper:

tests/services/test_clem_process_raw_lifs_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def test_lif_to_stack_service(
123123
mock_convert.assert_called_with(
124124
file=lif_file,
125125
root_folder=raw_dir.stem,
126-
number_of_processes=20,
126+
number_of_processes=16,
127127
)
128128
for result in dummy_results:
129129
for color in cast(dict[str, Any], result["output_files"]).keys():
@@ -222,7 +222,7 @@ def test_lif_to_stack_service_validation_failed(
222222
}
223223
lif_file_value: Any = str(lif_file) if valid_lif_file else 123456789
224224
root_folder: Any = raw_dir.stem if valid_root_folder else 123456789
225-
num_procs: Any = 20 if valid_num_procs else "This is a string"
225+
num_procs: Any = 16 if valid_num_procs else "This is a string"
226226
lif_to_stack_test_message = {
227227
"lif_file": lif_file_value,
228228
"root_folder": root_folder,

tests/wrappers/test_clem_process_raw_lifs_wrapper.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -313,19 +313,24 @@ def test_process_lif_file(
313313
return_value=raw_xml_metadata,
314314
)
315315

316-
# Mock out the sub-image processing function to return results iteratively
317-
mock_process_lif_subimage = mocker.patch(
318-
"cryoemservices.wrappers.clem_process_raw_lifs.process_lif_subimage"
319-
)
320-
mock_process_lif_subimage.side_effect = [
321-
create_dummy_result(
316+
# Store dummy result in mock Future objects
317+
mock_futures = []
318+
for scene_num in range(num_scenes):
319+
mock_future = MagicMock()
320+
mock_future.result.return_value = create_dummy_result(
322321
lif_file=lif_file,
323322
series_name=series_name,
324323
processed_dir=processed_dir,
325324
scene_num=scene_num,
326325
)
327-
for scene_num in range(num_scenes)
328-
]
326+
mock_futures.append(mock_future)
327+
328+
# Mock out the ProcessPoolExecutor used to run 'process_lif_subimage'
329+
mock_executor = mocker.patch(
330+
"cryoemservices.wrappers.clem_process_raw_lifs.ProcessPoolExecutor"
331+
)
332+
mock_pool = mock_executor.return_value.__enter__.return_value
333+
mock_pool.submit.side_effect = mock_futures
329334

330335
# Run the function
331336
results = process_lif_file(
@@ -335,7 +340,7 @@ def test_process_lif_file(
335340
)
336341

337342
# Check that nested list of results was collapsed correctly
338-
assert mock_process_lif_subimage.call_count == num_scenes
343+
assert mock_pool.submit.call_count == num_scenes
339344
assert len(results) == num_scenes
340345

341346

@@ -361,7 +366,7 @@ def test_lif_to_stack_wrapper(
361366
mock_send_to = mocker.patch("workflows.recipe.wrapper.RecipeWrapper.send_to")
362367

363368
# Set the number of simultaneous processes to run
364-
num_procs = 20
369+
num_procs = 16
365370

366371
# Construct a dictionary to pass to the wrapper
367372
message = {

0 commit comments

Comments
 (0)