Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3c8642f
Added 'images plugin' function to convert TIFF to PNG
tieneupin Dec 16, 2025
e9a98dc
Added additional step in CLEM image processing workflows to generate …
tieneupin Dec 16, 2025
b8770b3
Explicitly define target height and target width, as PIL orders width…
tieneupin Dec 16, 2025
2465e93
Fixed broken tests for CLEM 'align and merge' workflow
tieneupin Dec 17, 2025
0abfd78
Fixed broken tests for CLEM 'process raw LIFs' workflow
tieneupin Dec 17, 2025
f7daaa8
Fixed broken tests for CLEM 'process raw TIFFs' workflows
tieneupin Dec 17, 2025
e6110b2
Added '.dockerignore' file and an empty 'installers' folder to store …
tieneupin Dec 18, 2025
0351b0a
Update 'cryoemservices_cpu' Dockerfile to look for local IMOD install…
tieneupin Dec 18, 2025
fbe6580
Fixed broken 'images plugins' test
tieneupin Dec 18, 2025
47631a3
Return output file of 'tiff_to_apng' as a string
tieneupin Dec 18, 2025
dad8079
Added logs to track when jobs are submitted to Images service
tieneupin Dec 18, 2025
38ac92a
Added unit test for the 'tiff_to_apng' images plugin
tieneupin Dec 18, 2025
9034760
Added Ruff cache to Git ignore list
tieneupin Dec 18, 2025
2492fb5
Further optimsed test logic
tieneupin Dec 18, 2025
9fe32d0
Added comment to 'tiff_to_apng' function to describe what it does and…
tieneupin Dec 18, 2025
92b6343
Removed type coercion logic when evaluating the value of 'plugin_para…
tieneupin Jan 7, 2026
f56ea2f
Use 'tifffile' module to determine number of frames in TIFF file inst…
tieneupin Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Ignore everything by default
**

# Whitelist only the files and folders needed for building
!Dockerfiles/
!installers/
!src/
!pyproject.toml
!setup.py
!LICENSE
!README.md
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ venv.bak/
.dmypy.json
dmypy.json

# ruff
.ruff_cache/

# Pyre type checker
.pyre/

Expand Down
13 changes: 10 additions & 3 deletions Dockerfiles/cryoemservices_cpu
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ RUN apt-get update && \

# Build CryoEM Services, pipeliner, and IMOD in a branch image
FROM base as build

# Set up build arguments for this stage
ARG IMOD_INSTALLER=imod_5.1.9_RHEL8-64_CUDA12.0.sh

COPY ./ /cryoem-services/
RUN apt-get update && \
apt-get upgrade -y && \
Expand All @@ -31,10 +35,13 @@ RUN apt-get update && \
/cryoem-services \
http://gitlab.com/stephen-riggs/ccpem-pipeliner/-/archive/diamond_tomo/ccpem-pipeliner-diamond_tomo.zip \
&& \
curl https://bio3d.colorado.edu/imod/AMD64-RHEL5/imod_5.1.0_RHEL8-64_CUDA12.0.sh > imod_5.1.0_RHEL8-64_CUDA12.0.sh && \
chmod +x imod_5.1.0_RHEL8-64_CUDA12.0.sh && \
if [ ! -f /cryoem-services/installers/${IMOD_INSTALLER} ]; then \
echo "IMOD installer not found; downloading..."; \
curl https://bio3d.colorado.edu/imod/AMD64-RHEL5/${IMOD_INSTALLER} > /cryoem-services/installers/${IMOD_INSTALLER}; \
fi && \
chmod +x /cryoem-services/installers/${IMOD_INSTALLER} && \
mkdir imod && \
./imod_5.1.0_RHEL8-64_CUDA12.0.sh -dir imod -skip -y
/cryoem-services/installers/${IMOD_INSTALLER} -dir imod -skip -y

# Transfer completed build to base image
FROM base
Expand Down
2 changes: 2 additions & 0 deletions installers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This is placeholder folder in which installation files for the Docker containers can be placed.
The Dockerfiles will first check to see if the desired file is located here before attempting to download it.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ torch = [
"picked_particles" = "cryoemservices.services.images_plugins:picked_particles"
"picked_particles_3d_apng" = "cryoemservices.services.images_plugins:picked_particles_3d_apng"
"picked_particles_3d_central_slice" = "cryoemservices.services.images_plugins:picked_particles_3d_central_slice"
"tiff_to_apng" = "cryoemservices.services.images_plugins:tiff_to_apng"
"tilt_series_alignment" = "cryoemservices.services.images_plugins:tilt_series_alignment"
[project.entry-points."cryoemservices.services.process_recipe.filters"]
ispyb = "cryoemservices.util.process_recipe_tools:ispyb_filter"
Expand Down
7 changes: 6 additions & 1 deletion recipes/clem-align-and-merge-wrapper.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"align_across": "{align_across}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"parameters": {
"cluster": {
Expand Down Expand Up @@ -46,5 +47,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
7 changes: 6 additions & 1 deletion recipes/clem-align-and-merge.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"align_across": "{align_across}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"queue": "cluster.submission",
"service": "ALIGN-AND-MERGE"
Expand All @@ -22,5 +23,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
7 changes: 6 additions & 1 deletion recipes/clem-lif-to-stack-wrapper.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"root_folder": "{root_folder}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"parameters": {
"cluster": {
Expand Down Expand Up @@ -41,5 +42,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
7 changes: 6 additions & 1 deletion recipes/clem-lif-to-stack.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"root_folder": "{root_folder}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"queue": "cluster.submission",
"service": "LIF2STACK"
Expand All @@ -17,5 +18,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
7 changes: 6 additions & 1 deletion recipes/clem-tiff-to-stack-wrapper.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"metadata": "{metadata}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"parameters": {
"cluster": {
Expand Down Expand Up @@ -43,5 +44,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
7 changes: 6 additions & 1 deletion recipes/clem-tiff-to-stack.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"metadata": "{metadata}"
},
"output": {
"murfey_feedback": 2
"murfey_feedback": 2,
"images": 3
},
"queue": "cluster.submission",
"service": "TIFF2STACK"
Expand All @@ -19,5 +20,9 @@
"queue": "{feedback_queue}",
"service": "Murfey"
},
"3": {
"queue": "images",
"service": "Images"
},
"start": [[1, []]]
}
25 changes: 20 additions & 5 deletions src/cryoemservices/services/clem_align_and_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def call_align_and_merge(self, rw, header, message):

# Process files and collect output
try:
results = align_and_merge_stacks(
result = align_and_merge_stacks(
images=params.images,
metadata=params.metadata,
crop_to_n_frames=params.crop_to_n_frames,
Expand All @@ -82,23 +82,38 @@ def call_align_and_merge(self, rw, header, message):
)
rw.transport.nack(header)
return
if not results:
if not result:
self.log.error(
"Failed to complete the aligning and merging process for "
f"{params.series_name!r}"
)
rw.transport.nack(header)
return

# Request for PNG image to be created
images_params = {
"image_command": "tiff_to_apng",
"input_file": result["output_file"],
"output_file": result["thumbnail"],
"target_size": result["thumbnail_size"],
}
rw.send_to(
"images",
images_params,
)
self.log.info(
f"Submitted the following job to Images service: \n{images_params}"
)

# Send results to Murfey for registration
results["series_name"] = params.series_name
result["series_name"] = params.series_name
murfey_params = {
"register": "clem.register_align_and_merge_result",
"result": results,
"result": result,
}
rw.send_to("murfey_feedback", murfey_params)
self.log.info(
f"Submitted alignment and merging result for {results['series_name']!r} "
f"Submitted alignment and merging result for {result['series_name']!r} "
"to Murfey for registration"
)
rw.transport.ack(header)
Expand Down
17 changes: 17 additions & 0 deletions src/cryoemservices/services/clem_process_raw_lifs.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ def call_process_raw_lifs(self, rw, header, message):

# Send each subset of output files to Murfey for registration
for result in results:
# Request for PNG images to be created
for color in result["output_files"].keys():
images_params = {
"image_command": "tiff_to_apng",
"input_file": result["output_files"][color],
"output_file": result["thumbnails"][color],
"target_size": result["thumbnail_size"],
"color": color,
}
rw.send_to(
"images",
images_params,
)
self.log.info(
f"Submitted the following job to Images service: \n{images_params}"
)

# Create dictionary and send it to Murfey's "feedback_callback" function
murfey_params = {
"register": "clem.register_preprocessing_result",
Expand Down
17 changes: 17 additions & 0 deletions src/cryoemservices/services/clem_process_raw_tiffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ def call_process_raw_tiffs(self, rw, header, message):
rw.transport.nack(header)
return

# Request for PNG images to be created
for color in result["output_files"].keys():
images_params = {
"image_command": "tiff_to_apng",
"input_file": result["output_files"][color],
"output_file": result["thumbnails"][color],
"target_size": result["thumbnail_size"],
"color": color,
}
rw.send_to(
"images",
images_params,
)
self.log.info(
f"Submitted the following job to Images service: \n{images_params}"
)

# Create dictionary and send it to Murfey's "feedback_callback" function
murfey_params = {
"register": "clem.register_preprocessing_result",
Expand Down
82 changes: 82 additions & 0 deletions src/cryoemservices/services/images_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import pandas as pd
import PIL.Image
import starfile
import tifffile as tf
from PIL import ImageDraw, ImageEnhance, ImageFilter, ImageFont

from cryoemservices.services.cryolo import grid_bar_histogram
from cryoemservices.util.clem_array_functions import convert_to_rgb

logger = logging.getLogger("cryoemservices.services.images_plugins")
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -549,6 +551,86 @@ def picked_particles_3d_apng(plugin_params: Callable):
return outfile


def tiff_to_apng(plugin_params: Callable):
"""
Converts TIFF images/image stacks into PNGs.

This function only works with unsigned 8-bit (grayscale or RGB) TIFF images, as
Pillow cannot correctly parse 16-bit or higher channels, nor can it save float-
based images as PNGs.
"""
# Check that the essential parameters are provided
if not required_parameters(plugin_params, ["input_file", "output_file"]):
return False

# Load parameters
input_file = Path(plugin_params("input_file"))
output_file = Path(plugin_params("output_file"))
target_size: tuple[int | None, int | None] = (
tuple(plugin_params("target_size"))
if plugin_params("target_size") is not None
else (None, None)
)
target_height, target_width = target_size
color: str | None = plugin_params("color")

# Verify that the input file exists
if not input_file.is_file():
logger.error(f"File {input_file} not found")
return False

# Start of function
start = time.perf_counter()
img = PIL.Image.open(input_file)

# Determine number of frames in image
with tf.TiffFile(input_file) as tiff_file:
num_frames = len(tiff_file.pages)

# Collect image frames
frames: list[PIL.Image.Image] = []
for f in range(num_frames):
# Load relevant frame
img.seek(f)
frame = img.copy()

# Resize image if target size provided
if target_height and target_width:
frame.thumbnail((target_width, target_height))
# Convert only grayscale 8-bit images if a color LUT is provided
if color is not None and frame.mode == "L":
frame = PIL.Image.fromarray(
convert_to_rgb(np.asarray(frame, dtype="uint8"), color)
)
# Skip colorisation step and notify why
elif color is not None and frame.mode != "L":
logger.debug(f"Image format {frame.mode} not valid for color conversion")

# Append frame and load next frame in sequence
frames.append(frame)

# Save as PNG
if not output_file.parent.exists():
output_file.parent.mkdir(parents=True)
try:
frames[0].save(
output_file,
save_all=True,
append_images=frames[1:],
)
except Exception:
logger.error(f"Unable to create PNG from TIFF file {input_file}", exc_info=True)
return False

# Report on successful processing result
timing = time.perf_counter() - start
logger.info(
f"Converted TIFF to PNG {input_file} -> {output_file} in {timing:.1f} seconds",
extra={"image-processing-time": timing},
)
return str(output_file)


def tilt_series_alignment(plugin_params: Callable):
if not required_parameters(plugin_params, ["file", "aln_file", "pixel_size"]):
return False
Expand Down
Loading
Loading