Skip to content

Commit b9da8a3

Browse files
committed
Updated FIB atlas image registration workflow:
* Added logic to extract, read, and validate metadata from the image * Added logic to register the imaging sites in Murfey database
1 parent 287034e commit b9da8a3

1 file changed

Lines changed: 196 additions & 2 deletions

File tree

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,205 @@
1+
import logging
2+
import xml.etree.ElementTree as ET
3+
from functools import cached_property
14
from pathlib import Path
25

3-
from sqlmodel import Session
6+
import numpy as np
7+
import PIL.Image
8+
from pydantic import BaseModel, computed_field, model_validator
9+
from sqlmodel import Session, select
10+
11+
import murfey.util.db as MurfeyDB
12+
13+
logger = logging.getLogger("murfey.workflows.fib.register_atlas")
14+
15+
16+
class FIBAtlasMetadata(BaseModel):
17+
"""
18+
These fields should ALL be present in the Electron Snapshot image.
19+
Positions and pixel sizes are in metres, whereas angles are in radians.
20+
"""
21+
22+
visit_name: str
23+
file: Path
24+
# Acceleration voltage
25+
voltage: float
26+
# Beam shifts
27+
shift_x: float
28+
shift_y: float
29+
# Actual field of view
30+
len_x: float
31+
len_y: float
32+
# Stage position
33+
pos_x: float
34+
pos_y: float
35+
pos_z: float
36+
rotation: float
37+
tilt_alpha: float
38+
tilt_beta: float
39+
# Image dimensions
40+
pixels_x: int
41+
pixels_y: int
42+
# Pixel size
43+
pixel_size_x: float
44+
pixel_size_y: float
45+
46+
@model_validator(mode="after")
47+
def check_pixel_size_tolerance(self):
48+
"""
49+
The pixel size values for x and y should be nigh-identical
50+
"""
51+
if abs(self.pixel_size_x - self.pixel_size_y) > 1e-18:
52+
raise ValueError
53+
return self
54+
55+
# mypy doesn't support decorators on @property
56+
@computed_field # type: ignore
57+
@cached_property
58+
def pixel_size(self) -> float:
59+
"""
60+
Return an average of pixel sizes along the x- and y-axes
61+
"""
62+
return 0.5 * (self.pixel_size_x + self.pixel_size_y)
63+
64+
# mypy doesn't support decorators on @property
65+
@computed_field # type: ignore
66+
@cached_property
67+
def slot_number(self) -> int:
68+
"""
69+
Decide on a slot number for the site being inspected. From observation,
70+
the x-position is entirely negative for one slot and entirely positive
71+
for the other.
72+
"""
73+
return 1 if self.pos_x < 0 else 2
74+
75+
# mypy doesn't support decorators on @property
76+
@computed_field # type: ignore
77+
@cached_property
78+
def site_name(self) -> str:
79+
"""
80+
Create a site name for the current image based on the project name
81+
and its slot number. This assumes a specific folder structure of
82+
{visit_name}/maps/{project_name}
83+
"""
84+
path_parts = self.file.parts
85+
visit_idx = path_parts.index(self.visit_name)
86+
project_name = path_parts[visit_idx + 2] # {visit}/maps/{project_name}
87+
return f"{project_name}--slot_{self.slot_number}"
88+
89+
90+
def _parse_metadata(file: Path, visit_name: str):
91+
"""
92+
Parses through the atlas image's tags to extract the relevant metadata
93+
"""
94+
95+
# Metadata is stored in the TIFF file under tag number 34683
96+
img = PIL.Image.open(file)
97+
tags = dict(img.tag_v2)
98+
xml_metadata = ET.fromstring(str(tags.get(34683)))
99+
100+
# Extract key values from metadata
101+
return FIBAtlasMetadata(
102+
visit_name=visit_name,
103+
file=file,
104+
**{
105+
key: node.text
106+
if (node := xml_metadata.find(node_path)) is not None
107+
else None
108+
for key, node_path in (
109+
("voltage", ".//Optics/AccelerationVoltage"),
110+
("shift_x", ".//Optics/BeamShift/X"),
111+
("shift_y", ".//Optics/BeamShift/Y"),
112+
("len_x", ".//Optics/ScanFieldOfView/X"),
113+
("len_y", ".//Optics/ScanFieldOfView/Y"),
114+
("pos_x", ".//StageSettings/StagePosition/X"),
115+
("pos_y", ".//StageSettings/StagePosition/Y"),
116+
("pos_z", ".//StageSettings/StagePosition/Z"),
117+
("rotation", ".//StageSettings/StagePosition/Rotation"),
118+
("tilt_alpha", ".//StageSettings/StagePosition/Tilt/Alpha"),
119+
("tilt_beta", ".//StageSettings/StagePosition/Tilt/Beta"),
120+
("pixels_x", ".//BinaryResult/ImageSize/X"),
121+
("pixels_y", ".//BinaryResult/ImageSize/Y"),
122+
("pixel_size_x", ".//BinaryResult/PixelSize/X"),
123+
("pixel_size_y", ".//BinaryResult/PixelSize/Y"),
124+
)
125+
},
126+
)
127+
128+
129+
def _register_fib_imaging_site(
130+
session_id: int,
131+
metadata: FIBAtlasMetadata,
132+
murfey_db: Session,
133+
):
134+
"""
135+
Register FIB atlas in Murfey database or update existing entry with newer image
136+
"""
137+
if not (
138+
fib_imaging_site := murfey_db.exec(
139+
select(MurfeyDB.ImagingSite)
140+
.where(MurfeyDB.ImagingSite.session_id == session_id)
141+
.where(MurfeyDB.ImagingSite.image_path == str(metadata.file))
142+
).one_or_none()
143+
):
144+
fib_imaging_site = MurfeyDB.ImagingSite(
145+
session_id=session_id,
146+
site_name=metadata.site_name,
147+
data_type="atlas",
148+
image_path=str(metadata.file),
149+
pos_x=metadata.pos_x,
150+
pos_y=metadata.pos_y,
151+
pos_z=metadata.pos_z,
152+
rotation=float(np.rad2deg(metadata.rotation)),
153+
tilt_alpha=float(np.rad2deg(metadata.tilt_alpha)),
154+
tilt_beta=float(np.rad2deg(metadata.tilt_beta)),
155+
len_x=metadata.len_x,
156+
len_y=metadata.len_y,
157+
image_pixels_x=metadata.pixels_x,
158+
image_pixels_y=metadata.pixels_y,
159+
image_pixel_size=metadata.pixel_size,
160+
)
161+
murfey_db.add(fib_imaging_site)
162+
murfey_db.commit()
4163

5164

6165
def run(
7166
session_id: int,
8167
file: Path,
9168
murfey_db: Session,
10169
):
11-
pass
170+
# Outer try-finally block to ensure database connection closes
171+
try:
172+
# Load visit information
173+
try:
174+
session_entry = murfey_db.exec(
175+
select(MurfeyDB.Session).where(MurfeyDB.Session.id == session_id)
176+
).one()
177+
visit_name = session_entry.visit
178+
except Exception:
179+
logger.error(
180+
"Exception encountered while querying Murfey database", exc_info=True
181+
)
182+
return False
183+
184+
# Extract metadata from Electron Snapshot image
185+
try:
186+
metadata = _parse_metadata(file, visit_name)
187+
except Exception:
188+
logger.error(f"Error extracting metadata from file {file}", exc_info=True)
189+
return False
190+
191+
# Register imaging site in Murfey, or update existing one
192+
try:
193+
_register_fib_imaging_site(session_id, metadata, murfey_db)
194+
logger.info(
195+
f"Registered FIB atlas image {file} for slot {metadata.slot_number} in Murfey database"
196+
)
197+
except Exception:
198+
logger.error(
199+
f"Error registering FIB atlas image {file} in Murfey database",
200+
exc_info=True,
201+
)
202+
return False
203+
return True
204+
finally:
205+
murfey_db.close()

0 commit comments

Comments
 (0)