Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
92b7ddf
Rearranged stag position fields based on their location in the 'Proje…
tieneupin Apr 30, 2026
8ad14f0
Moved the 'make_gif' function over to the backend server's FIB workfl…
tieneupin Apr 30, 2026
b100c56
Updated the logic used to post requests to make GIFs from the drift c…
tieneupin Apr 30, 2026
a9e5637
Merged recent changes from 'main' branch
tieneupin Apr 30, 2026
b7f740e
Need 'default_factory' for list
tieneupin Apr 30, 2026
20f3fd1
Use current file's destination path to determine GIF file path
tieneupin Apr 30, 2026
654561e
One more log to indicate successful creation of GIF file
tieneupin Apr 30, 2026
04b7358
try-except the 'chmod' function
tieneupin Apr 30, 2026
2c11304
Migrated and fixed test for 'make_gif' function
tieneupin Apr 30, 2026
8518690
Forgot to sort images
tieneupin Apr 30, 2026
b8d2b4f
Updated test for FIBContext to handle new drift correction FIB image …
tieneupin Apr 30, 2026
d9db531
Sanitise and verify FIB output file path
tieneupin May 5, 2026
cc44ea0
Change sanitisation logic
tieneupin May 5, 2026
316d837
PIL.Image conditional no longer needed
tieneupin May 5, 2026
849d597
Add logic to check that 'make_gif' commands were being sent correctly…
tieneupin May 5, 2026
cf72219
Create a class function to determine the output directory to save pro…
tieneupin May 5, 2026
016f3c6
Updated tests
tieneupin May 5, 2026
c9293b1
Forgot to include most nested directory in iterative 'os.chmod' run
tieneupin May 5, 2026
94ecd0b
Adjust 'mkdir' logic
tieneupin May 5, 2026
b27e198
Fixed broken test
tieneupin May 5, 2026
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
328 changes: 239 additions & 89 deletions src/murfey/client/contexts/fib.py

Large diffs are not rendered by default.

74 changes: 0 additions & 74 deletions src/murfey/server/api/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

import numpy as np
import sqlalchemy
from fastapi import APIRouter, Depends
from ispyb.sqlalchemy import (
Expand All @@ -21,11 +20,6 @@
from sqlmodel import col, select
from werkzeug.utils import secure_filename

try:
from PIL import Image
except ImportError:
Image = None

try:
from smartem_backend.api_client import SmartEMAPIClient
from smartem_common.schemas import (
Expand Down Expand Up @@ -1194,71 +1188,3 @@ def register_sample_image(
if _transport_object:
return _transport_object.do_insert_sample_image(record)
return {"success": False}


class MillingParameters(BaseModel):
lamella_number: int
images: List[str]
raw_directory: str


@correlative_router.post(
"/year/{year}/visits/{visit_name}/sessions/{session_id}/make_milling_gif"
)
async def make_gif(
year: int,
visit_name: str,
session_id: int,
gif_params: MillingParameters,
db=murfey_db,
):
instrument_name = (
db.exec(select(Session).where(Session.id == session_id)).one().instrument_name
)
machine_config = get_machine_config(instrument_name=instrument_name)[
instrument_name
]
output_dir = (
(machine_config.rsync_basepath or Path("")).resolve()
/ secure_filename(str(year))
/ secure_filename(visit_name)
/ "processed"
)
output_dir.mkdir(exist_ok=True)
os.chmod(output_dir, mode=machine_config.mkdir_chmod)
output_dir = output_dir / secure_filename(gif_params.raw_directory)
output_dir.mkdir(exist_ok=True)
os.chmod(output_dir, mode=machine_config.mkdir_chmod)
output_path = output_dir / f"lamella_{gif_params.lamella_number}_milling.gif"

if Image is not None:
images = [Image.open(f) for f in gif_params.images]
else:
images = []
for im in images:
im.thumbnail((512, 512))

# Normalize and convert individual frames to 8-bit
arr: list[np.ndarray] = []
for im in images:
frame = np.array(im).astype(np.float32)
vmin, vmax = np.percentile(frame, (0.5, 99.5))
scale = 255 / ((vmax - vmin) or 1)
np.clip(frame, a_min=vmin, a_max=vmax, out=frame)
np.subtract(frame, vmin, out=frame)
np.multiply(frame, scale, out=frame)
arr.append(frame.astype(np.uint8))
arr = np.array(arr).astype(np.uint8)

# Convert back to Image objects and save as GIF
converted = [Image.fromarray(arr[f], mode="L") for f in range(len(images))]
converted[0].save(
output_path,
format="GIF",
append_images=converted[1:],
save_all=True,
duration=30,
loop=0,
)

return {"output_gif": str(output_path)}
82 changes: 81 additions & 1 deletion src/murfey/server/api/workflow_fib.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import json
import logging
import os
from importlib.metadata import entry_points
from pathlib import Path

import numpy as np
import PIL.Image
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlmodel import Session
from sqlmodel import Session, select

import murfey.util.db as MurfeyDB
from murfey.server.api.auth import validate_instrument_token
from murfey.server.murfey_db import murfey_db
from murfey.util import sanitise_path
from murfey.util.config import get_machine_config
from murfey.util.models import LamellaSiteInfo

logger = logging.getLogger("murfey.server.api.workflow_fib")
Expand Down Expand Up @@ -57,3 +63,77 @@ def register_fib_milling_progress(
"Received the following FIB metadata for registration:\n"
f"{json.dumps(site_info.model_dump(exclude_none=True), indent=2, default=str)}"
)


class FIBGIFParameters(BaseModel):
lamella_number: int
images: list[Path]
output_file: Path


@router.post("/sessions/{session_id}/make_gif")
async def make_gif(
session_id: int,
gif_params: FIBGIFParameters,
db=murfey_db,
):
# Load machine config and session info
session_entry = db.exec(
select(MurfeyDB.Session).where(MurfeyDB.Session.id == session_id)
).one()
instrument_name = session_entry.instrument_name
visit_name = session_entry.visit
machine_config = get_machine_config(instrument_name=instrument_name)[
instrument_name
]
rsync_basepath = machine_config.rsync_basepath or Path(".").resolve()

# Sanitise and verify that the output directory is relative to rsync basepath
output_file = sanitise_path(gif_params.output_file)
if not output_file.is_relative_to(rsync_basepath):
logger.error("Output file path is not permitted")
raise ValueError

# Create folders in the visit directory and onwards and change permissions
visit_index = output_file.parts.index(visit_name)
for current_path in list(reversed(output_file.parents))[visit_index + 1 :]:
if not current_path.exists():
current_path.mkdir(parents=True)
logger.debug(f"Created output directory {current_path}")
try:
os.chmod(current_path, mode=machine_config.mkdir_chmod)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
except PermissionError:
logger.warning(
f"Insufficient permissions to modify directory {current_path}"
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
)
continue

# Load the images as PIL Image objects
images = [PIL.Image.open(f) for f in gif_params.images]
for im in images:
im.thumbnail((512, 512))

# Normalize and convert individual frames to 8-bit
arr: list[np.ndarray] = []
for im in images:
frame = np.array(im).astype(np.float32)
vmin, vmax = np.percentile(frame, (0.5, 99.5))
scale = 255 / ((vmax - vmin) or 1)
np.clip(frame, a_min=vmin, a_max=vmax, out=frame)
np.subtract(frame, vmin, out=frame)
np.multiply(frame, scale, out=frame)
arr.append(frame.astype(np.uint8))
arr = np.array(arr).astype(np.uint8)

# Convert back to PIL.Image objects and save as GIF
converted = [PIL.Image.fromarray(arr[f], mode="L") for f in range(len(images))]
converted[0].save(
output_file,
format="GIF",
append_images=converted[1:],
save_all=True,
duration=30,
loop=0,
)
logger.info(f"Created GIF file {output_file}")
return {"output_gif": str(output_file)}
12 changes: 7 additions & 5 deletions src/murfey/util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,22 @@ class StagePositionInfo(BaseModel):
"ChunkCoincidenceStagePosition" currently correspond to.
"""

# Top-level values
preparation: StagePositionValues | None = (
None # PreparationSiteLocation/StagePosition/StagePosition
)
chunk_coincidence: StagePositionValues | None = (
None # Parameters/ChunkCoincidenceStagePosition/StagePosition
)
chunk: StagePositionValues | None = (
None # ChunkSiteLocation/StagePosition/StagePosition
)
thinning_1: StagePositionValues | None = (
None # Parameters/ThinningStagePosition/StagePosition
None # ThinningSiteLocation/StagePosition/StagePosition
)
# Stored under Parameters
chunk_coincidence: StagePositionValues | None = (
None # Parameters/ChunkCoincidenceStagePosition/StagePosition
)
thinning_2: StagePositionValues | None = (
None # ThinningSiteLocation/StagePosition/StagePosition
None # Parameters/ThinningStagePosition/StagePosition
)


Expand Down
18 changes: 7 additions & 11 deletions src/murfey/util/route_manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1270,17 +1270,6 @@ murfey.server.api.workflow.correlative_router:
type: str
methods:
- POST
- path: /workflow/correlative/year/{year}/visits/{visit_name}/sessions/{session_id}/make_milling_gif
function: make_gif
path_params:
- name: year
type: int
- name: visit_name
type: str
- name: session_id
type: int
methods:
- POST
murfey.server.api.workflow.router:
- path: /workflow/visits/{visit_name}/sessions/{session_id}/register_data_collection_group
function: register_dc_group
Expand Down Expand Up @@ -1447,3 +1436,10 @@ murfey.server.api.workflow_fib.router:
type: int
methods:
- POST
- path: /workflow/fib/sessions/{session_id}/make_gif
function: make_gif
path_params:
- name: session_id
type: int
methods:
- POST
Loading
Loading