diff --git a/src/murfey/util/db.py b/src/murfey/util/db.py index 801ac5e4..193e0ff2 100644 --- a/src/murfey/util/db.py +++ b/src/murfey/util/db.py @@ -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" @@ -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 diff --git a/src/murfey/util/models.py b/src/murfey/util/models.py index c1ae42a5..004cb5f6 100644 --- a/src/murfey/util/models.py +++ b/src/murfey/util/models.py @@ -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 diff --git a/src/murfey/workflows/clem/register_align_and_merge_results.py b/src/murfey/workflows/clem/register_align_and_merge_results.py index fb5f563a..a85ce9da 100644 --- a/src/murfey/workflows/clem/register_align_and_merge_results.py +++ b/src/murfey/workflows/clem/register_align_and_merge_results.py @@ -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 diff --git a/src/murfey/workflows/clem/register_preprocessing_results.py b/src/murfey/workflows/clem/register_preprocessing_results.py index 2cb19e0e..a5628268 100644 --- a/src/murfey/workflows/clem/register_preprocessing_results.py +++ b/src/murfey/workflows/clem/register_preprocessing_results.py @@ -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[ @@ -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 ) @@ -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() @@ -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 @@ -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 @@ -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( @@ -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( diff --git a/tests/workflows/clem/test_register_preprocessing_results.py b/tests/workflows/clem/test_register_preprocessing_results.py index 06f7a5a0..3353be98 100644 --- a/tests/workflows/clem/test_register_preprocessing_results.py +++ b/tests/workflows/clem/test_register_preprocessing_results.py @@ -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] @@ -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": {},