Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 6 additions & 7 deletions src/murfey/workflows/fib/register_atlas.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,15 @@


def _make_thumbnail(file: Path, metadata: FIBImageMetadata, visit_name: str):
img = PIL.Image.open(file)
img.thumbnail((512, 512))

# Find visit directory path
visit_idx = file.parts.index(visit_name)
visit_dir = list(reversed(file.parents))[visit_idx]
visit_dir = Path(*file.parts[: visit_idx + 1])

# Construct path to thumbnail
processed_dir = visit_dir / "processed"
image_number = number_from_name(file.stem)
save_path = (
processed_dir
visit_dir
/ "processed"
/ metadata.project_name
/ f"grid_{metadata.slot_number}"
/ "atlas"
Expand All @@ -41,7 +38,9 @@ def _make_thumbnail(file: Path, metadata: FIBImageMetadata, visit_name: str):
save_path.parent.mkdir(parents=True, exist_ok=True)

# Save the thumbnail
img.save(save_path)
with PIL.Image.open(file) as img:
img.thumbnail((512, 512))
img.save(save_path)
return save_path


Expand Down
48 changes: 48 additions & 0 deletions src/murfey/workflows/fib/register_lamella_evaluation_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import Any, cast

import PIL.Image
from pydantic import BaseModel
from sqlmodel import Session as SQLModelSession, select

Expand Down Expand Up @@ -40,6 +41,40 @@ def _get_timestamp(name: str):
raise ValueError(f"No datetime match found in {name}")


def _make_thumbnail(file: Path, metadata: FIBImageMetadata, visit_name: str):
# Find the visit directory
visit_idx = file.parts.index(visit_name)
visit_dir = Path(*file.parts[: visit_idx + 1])

# Lamella number field should have been populated
if not metadata.lamella_number:
raise ValueError("No lamella number associated with this visit")

# Extract parts of the file name to retain
timestamp, step_name = file.stem.split("_drift_corrected_image_")
step_name = step_name.split(" - ")[0].replace(" ", "_").lower()

# Add parts to the thumbnail name
thumbnail_name = f"lamella_{metadata.lamella_number}_{timestamp}_{step_name}.png"

# Construct full path to the thumbnail image
save_path = (
visit_dir
/ "processed"
/ metadata.project_name
/ f"grid_{metadata.slot_number}"
/ "lamella_evaluation_images"
/ thumbnail_name
)
save_path.parent.mkdir(parents=True, exist_ok=True)

# Save the thumbnail image
with PIL.Image.open(file) as img:
img.thumbnail((512, 512)) # Shrink to fit within 512 x 512
img.save(save_path)
return save_path


def _register_fib_imaging_site(
session_id: int,
metadata: FIBImageMetadata,
Expand Down Expand Up @@ -144,6 +179,19 @@ def run(
)
return {"success": False, "requeue": False}

try:
# Make a thumbnail of the image and update metadata accordingly
metadata.thumbnail_path = _make_thumbnail(
file=fib_info.lamella_image_file,
metadata=metadata,
visit_name=visit_name,
)
except Exception:
logger.warning(
f"Error creating thumbnail of file {fib_info.lamella_image_file}",
exc_info=True,
)

try:
# Register imaging site to Murfey, or update existing one
_ = _register_fib_imaging_site(fib_info.session_id, metadata, murfey_db)
Expand Down
6 changes: 3 additions & 3 deletions tests/workflows/fib/test_register_atlas.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ def test_run_with_db(
)

# Mock 'PIL.Image.open' and create a test image
mocker.patch(
"murfey.workflows.fib.register_atlas.PIL.Image.open",
return_value=PIL.Image.fromarray(np.ones((2048, 1152), dtype=np.uint8)),
mock_open = mocker.patch("murfey.workflows.fib.register_atlas.PIL.Image.open")
mock_open.__enter__.return_value = PIL.Image.fromarray(
np.ones((2048, 1152), dtype=np.uint8)
)

# Run the function and check that it's run through to completion
Expand Down
129 changes: 81 additions & 48 deletions tests/workflows/fib/test_register_lamella_evaluation_image.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from pathlib import Path
from unittest.mock import MagicMock

import numpy as np
import PIL.Image
import pytest
from pytest_mock import MockerFixture
from sqlmodel import Session as SQLModelSession, select
Expand All @@ -10,7 +10,6 @@
from murfey.util.config import MachineConfig
from murfey.workflows.fib.register_lamella_evaluation_image import (
FIBImageMetadata,
FIBLamellaImageInfo,
_register_fib_imaging_site,
run,
)
Expand Down Expand Up @@ -77,6 +76,7 @@ def test_register_fib_imaging_site_with_db(
"pos_z": 0.01,
"rotation": 1.833,
"slot_number": 2,
"lamella_number": 1,
"tilt_alpha": 0,
"tilt_beta": 0,
"pixels_x": 3072,
Expand Down Expand Up @@ -141,21 +141,21 @@ def test_register_fib_imaging_site_with_db(
assert registered_site.image_path == str(file)


def test_run(
def test_run_with_db(
mocker: MockerFixture,
visit_dir: Path,
murfey_db_session: SQLModelSession,
):
# Set up parameters

# Mock the logger
mock_logger = mocker.patch(
"murfey.workflows.fib.register_lamella_evaluation_image.logger"
# Register a Session for this test
murfey_session = MurfeyDB.Session(
id=session_id,
visit=visit_name,
name=visit_name,
instrument_name=instrument_name,
started=True,
)

# Mock the database call
mock_session = MagicMock(visit=visit_name, instrument_name=instrument_name)
mock_murfey_db = MagicMock()
mock_murfey_db.exec.return_value.one.return_value = mock_session
murfey_db_session.add(murfey_session)
murfey_db_session.commit()

# Mock the machine config
machine_config = MachineConfig(
Expand All @@ -168,18 +168,38 @@ def test_run(
return_value={instrument_name: machine_config},
)

# Create the test image file to use
file = (
# Create the test image files and their thumbnails
raw_lamella_dir = (
visit_dir
/ "autotem"
/ visit_name
/ "Sites"
/ "Lamella"
/ "LamellaEvaluationImages"
/ "2026-04-16-02-39-40_drift_corrected_image_Polishing 2 - Electron Image.png"
)
raw_lamella_dir.mkdir(parents=True, exist_ok=True)
processed_dir = (
visit_dir / "processed" / visit_name / "grid_2" / "lamella_evaluation_images"
)
processed_dir.mkdir(parents=True, exist_ok=True)

files: list[Path] = []
thumbnails: list[Path] = []
for file_name in [
"2026-04-16-02-39-38_drift_corrected_image_Finer Milling - Electron Image.png",
"2026-04-16-02-39-40_drift_corrected_image_Polishing 2 - Electron Image.png",
]:
file = raw_lamella_dir / file_name
file.touch()
files.append(file)

# Mock the results of 'parse_image_metadata'
timestamp, step_name = file_name.split("_drift_corrected_image_")
step_name = step_name.split(" - ")[0].replace(" ", "_").lower()
thumbnail = processed_dir / f"lamella_1_{timestamp}_{step_name}.png"
thumbnail.touch()
thumbnails.append(thumbnail)

# Mock the expected metadata returns
metadata_dict = {
"voltage": 2000,
"shift_x": 0,
Expand All @@ -191,49 +211,62 @@ def test_run(
"pos_z": 0.01,
"rotation": 1.833,
"slot_number": 2,
"lamella_number": 1,
"tilt_alpha": 0,
"tilt_beta": 0,
"pixels_x": 3072,
"pixels_y": 2048,
"pixels_x": 1500,
"pixels_y": 1000,
"pixel_size_x": 1e-6,
"pixel_size_y": 1e-6,
}
metadata = FIBImageMetadata(
visit_name=visit_name,
file=file,
**metadata_dict,
)
mocker.patch(
"murfey.workflows.fib.register_lamella_evaluation_image.parse_image_metadata",
return_value=metadata_dict,
)

# Mock the results of '_register_fib_image_site'
mock_register_imaging_site = mocker.patch(
"murfey.workflows.fib.register_lamella_evaluation_image._register_fib_imaging_site",
return_value=MagicMock(),
# Mock 'PIL.Image.open' and create a test image
mock_open = mocker.patch(
"murfey.workflows.fib.register_lamella_evaluation_image.PIL.Image.open"
)
mock_open.__enter__.return_value = PIL.Image.fromarray(
np.ones((1500, 1000), dtype=np.uint8)
)

# Construct the message to pass to the function
message = {
"register": "fib.register_lamella_evaluation_image",
"session_id": session_id,
"lamella_image_file": str(file),
}
fib_info = FIBLamellaImageInfo(**message)

# Run function and check that expected calls were made
result = run(message, mock_murfey_db)
for file in files:
# Construct the message to pass to the function
message = {
"register": "fib.register_lamella_evaluation_image",
"session_id": session_id,
"lamella_image_file": str(file),
}
result = run(message, murfey_db_session)
assert result["success"]

# Metadata should have been extracted and logged
mock_logger.info.assert_any_call(
"Extracted the following metadata from the image:\n"
f"{json.dumps(metadata.model_dump(), indent=2, default=str)}"
# 'PIL.Image.open' should have been called for each image
assert mock_open.call_count == len(files)

# Both thumbnails should have been generated
for thumbnail in thumbnails:
assert thumbnail.is_file()

# There should only be one ImagingSite entry associated with the visit
imaging_sites = murfey_db_session.exec(
select(MurfeyDB.ImagingSite)
.where(MurfeyDB.ImagingSite.session_id == session_id)
.where(MurfeyDB.ImagingSite.data_type == "grid_square")
).all()
assert len(imaging_sites) == 1

# The later image ("Polishing 2") should have been registered
imaging_site = imaging_sites[0]
assert (
imaging_site.image_path is not None and "Polishing 2" in imaging_site.image_path
)
# Imaging site registration function should have been called
mock_register_imaging_site.assert_called_once_with(
fib_info.session_id,
metadata,
mock_murfey_db,
assert (
imaging_site.thumbnail_path is not None
and "polishing_2" in imaging_site.thumbnail_path
)
assert result["success"]

# Site name should have been constructed correctly
assert imaging_site.site_name == f"{visit_name}/grid_2/lamella_1"
Loading