From f31a964a034db5024c9a608a97fd219006706a9d Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 14:08:06 +0100 Subject: [PATCH 1/6] More readable way of getting the visit directory --- src/murfey/workflows/fib/register_atlas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/murfey/workflows/fib/register_atlas.py b/src/murfey/workflows/fib/register_atlas.py index a3cb0ef20..eba5258ac 100644 --- a/src/murfey/workflows/fib/register_atlas.py +++ b/src/murfey/workflows/fib/register_atlas.py @@ -26,7 +26,7 @@ def _make_thumbnail(file: Path, metadata: FIBImageMetadata, visit_name: str): # 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" From c85f26afcc2816cdbc08df1f0d23ab7fa9d5fd25 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 14:43:48 +0100 Subject: [PATCH 2/6] Remove unnecessary variable assignment and open image in a context manager --- src/murfey/workflows/fib/register_atlas.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/murfey/workflows/fib/register_atlas.py b/src/murfey/workflows/fib/register_atlas.py index eba5258ac..8b7ca0315 100644 --- a/src/murfey/workflows/fib/register_atlas.py +++ b/src/murfey/workflows/fib/register_atlas.py @@ -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 = 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" @@ -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 From 02220486d2718376d31f5eaf830a93b1f4d62e08 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 15:16:32 +0100 Subject: [PATCH 3/6] Updated test for 'register_atlas' to reflect opening image using context manager --- tests/workflows/fib/test_register_atlas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/workflows/fib/test_register_atlas.py b/tests/workflows/fib/test_register_atlas.py index 1128ef5e3..358d27f38 100644 --- a/tests/workflows/fib/test_register_atlas.py +++ b/tests/workflows/fib/test_register_atlas.py @@ -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 From 5de92fb34522492bf9f6af382a4af71e4127c6eb Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 15:42:46 +0100 Subject: [PATCH 4/6] Add logic to create a thumbnail image for the lamella evaluation image being registered --- .../fib/register_lamella_evaluation_image.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/murfey/workflows/fib/register_lamella_evaluation_image.py b/src/murfey/workflows/fib/register_lamella_evaluation_image.py index 050b11d51..529fa8a06 100644 --- a/src/murfey/workflows/fib/register_lamella_evaluation_image.py +++ b/src/murfey/workflows/fib/register_lamella_evaluation_image.py @@ -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 @@ -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, @@ -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) From 2df5041a53fc599334de8a8752d96566092e6aef Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 16:06:50 +0100 Subject: [PATCH 5/6] Updated test to be run using the test database --- .../test_register_lamella_evaluation_image.py | 126 +++++++++++------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/tests/workflows/fib/test_register_lamella_evaluation_image.py b/tests/workflows/fib/test_register_lamella_evaluation_image.py index 1f984315a..c627770bf 100644 --- a/tests/workflows/fib/test_register_lamella_evaluation_image.py +++ b/tests/workflows/fib/test_register_lamella_evaluation_image.py @@ -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 @@ -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, ) @@ -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, @@ -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( @@ -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 = [] + thumbnails = [] + 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, @@ -191,49 +211,59 @@ 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) + + # There should 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.thumbnail_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" From 84e30bd1d27249446fc6e84dab944d913b6b85a2 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Sep 2026 16:18:45 +0100 Subject: [PATCH 6/6] Fixed typo and checked that thumbnails were generated successfully --- .../fib/test_register_lamella_evaluation_image.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/workflows/fib/test_register_lamella_evaluation_image.py b/tests/workflows/fib/test_register_lamella_evaluation_image.py index c627770bf..ecd4c8380 100644 --- a/tests/workflows/fib/test_register_lamella_evaluation_image.py +++ b/tests/workflows/fib/test_register_lamella_evaluation_image.py @@ -183,8 +183,8 @@ def test_run_with_db( ) processed_dir.mkdir(parents=True, exist_ok=True) - files = [] - thumbnails = [] + 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", @@ -246,7 +246,11 @@ def test_run_with_db( # 'PIL.Image.open' should have been called for each image assert mock_open.call_count == len(files) - # There should be one ImagingSite entry associated with the visit + # 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) @@ -257,8 +261,7 @@ def test_run_with_db( # 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.thumbnail_path + imaging_site.image_path is not None and "Polishing 2" in imaging_site.image_path ) assert ( imaging_site.thumbnail_path is not None