Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bc15f7e
First pass of removing files and attributes related to the Murfey TUI…
tieneupin Dec 11, 2025
761ff35
Simplified optional dependency keys used in 'pyproject.toml'; 'instru…
tieneupin Dec 11, 2025
a5b2ee8
Removed 'tui' folder from tests
tieneupin Dec 11, 2025
ce6d2fd
Added a HTTPS-based log handler to post logs in batches to a specifie…
tieneupin Dec 12, 2025
b76c121
Added new backend router for logging-related endpoints
tieneupin Dec 12, 2025
277cca3
Replaced CustomHandler with HTTPSHandler in instrument server
tieneupin Dec 12, 2025
d3effca
Removed all websocket components from the client side
tieneupin Dec 12, 2025
ff1856b
Removed apparently unused endpoints and functions from websocket router
tieneupin Dec 12, 2025
30004b1
Created 'murfey.util.logging' to store LogFilter and HTTPSHandler cla…
tieneupin Dec 12, 2025
69efbf0
Merge recent changes from 'main' branch
tieneupin Dec 12, 2025
f001b9c
Forgot to update router path
tieneupin Dec 12, 2025
a7c7deb
Fixed broken test
tieneupin Dec 12, 2025
8c2351c
Minor updates to comments and iteration logic
tieneupin Dec 12, 2025
d4e7e16
Added unit test for the HTTPS log handler
tieneupin Dec 12, 2025
f0284f1
Resolved merge conflicts with 'main' branch
tieneupin Dec 12, 2025
b35d452
Don't assert 'https_handler.close()', just run it
tieneupin Dec 12, 2025
d75cee7
Attempt at fixing warnings raised by CodeQL
tieneupin Dec 12, 2025
d7bdd8f
Fixed incorrectly named loggers
tieneupin Dec 12, 2025
bfa7ab3
Added unit test for the 'forward_logs' API endpoint
tieneupin Dec 15, 2025
ac158c0
Make test file names more specific
tieneupin Dec 15, 2025
9d36424
Updated code to remove references to the 'client' optional dependency…
tieneupin Dec 15, 2025
52e4d50
Updated documentation
tieneupin Dec 15, 2025
c9fe70c
Missed a couple of 'pip install' commands in GitHub workflows
tieneupin Dec 15, 2025
d0b40d5
Added new fields to the Pydantic models for the CLEM results to regis…
tieneupin Dec 15, 2025
b9909c3
Added comments to 'GridSquareParameters' table to clarify what the di…
tieneupin Dec 19, 2025
7fb882a
Added new columns to the 'CLEMImageSeries' database table to store in…
tieneupin Dec 19, 2025
4245c8f
Updated DataCollectionGroup and GridSquare registration logic for CLE…
tieneupin Dec 19, 2025
bab2754
Updated 'AlignAndMergeResult' Pydantic model
tieneupin Dec 19, 2025
9f0002f
Added new message keys to CLEM workflow test
tieneupin Dec 19, 2025
055512e
Merge recent changes from 'main' branch
tieneupin Dec 19, 2025
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
12 changes: 8 additions & 4 deletions src/murfey/util/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ class CLEMImageSeries(SQLModel, table=True): # type: ignore
series_name: str = Field(
index=True
) # Name of the series, as determined from the metadata
search_string: Optional[str] = Field(default=None) # Path for globbing with
image_search_string: Optional[str] = Field(default=None)
thumbnail_search_string: Optional[str] = Field(default=None)

session: Optional["Session"] = Relationship(
back_populates="image_series"
Expand Down Expand Up @@ -295,9 +296,12 @@ class CLEMImageSeries(SQLModel, table=True): # type: ignore
number_of_members: Optional[int] = Field(default=None)

# Shape and resolution information
pixels_x: Optional[int] = Field(default=None)
pixels_y: Optional[int] = Field(default=None)
pixel_size: Optional[float] = Field(default=None)
image_pixels_x: Optional[int] = Field(default=None)
image_pixels_y: Optional[int] = Field(default=None)
image_pixel_size: Optional[float] = Field(default=None)
thumbnail_pixels_x: Optional[int] = Field(default=None)
thumbnail_pixels_y: Optional[int] = Field(default=None)
thumbnail_pixel_size: Optional[float] = Field(default=None)
units: Optional[str] = Field(default=None)

# Extent of the imaged area in real space
Expand Down
18 changes: 15 additions & 3 deletions src/murfey/util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,34 @@ class Base(BaseModel):

class GridSquareParameters(BaseModel):
tag: str
image: str = ""

x_location: Optional[float] = None
x_location_scaled: Optional[int] = None
y_location: Optional[float] = None

# Image coordinates when overlaid on atlas (in pixels0)
x_location_scaled: Optional[int] = None
y_location_scaled: Optional[int] = None

x_stage_position: Optional[float] = None
y_stage_position: Optional[float] = None

# Size of original image (in pixels)
readout_area_x: Optional[int] = None
readout_area_y: Optional[int] = None

# Size of thumbnail used (in pixels)
thumbnail_size_x: Optional[int] = None
thumbnail_size_y: Optional[int] = None

height: Optional[int] = None
height_scaled: Optional[int] = None
width: Optional[int] = None

# Size of image when overlaid on atlas (in pixels)
height_scaled: Optional[int] = None
width_scaled: Optional[int] = None

pixel_size: Optional[float] = None
image: str = ""
angle: Optional[float] = None


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class AlignAndMergeResult(BaseModel):
align_self: Optional[str] = None
flatten: Optional[str] = "mean"
align_across: Optional[str] = None
composite_image: Path
output_file: Path
thumbnail: Optional[Path] = None
thumbnail_size: Optional[tuple[int, int]] = None

@field_validator("image_stacks", mode="before")
@classmethod
Expand Down
86 changes: 65 additions & 21 deletions src/murfey/workflows/clem/register_preprocessing_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class CLEMPreprocessingResult(BaseModel):
output_files: dict[
Literal["gray", "red", "green", "blue", "cyan", "magenta", "yellow"], Path
]
thumbnails: dict[
Literal["gray", "red", "green", "blue", "cyan", "magenta", "yellow"], Path
] = {}
thumbnail_size: Optional[tuple[int, int]] = None # height, width
metadata: Path
parent_lif: Optional[Path] = None
parent_tiffs: dict[
Expand All @@ -54,7 +58,10 @@ class CLEMPreprocessingResult(BaseModel):
def _is_clem_atlas(result: CLEMPreprocessingResult):
# If an image has a width/height of at least 1.5 mm, it should qualify as an atlas
return (
max(result.pixels_x * result.pixel_size, result.pixels_y * result.pixel_size)
max(
result.pixels_x * result.pixel_size,
result.pixels_y * result.pixel_size,
)
>= processing_params.atlas_threshold
)

Expand Down Expand Up @@ -149,17 +156,29 @@ def _register_clem_image_series(
murfey_db.commit()

# Add metadata for this series
clem_img_series.search_string = str(output_file.parent / "*tiff")
clem_img_series.image_search_string = str(output_file.parent / "*tiff")
clem_img_series.data_type = "atlas" if _is_clem_atlas(result) else "grid_square"
clem_img_series.number_of_members = result.number_of_members
clem_img_series.pixels_x = result.pixels_x
clem_img_series.pixels_y = result.pixels_y
clem_img_series.pixel_size = result.pixel_size
clem_img_series.image_pixels_x = result.pixels_x
clem_img_series.image_pixels_y = result.pixels_y
clem_img_series.image_pixel_size = result.pixel_size
clem_img_series.units = result.units
clem_img_series.x0 = result.extent[0]
clem_img_series.x1 = result.extent[1]
clem_img_series.y0 = result.extent[2]
clem_img_series.y1 = result.extent[3]
# Register thumbnails if they are present
if result.thumbnails and result.thumbnail_size:
thumbnail = list(result.thumbnails.values())[0]
clem_img_series.thumbnail_search_string = str(thumbnail.parent / "*.png")

thumbnail_height, thumbnail_width = result.thumbnail_size
scaling_factor = min(
thumbnail_height / result.pixels_y, thumbnail_width / result.pixels_x
)
clem_img_series.thumbnail_pixel_size = result.pixel_size / scaling_factor
clem_img_series.thumbnail_pixels_x = int(result.pixels_x * scaling_factor)
clem_img_series.thumbnail_pixels_y = int(result.pixels_y * scaling_factor)
murfey_db.add(clem_img_series)
murfey_db.commit()
murfey_db.close()
Expand Down Expand Up @@ -189,8 +208,23 @@ def _register_dcg_and_atlas(
# Determine values for atlas
if _is_clem_atlas(result):
output_file = list(result.output_files.values())[0]
atlas_name = str(output_file.parent / "*.tiff")
atlas_pixel_size = result.pixel_size
# Register the thumbnail entries if they are provided
if result.thumbnails and result.thumbnail_size is not None:
# Glob path to the thumbnail files
thumbnail = list(result.thumbnails.values())[0]
atlas_name = str(thumbnail.parent / "*.png")

# Work out the scaling factor used
thumbnail_height, thumbnail_width = result.thumbnail_size
scaling_factor = min(
thumbnail_width / result.pixels_x,
thumbnail_height / result.pixels_y,
)
atlas_pixel_size = result.pixel_size / scaling_factor
# Otherwise, register the TIFF files themselves
else:
atlas_name = str(output_file.parent / "*.tiff")
atlas_pixel_size = result.pixel_size
else:
atlas_name = ""
atlas_pixel_size = 0.0
Expand Down Expand Up @@ -308,8 +342,6 @@ def _register_grid_square(
and atlas_entry.x1 is not None
and atlas_entry.y0 is not None
and atlas_entry.y1 is not None
and atlas_entry.pixels_x is not None
and atlas_entry.pixels_y is not None
):
atlas_width_real = atlas_entry.x1 - atlas_entry.x0
atlas_height_real = atlas_entry.y1 - atlas_entry.y0
Expand All @@ -318,32 +350,40 @@ def _register_grid_square(
return

for clem_img_series in clem_img_series_to_register:
# Register datasets using thumbnail sizes and scales
if (
clem_img_series.x0 is not None
and clem_img_series.x1 is not None
and clem_img_series.y0 is not None
and clem_img_series.y1 is not None
and clem_img_series.thumbnail_pixels_x is not None
and clem_img_series.thumbnail_pixels_y is not None
and clem_img_series.thumbnail_pixel_size is not None
):
# Find pixel corresponding to image midpoint on atlas
x_mid_real = (
0.5 * (clem_img_series.x0 + clem_img_series.x1) - atlas_entry.x0
)
x_mid_px = int(x_mid_real / atlas_width_real * atlas_entry.pixels_x)
x_mid_px = int(
x_mid_real / atlas_width_real * clem_img_series.thumbnail_pixels_x
)
y_mid_real = (
0.5 * (clem_img_series.y0 + clem_img_series.y1) - atlas_entry.y0
)
y_mid_px = int(y_mid_real / atlas_height_real * atlas_entry.pixels_y)
y_mid_px = int(
y_mid_real / atlas_height_real * clem_img_series.thumbnail_pixels_y
)

# Find the number of pixels in width and height the image corresponds to on the atlas
# Find the size of the image, in pixels, when overlaid the atlas
width_scaled = int(
(clem_img_series.x1 - clem_img_series.x0)
/ atlas_width_real
* atlas_entry.pixels_x
* clem_img_series.thumbnail_pixels_x
)
height_scaled = int(
(clem_img_series.y1 - clem_img_series.y0)
/ atlas_height_real
* atlas_entry.pixels_y
* clem_img_series.thumbnail_pixels_y
)
else:
logger.warning(
Expand All @@ -358,14 +398,18 @@ def _register_grid_square(
x_location_scaled=x_mid_px,
y_location=clem_img_series.y0,
y_location_scaled=y_mid_px,
height=clem_img_series.pixels_x,
height_scaled=height_scaled,
width=clem_img_series.pixels_y,
readout_area_x=clem_img_series.image_pixels_x,
readout_area_y=clem_img_series.image_pixels_y,
thumbnail_size_x=clem_img_series.thumbnail_pixels_x,
thumbnail_size_y=clem_img_series.thumbnail_pixels_y,
width=clem_img_series.image_pixels_x,
width_scaled=width_scaled,
x_stage_position=clem_img_series.x0,
y_stage_position=clem_img_series.y0,
pixel_size=clem_img_series.pixel_size,
image=clem_img_series.search_string,
height=clem_img_series.image_pixels_y,
height_scaled=height_scaled,
x_stage_position=0.5 * (clem_img_series.x0 + clem_img_series.x1),
y_stage_position=0.5 * (clem_img_series.y0 + clem_img_series.y1),
pixel_size=clem_img_series.image_pixel_size,
image=clem_img_series.thumbnail_search_string,
)
# Register or update the grid square entry as required
if grid_square_result := murfey_db.exec(
Expand Down
10 changes: 10 additions & 0 deletions tests/workflows/clem/test_register_preprocessing_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ def generate_preprocessing_messages(
output_files = {color: str(series_path / f"{color}.tiff") for color in colors}
for output_file in output_files.values():
Path(output_file).touch(exist_ok=True)
thumbnails = {
color: str(series_path / ".thumbnails" / f"{color}.png") for color in colors
}
for v in thumbnails.values():
if not (thumbnail := Path(v)).parent.exists():
thumbnail.parent.mkdir(parents=True)
thumbnail.touch(exist_ok=True)
thumbnail_size = (512, 512)
is_stack = dataset[1]
is_montage = dataset[2]
shape = dataset[3]
Expand All @@ -91,6 +99,8 @@ def generate_preprocessing_messages(
"is_stack": is_stack,
"is_montage": is_montage,
"output_files": output_files,
"thumbnails": thumbnails,
"thumbnail_size": thumbnail_size,
"metadata": str(metadata),
"parent_lif": None,
"parent_tiffs": {},
Expand Down