Skip to content

Commit 9625beb

Browse files
authored
Register CLEM PNGs (#719)
* Added new fields to the Pydantic models for the CLEM results to register thumbnail images for ISPyB with * Added comments to 'GridSquareParameters' table to clarify what the different fields are used for * Added new columns to the 'CLEMImageSeries' database table to store information about the thumbnails used when registering data to ISPyB * Updated DataCollectionGroup and GridSquare registration logic for CLEM workflow to make use of scaled down thumbnails instead of full-sized TIFFs
1 parent 48d6406 commit 9625beb

File tree

5 files changed

+101
-29
lines changed

5 files changed

+101
-29
lines changed

src/murfey/util/db.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,8 @@ class CLEMImageSeries(SQLModel, table=True): # type: ignore
239239
series_name: str = Field(
240240
index=True
241241
) # Name of the series, as determined from the metadata
242-
search_string: Optional[str] = Field(default=None) # Path for globbing with
242+
image_search_string: Optional[str] = Field(default=None)
243+
thumbnail_search_string: Optional[str] = Field(default=None)
243244

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

297298
# Shape and resolution information
298-
pixels_x: Optional[int] = Field(default=None)
299-
pixels_y: Optional[int] = Field(default=None)
300-
pixel_size: Optional[float] = Field(default=None)
299+
image_pixels_x: Optional[int] = Field(default=None)
300+
image_pixels_y: Optional[int] = Field(default=None)
301+
image_pixel_size: Optional[float] = Field(default=None)
302+
thumbnail_pixels_x: Optional[int] = Field(default=None)
303+
thumbnail_pixels_y: Optional[int] = Field(default=None)
304+
thumbnail_pixel_size: Optional[float] = Field(default=None)
301305
units: Optional[str] = Field(default=None)
302306

303307
# Extent of the imaged area in real space

src/murfey/util/models.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,34 @@ class Base(BaseModel):
127127

128128
class GridSquareParameters(BaseModel):
129129
tag: str
130+
image: str = ""
131+
130132
x_location: Optional[float] = None
131-
x_location_scaled: Optional[int] = None
132133
y_location: Optional[float] = None
134+
135+
# Image coordinates when overlaid on atlas (in pixels0)
136+
x_location_scaled: Optional[int] = None
133137
y_location_scaled: Optional[int] = None
138+
134139
x_stage_position: Optional[float] = None
135140
y_stage_position: Optional[float] = None
141+
142+
# Size of original image (in pixels)
136143
readout_area_x: Optional[int] = None
137144
readout_area_y: Optional[int] = None
145+
146+
# Size of thumbnail used (in pixels)
138147
thumbnail_size_x: Optional[int] = None
139148
thumbnail_size_y: Optional[int] = None
149+
140150
height: Optional[int] = None
141-
height_scaled: Optional[int] = None
142151
width: Optional[int] = None
152+
153+
# Size of image when overlaid on atlas (in pixels)
154+
height_scaled: Optional[int] = None
143155
width_scaled: Optional[int] = None
156+
144157
pixel_size: Optional[float] = None
145-
image: str = ""
146158
angle: Optional[float] = None
147159

148160

src/murfey/workflows/clem/register_align_and_merge_results.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ class AlignAndMergeResult(BaseModel):
2222
align_self: Optional[str] = None
2323
flatten: Optional[str] = "mean"
2424
align_across: Optional[str] = None
25-
composite_image: Path
25+
output_file: Path
26+
thumbnail: Optional[Path] = None
27+
thumbnail_size: Optional[tuple[int, int]] = None
2628

2729
@field_validator("image_stacks", mode="before")
2830
@classmethod

src/murfey/workflows/clem/register_preprocessing_results.py

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class CLEMPreprocessingResult(BaseModel):
3838
output_files: dict[
3939
Literal["gray", "red", "green", "blue", "cyan", "magenta", "yellow"], Path
4040
]
41+
thumbnails: dict[
42+
Literal["gray", "red", "green", "blue", "cyan", "magenta", "yellow"], Path
43+
] = {}
44+
thumbnail_size: Optional[tuple[int, int]] = None # height, width
4145
metadata: Path
4246
parent_lif: Optional[Path] = None
4347
parent_tiffs: dict[
@@ -54,7 +58,10 @@ class CLEMPreprocessingResult(BaseModel):
5458
def _is_clem_atlas(result: CLEMPreprocessingResult):
5559
# If an image has a width/height of at least 1.5 mm, it should qualify as an atlas
5660
return (
57-
max(result.pixels_x * result.pixel_size, result.pixels_y * result.pixel_size)
61+
max(
62+
result.pixels_x * result.pixel_size,
63+
result.pixels_y * result.pixel_size,
64+
)
5865
>= processing_params.atlas_threshold
5966
)
6067

@@ -149,17 +156,29 @@ def _register_clem_image_series(
149156
murfey_db.commit()
150157

151158
# Add metadata for this series
152-
clem_img_series.search_string = str(output_file.parent / "*tiff")
159+
clem_img_series.image_search_string = str(output_file.parent / "*tiff")
153160
clem_img_series.data_type = "atlas" if _is_clem_atlas(result) else "grid_square"
154161
clem_img_series.number_of_members = result.number_of_members
155-
clem_img_series.pixels_x = result.pixels_x
156-
clem_img_series.pixels_y = result.pixels_y
157-
clem_img_series.pixel_size = result.pixel_size
162+
clem_img_series.image_pixels_x = result.pixels_x
163+
clem_img_series.image_pixels_y = result.pixels_y
164+
clem_img_series.image_pixel_size = result.pixel_size
158165
clem_img_series.units = result.units
159166
clem_img_series.x0 = result.extent[0]
160167
clem_img_series.x1 = result.extent[1]
161168
clem_img_series.y0 = result.extent[2]
162169
clem_img_series.y1 = result.extent[3]
170+
# Register thumbnails if they are present
171+
if result.thumbnails and result.thumbnail_size:
172+
thumbnail = list(result.thumbnails.values())[0]
173+
clem_img_series.thumbnail_search_string = str(thumbnail.parent / "*.png")
174+
175+
thumbnail_height, thumbnail_width = result.thumbnail_size
176+
scaling_factor = min(
177+
thumbnail_height / result.pixels_y, thumbnail_width / result.pixels_x
178+
)
179+
clem_img_series.thumbnail_pixel_size = result.pixel_size / scaling_factor
180+
clem_img_series.thumbnail_pixels_x = int(result.pixels_x * scaling_factor)
181+
clem_img_series.thumbnail_pixels_y = int(result.pixels_y * scaling_factor)
163182
murfey_db.add(clem_img_series)
164183
murfey_db.commit()
165184
murfey_db.close()
@@ -189,8 +208,23 @@ def _register_dcg_and_atlas(
189208
# Determine values for atlas
190209
if _is_clem_atlas(result):
191210
output_file = list(result.output_files.values())[0]
192-
atlas_name = str(output_file.parent / "*.tiff")
193-
atlas_pixel_size = result.pixel_size
211+
# Register the thumbnail entries if they are provided
212+
if result.thumbnails and result.thumbnail_size is not None:
213+
# Glob path to the thumbnail files
214+
thumbnail = list(result.thumbnails.values())[0]
215+
atlas_name = str(thumbnail.parent / "*.png")
216+
217+
# Work out the scaling factor used
218+
thumbnail_height, thumbnail_width = result.thumbnail_size
219+
scaling_factor = min(
220+
thumbnail_width / result.pixels_x,
221+
thumbnail_height / result.pixels_y,
222+
)
223+
atlas_pixel_size = result.pixel_size / scaling_factor
224+
# Otherwise, register the TIFF files themselves
225+
else:
226+
atlas_name = str(output_file.parent / "*.tiff")
227+
atlas_pixel_size = result.pixel_size
194228
else:
195229
atlas_name = ""
196230
atlas_pixel_size = 0.0
@@ -308,8 +342,6 @@ def _register_grid_square(
308342
and atlas_entry.x1 is not None
309343
and atlas_entry.y0 is not None
310344
and atlas_entry.y1 is not None
311-
and atlas_entry.pixels_x is not None
312-
and atlas_entry.pixels_y is not None
313345
):
314346
atlas_width_real = atlas_entry.x1 - atlas_entry.x0
315347
atlas_height_real = atlas_entry.y1 - atlas_entry.y0
@@ -318,32 +350,40 @@ def _register_grid_square(
318350
return
319351

320352
for clem_img_series in clem_img_series_to_register:
353+
# Register datasets using thumbnail sizes and scales
321354
if (
322355
clem_img_series.x0 is not None
323356
and clem_img_series.x1 is not None
324357
and clem_img_series.y0 is not None
325358
and clem_img_series.y1 is not None
359+
and clem_img_series.thumbnail_pixels_x is not None
360+
and clem_img_series.thumbnail_pixels_y is not None
361+
and clem_img_series.thumbnail_pixel_size is not None
326362
):
327363
# Find pixel corresponding to image midpoint on atlas
328364
x_mid_real = (
329365
0.5 * (clem_img_series.x0 + clem_img_series.x1) - atlas_entry.x0
330366
)
331-
x_mid_px = int(x_mid_real / atlas_width_real * atlas_entry.pixels_x)
367+
x_mid_px = int(
368+
x_mid_real / atlas_width_real * clem_img_series.thumbnail_pixels_x
369+
)
332370
y_mid_real = (
333371
0.5 * (clem_img_series.y0 + clem_img_series.y1) - atlas_entry.y0
334372
)
335-
y_mid_px = int(y_mid_real / atlas_height_real * atlas_entry.pixels_y)
373+
y_mid_px = int(
374+
y_mid_real / atlas_height_real * clem_img_series.thumbnail_pixels_y
375+
)
336376

337-
# Find the number of pixels in width and height the image corresponds to on the atlas
377+
# Find the size of the image, in pixels, when overlaid the atlas
338378
width_scaled = int(
339379
(clem_img_series.x1 - clem_img_series.x0)
340380
/ atlas_width_real
341-
* atlas_entry.pixels_x
381+
* clem_img_series.thumbnail_pixels_x
342382
)
343383
height_scaled = int(
344384
(clem_img_series.y1 - clem_img_series.y0)
345385
/ atlas_height_real
346-
* atlas_entry.pixels_y
386+
* clem_img_series.thumbnail_pixels_y
347387
)
348388
else:
349389
logger.warning(
@@ -358,14 +398,18 @@ def _register_grid_square(
358398
x_location_scaled=x_mid_px,
359399
y_location=clem_img_series.y0,
360400
y_location_scaled=y_mid_px,
361-
height=clem_img_series.pixels_x,
362-
height_scaled=height_scaled,
363-
width=clem_img_series.pixels_y,
401+
readout_area_x=clem_img_series.image_pixels_x,
402+
readout_area_y=clem_img_series.image_pixels_y,
403+
thumbnail_size_x=clem_img_series.thumbnail_pixels_x,
404+
thumbnail_size_y=clem_img_series.thumbnail_pixels_y,
405+
width=clem_img_series.image_pixels_x,
364406
width_scaled=width_scaled,
365-
x_stage_position=clem_img_series.x0,
366-
y_stage_position=clem_img_series.y0,
367-
pixel_size=clem_img_series.pixel_size,
368-
image=clem_img_series.search_string,
407+
height=clem_img_series.image_pixels_y,
408+
height_scaled=height_scaled,
409+
x_stage_position=0.5 * (clem_img_series.x0 + clem_img_series.x1),
410+
y_stage_position=0.5 * (clem_img_series.y0 + clem_img_series.y1),
411+
pixel_size=clem_img_series.image_pixel_size,
412+
image=clem_img_series.thumbnail_search_string,
369413
)
370414
# Register or update the grid square entry as required
371415
if grid_square_result := murfey_db.exec(

tests/workflows/clem/test_register_preprocessing_results.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ def generate_preprocessing_messages(
7777
output_files = {color: str(series_path / f"{color}.tiff") for color in colors}
7878
for output_file in output_files.values():
7979
Path(output_file).touch(exist_ok=True)
80+
thumbnails = {
81+
color: str(series_path / ".thumbnails" / f"{color}.png") for color in colors
82+
}
83+
for v in thumbnails.values():
84+
if not (thumbnail := Path(v)).parent.exists():
85+
thumbnail.parent.mkdir(parents=True)
86+
thumbnail.touch(exist_ok=True)
87+
thumbnail_size = (512, 512)
8088
is_stack = dataset[1]
8189
is_montage = dataset[2]
8290
shape = dataset[3]
@@ -91,6 +99,8 @@ def generate_preprocessing_messages(
9199
"is_stack": is_stack,
92100
"is_montage": is_montage,
93101
"output_files": output_files,
102+
"thumbnails": thumbnails,
103+
"thumbnail_size": thumbnail_size,
94104
"metadata": str(metadata),
95105
"parent_lif": None,
96106
"parent_tiffs": {},

0 commit comments

Comments
 (0)