Skip to content

Commit 975c2f3

Browse files
Convert xrm images to tiffs (#837)
This adds functionality to read xrm images and request these are converted to tiffs. Currently this identifies atlas and roi images by finding out if they are montages, and splits them based on pixel size. Better methods may be possible but this should work for now. Rather than doing the xrm to tiff conversion here it puts a message onto rabbitmq. See DiamondLightSource/cryoem-services#278 Also contains a stub for registering rois in the database.
1 parent 8b6fcac commit 975c2f3

6 files changed

Lines changed: 441 additions & 76 deletions

File tree

src/murfey/client/contexts/sxt.py

Lines changed: 183 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def _find_reference(txrm_file: Path) -> Path | None:
5858
return None
5959

6060

61+
def _get_ole_header_value(ole_file, title: str, dtype: np.dtype):
62+
return np.frombuffer(ole_file.openstream(title).getvalue(), dtype)
63+
64+
6165
class SXTContext(Context):
6266
def __init__(
6367
self,
@@ -156,69 +160,221 @@ def post_transfer(
156160
environment=environment,
157161
**kwargs,
158162
)
163+
metadata: dict[str, Any] = {}
164+
165+
if transferred_file.suffix == ".xrm" and environment:
166+
# Make sure we have a dcg for this grid
167+
dcg_tag = ensure_dcg_exists(
168+
collection_type="sxt",
169+
metadata_source=self._basepath,
170+
environment=environment,
171+
machine_config=self._machine_config,
172+
token=self._token,
173+
)
159174

160-
data_suffixes = [".txrm"]
175+
with OleFileIO(str(transferred_file)) as xrm_ole:
176+
if xrm_ole.exists("ImageInfo/XPosition") and xrm_ole.exists(
177+
"ImageInfo/YPosition"
178+
):
179+
x_tiles = _get_ole_header_value(
180+
xrm_ole, "ImageInfo/XPosition", np.float32
181+
).tolist()
182+
y_tiles = _get_ole_header_value(
183+
xrm_ole, "ImageInfo/YPosition", np.float32
184+
).tolist()
185+
metadata["x_position"] = x_tiles[int(len(x_tiles) / 2)]
186+
metadata["y_position"] = y_tiles[int(len(y_tiles) / 2)]
161187

162-
if transferred_file.suffix in data_suffixes and environment:
188+
if xrm_ole.exists("ImageInfo/PixelSize"):
189+
metadata["pixel_size"] = _get_ole_header_value(
190+
xrm_ole, "ImageInfo/PixelSize", np.float32
191+
).tolist()[0]
192+
193+
if xrm_ole.exists("ImageInfo/ImageHeight"):
194+
metadata["height"] = _get_ole_header_value(
195+
xrm_ole, "ImageInfo/ImageHeight", np.int32
196+
).tolist()[0]
197+
198+
if xrm_ole.exists("ImageInfo/ImageWidth"):
199+
metadata["width"] = _get_ole_header_value(
200+
xrm_ole, "ImageInfo/ImageWidth", np.int32
201+
).tolist()[0]
202+
203+
# Find images which are not mosaics (txrm spec typos this as mosiac)
204+
if xrm_ole.exists("ImageInfo/MosiacRows") and xrm_ole.exists(
205+
"ImageInfo/MosiacColumns"
206+
):
207+
metadata["mosaic_rows"] = _get_ole_header_value(
208+
xrm_ole, "ImageInfo/MosiacRows", np.int32
209+
)[0]
210+
metadata["mosaic_columns"] = _get_ole_header_value(
211+
xrm_ole, "ImageInfo/MosiacColumns", np.int32
212+
)[0]
213+
metadata["mosaic_size"] = int(
214+
metadata["mosaic_rows"] * metadata["mosaic_columns"]
215+
)
216+
217+
source = _get_source(transferred_file, environment=environment)
218+
if source:
219+
image_path = _file_transferred_to(
220+
environment,
221+
source,
222+
transferred_file,
223+
Path(self._machine_config.get("rsync_basepath", "")),
224+
)
225+
if (
226+
environment.visit
227+
in Path(environment.default_destinations[source]).parts
228+
):
229+
# Split either side of the raw directory
230+
visit_idx = Path(
231+
environment.default_destinations[source]
232+
).parts.index(environment.visit)
233+
destination_base = "/".join(
234+
Path(environment.default_destinations[source]).parts[
235+
: visit_idx + 1
236+
]
237+
)
238+
destination_extra = "/".join(
239+
Path(environment.default_destinations[source]).parts[
240+
visit_idx + 2 :
241+
]
242+
)
243+
else:
244+
destination_base = str(
245+
Path(environment.default_destinations[source])
246+
/ environment.visit
247+
)
248+
destination_extra = ""
249+
converted_file_path = (
250+
Path(self._machine_config.get("rsync_basepath", ""))
251+
/ destination_base
252+
/ self._machine_config.get("processed_directory_name", "")
253+
/ self._machine_config.get("processed_extra_directory", "")
254+
/ destination_extra
255+
/ f"{transferred_file.relative_to(source).stem}_Annotated.tiff"
256+
)
257+
capture_post(
258+
base_url=str(environment.url.geturl()),
259+
router_name="workflow_sxt.router",
260+
function_name="convert_xrm_to_tiff",
261+
token=self._token,
262+
instrument_name=environment.instrument_name,
263+
data={
264+
"xrm_path": str(image_path),
265+
"tiff_path": str(converted_file_path),
266+
},
267+
)
268+
269+
if (
270+
metadata.get("mosaic_size", 1) > 0
271+
and metadata.get("pixel_size", 0) > 0.1
272+
):
273+
# Large pixel size, this is an atlas
274+
dcg_data = {
275+
"experiment_type_id": 44, # Atlas
276+
"tag": dcg_tag,
277+
"atlas": str(converted_file_path),
278+
"atlas_pixel_size": round(metadata.get("pixel_size", 0), 2),
279+
"atlas_x_stage_position": metadata.get("x_position", None),
280+
"atlas_y_stage_position": metadata.get("y_position", None),
281+
"atlas_height": int(
282+
metadata.get("height", 0) * metadata["mosaic_rows"]
283+
),
284+
"atlas_width": int(
285+
metadata.get("width", 0) * metadata["mosaic_columns"]
286+
),
287+
}
288+
capture_post(
289+
base_url=str(environment.url.geturl()),
290+
router_name="workflow.router",
291+
function_name="register_dc_group",
292+
token=self._token,
293+
instrument_name=environment.instrument_name,
294+
visit_name=environment.visit,
295+
session_id=environment.murfey_session,
296+
data=dcg_data,
297+
)
298+
elif metadata.get("mosaic_size", 1) > 0:
299+
# Other mosaic images are of grid squares
300+
capture_post(
301+
base_url=str(environment.url.geturl()),
302+
router_name="workflow_sxt.router",
303+
function_name="register_sxt_roi",
304+
token=self._token,
305+
instrument_name=environment.instrument_name,
306+
visit_name=environment.visit,
307+
session_id=environment.murfey_session,
308+
data={
309+
"tag": dcg_tag,
310+
"name": transferred_file.stem,
311+
"x_stage_position": metadata.get("x_position", None),
312+
"y_stage_position": metadata.get("y_position", None),
313+
"pixel_size": round(metadata.get("pixel_size", 0), 2),
314+
"height": int(
315+
metadata.get("height", 0) * metadata["mosaic_rows"]
316+
),
317+
"width": int(
318+
metadata.get("width", 0) * metadata["mosaic_columns"]
319+
),
320+
"image": str(converted_file_path),
321+
},
322+
)
323+
324+
elif transferred_file.suffix == ".txrm" and environment:
163325
source = _get_source(transferred_file, environment)
164326
if not source:
165327
logger.warning(f"No source found for file {transferred_file}")
166328
return False
167329

168330
# Read the tilt angles and pixel size from the txrm
169331
angles: list = []
170-
metadata: dict[str, Any] = {
171-
"source": str(self._basepath),
172-
"tilt_series_tag": transferred_file.stem,
173-
}
332+
metadata["source"] = str(self._basepath)
333+
metadata["tilt_series_tag"] = transferred_file.stem
174334
with OleFileIO(str(transferred_file)) as txrm_ole:
175335
if txrm_ole.exists("ReferenceData/Image"):
176336
metadata["has_reference"] = True
177337

178338
if txrm_ole.exists("ImageInfo/Angles"):
179-
angles = np.frombuffer(
180-
txrm_ole.openstream("ImageInfo/Angles").getvalue(), np.float32
339+
angles = _get_ole_header_value(
340+
txrm_ole, "ImageInfo/Angles", np.float32
181341
).tolist()
182342
metadata["minimum_angle"] = min(angles)
183343
metadata["maximum_angle"] = max(angles)
184344

185345
if txrm_ole.exists("ImageInfo/PixelSize"):
186-
pixel_size_txrm = np.frombuffer(
187-
txrm_ole.openstream("ImageInfo/PixelSize").getvalue(),
188-
np.float32,
346+
pixel_size_txrm = _get_ole_header_value(
347+
txrm_ole, "ImageInfo/PixelSize", np.float32
189348
).tolist()
190349
metadata["pixel_size"] = pixel_size_txrm[0] * 1e4
191350

192351
if txrm_ole.exists("ImageInfo/ImageWidth"):
193-
image_width_txrm = np.frombuffer(
194-
txrm_ole.openstream("ImageInfo/ImageWidth").getvalue(), np.int32
352+
image_width_txrm = _get_ole_header_value(
353+
txrm_ole, "ImageInfo/ImageWidth", np.int32
195354
).tolist()
196355
metadata["image_size_x"] = image_width_txrm[0]
197356

198357
if txrm_ole.exists("ImageInfo/ImageHeight"):
199-
image_height_txrm = np.frombuffer(
200-
txrm_ole.openstream("ImageInfo/ImageHeight").getvalue(),
201-
np.int32,
358+
image_height_txrm = _get_ole_header_value(
359+
txrm_ole, "ImageInfo/ImageHeight", np.int32
202360
).tolist()
203361
metadata["image_size_y"] = image_height_txrm[0]
204362

205363
if txrm_ole.exists("ImageInfo/ExpTimes"):
206-
exposure_time_txrm = np.frombuffer(
207-
txrm_ole.openstream("ImageInfo/ExpTimes").getvalue(), np.float32
364+
exposure_time_txrm = _get_ole_header_value(
365+
txrm_ole, "ImageInfo/ExpTimes", np.float32
208366
).tolist()
209367
metadata["exposure_time"] = exposure_time_txrm[0]
210368

211369
if txrm_ole.exists("ImageInfo/XrayMagnification"):
212-
magnification_txrm = np.frombuffer(
213-
txrm_ole.openstream("ImageInfo/XrayMagnification").getvalue(),
214-
np.float32,
370+
magnification_txrm = _get_ole_header_value(
371+
txrm_ole, "ImageInfo/XrayMagnification", np.float32
215372
).tolist()
216373
metadata["magnification"] = magnification_txrm[0]
217374

218375
if txrm_ole.exists("ImageInfo/ImagesTaken"):
219-
tilt_count_txrm = np.frombuffer(
220-
txrm_ole.openstream("ImageInfo/ImagesTaken").getvalue(),
221-
np.int32,
376+
tilt_count_txrm = _get_ole_header_value(
377+
txrm_ole, "ImageInfo/ImagesTaken", np.int32
222378
).tolist()
223379
metadata["tilt_series_length"] = tilt_count_txrm[0]
224380

@@ -235,9 +391,8 @@ def post_transfer(
235391
.split("\x00")
236392
if i
237393
]
238-
axis_values = np.frombuffer(
239-
txrm_ole.openstream("PositionInfo/MotorPositions").getvalue(),
240-
np.float32,
394+
axis_values = _get_ole_header_value(
395+
txrm_ole, "PositionInfo/MotorPositions", np.float32
241396
)
242397
if "Energy" in axis_names:
243398
energy_index = list(np.array(axis_names) == "Energy").index(
@@ -302,7 +457,7 @@ def post_transfer(
302457
reference_file_transferred_to = None
303458
capture_post(
304459
base_url=str(environment.url.geturl()),
305-
router_name="workflow.sxt_router",
460+
router_name="workflow_sxt.router",
306461
function_name="process_sxt_tilt_series",
307462
token=self._token,
308463
instrument_name=environment.instrument_name,

src/murfey/server/api/workflow.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@
7878
motion_corrected_mrc,
7979
)
8080
from murfey.util.tomo import midpoint
81-
from murfey.workflows.sxt.process_sxt_tilt_series import (
82-
SXTTiltSeriesInfo,
83-
)
8481
from murfey.workflows.tomo.tomo_metadata import register_search_map_in_database
8582

8683
logger = getLogger("murfey.server.api.workflow")
@@ -100,6 +97,10 @@ class DCGroupParameters(BaseModel):
10097
atlas: str = ""
10198
sample: Optional[int] = None
10299
atlas_pixel_size: float = 0
100+
atlas_x_stage_position: float | None = None
101+
atlas_y_stage_position: float | None = None
102+
atlas_width: int | None = None
103+
atlas_height: int | None = None
103104
create_smartem_grid: bool = False
104105
acquisition_uuid: Optional[str] = None
105106

@@ -1071,32 +1072,6 @@ def _add_tilt():
10711072
_add_tilt()
10721073

10731074

1074-
sxt_router = APIRouter(
1075-
prefix="/workflow/sxt",
1076-
dependencies=[Depends(validate_instrument_token)],
1077-
tags=["Workflows: Soft x-ray tomography"],
1078-
)
1079-
1080-
1081-
@sxt_router.post("/visits/{visit_name}/sessions/{session_id}/sxt_tilt_series")
1082-
def process_sxt_tilt_series(
1083-
visit_name: str,
1084-
session_id: MurfeySessionID,
1085-
tilt_series_info: SXTTiltSeriesInfo,
1086-
db=murfey_db,
1087-
):
1088-
if _transport_object:
1089-
_transport_object.send(
1090-
_transport_object.feedback_queue,
1091-
{
1092-
"register": "sxt.process_tilt_series",
1093-
"session_id": session_id,
1094-
"visit_name": visit_name,
1095-
"tilt_series_info": tilt_series_info.model_dump(mode="json"),
1096-
},
1097-
)
1098-
1099-
11001075
correlative_router = APIRouter(
11011076
prefix="/workflow/correlative",
11021077
dependencies=[Depends(validate_instrument_token)],

0 commit comments

Comments
 (0)