Skip to content

Commit b2483a6

Browse files
Add standoff transform to Transducer (OpenwaterHealth#166)
1 parent 2753fc9 commit b2483a6

4 files changed

Lines changed: 61 additions & 4 deletions

File tree

src/openlifu/virtual_fit.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,24 @@ def from_dict(parameter_dict:Dict[str,Any]) -> VirtualFitOptions: # Override Dic
106106
# (Currently we disable pylint E1121 because it is a temporary issue
107107
# which should be resolved by #165 and #166)
108108
def virtual_fit( # pylint: disable=E1121
109-
standoff_transform : np.ndarray,
110109
volume_array : np.ndarray,
111110
volume_affine_RAS : np.ndarray,
112111
units: str,
113112
target_RAS : Sequence[float],
113+
standoff_transform : np.ndarray,
114114
options : VirtualFitOptions,
115115
) -> List[np.ndarray]:
116116
"""Run patient-specific "virtual fitting" algorithm, suggesting a series of candidate transducer
117117
transforms for optimal sonicaiton of a given target.
118118
119119
Args:
120-
standoff_transform: See `create_standoff_transform` documentation for the meaning of this
121120
volume_array: A 3D volume MRI
122121
volume_affine_RAS: A 4x4 affine transform that maps `volume_array` into RAS space with certain units
123122
units: The spatial units of the RAS space into which volume_affine_RAS maps
124123
target_RAS: A 3D point, in the coordinates and units of `volume_affine_RAS` (the `units` argument)
124+
standoff_transform: See the documentation of `create_standoff_transform` or
125+
`Transducer.standoff_transform` for the meaning of this. Here it should be provided in the
126+
units `units`. The method `Transducer.get_standoff_transform_in_units` is useful for getting this.
125127
options : Virtual fitting algorithm configuration. See the `VirtualFitOptions` documentation.
126128
127129
Returns: A list of transducer transform candidates sorted starting from the best-scoring one. The transforms map transducer space

src/openlifu/xdc/transducer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,26 @@ class Transducer:
2222
frequency: float = 400.6e3
2323
units: str = "m"
2424
attrs: Dict[str, Any] = field(default_factory= dict)
25+
2526
registration_surface_filename: str | None = None
2627
"""Relative path to an open surface of the transducer to be used for registration"""
28+
2729
transducer_body_filename: str | None = None
2830
"""Relative path to the closed surface mesh for visualizing the transducer body"""
2931

32+
standoff_transform: np.ndarray = field(default_factory=lambda: np.eye(4, dtype=float))
33+
"""Affine transform representing the way in which the standoff for this transducer displaces the transducer.
34+
35+
A "standoff transform" applies a displacement in transducer space that moves a transducer to where it would
36+
be situated with the standoff in place. The idea is that if you start with a transform that places a transducer
37+
directly against skin, then pre-composing that transform by a "standoff transform" serves to nudge the transducer
38+
such that there is space for the standoff to be between it and the skin.
39+
40+
See also `openlifu.geo.create_standoff_transform`.
41+
42+
The units of this transform are assumed to be the native units of the transducer, the `Transducer.units` field.
43+
"""
44+
3045
def __post_init__(self):
3146
logging.info("Initializing transducer array")
3247
if self.name == "":
@@ -155,6 +170,12 @@ def convert_transform(self, matrix:np.ndarray, units:str) -> np.ndarray:
155170
matrix[0:3, 3] *= getunitconversion(units, self.units)
156171
return matrix
157172

173+
def get_standoff_transform_in_units(self, units:str) -> np.ndarray:
174+
"""Get the transducer's standoff transform in the desired units."""
175+
matrix = self.standoff_transform.copy()
176+
matrix[0:3, 3] *= getunitconversion(self.units, units)
177+
return matrix
178+
158179
@staticmethod
159180
def merge(list_of_transducers:List[Transducer]) -> Transducer:
160181
merged_array = list_of_transducers[0].copy()
@@ -175,6 +196,7 @@ def rescale(self, units):
175196
def to_dict(self):
176197
d = self.__dict__.copy()
177198
d["elements"] = [element.to_dict() for element in d["elements"]]
199+
d["standoff_transform"] = d["standoff_transform"].tolist()
178200
return d
179201

180202
def to_file(self, filename):
@@ -197,6 +219,8 @@ def from_file(filename):
197219
def from_dict(d, **kwargs):
198220
d = d.copy()
199221
d["elements"] = Element.from_dict(d["elements"])
222+
if "standoff_transform" in d:
223+
d["standoff_transform"] = np.array(d["standoff_transform"])
200224
return Transducer(**d, **kwargs)
201225

202226
@staticmethod

tests/resources/example_db/transducers/example_transducer/example_transducer.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,5 +965,11 @@
965965
],
966966
"frequency": 400600,
967967
"units": "mm",
968-
"attrs": {}
968+
"attrs": {},
969+
"standoff_transform": [
970+
[0.9, 0.1, 0, 0],
971+
[-0.1, 0.9, 0, 0],
972+
[0, 0, 1, 2.6],
973+
[0, 0, 0, 1]
974+
]
969975
}

tests/test_transducer.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from helpers import dataclasses_are_equal
88

9-
from openlifu import Transducer
9+
from openlifu.xdc import Element, Transducer
1010

1111

1212
@pytest.fixture()
@@ -64,3 +64,28 @@ def test_get_effective_origin():
6464
transducer.get_effective_origin(apodizations = apodizations_with_just_one_element, units = "um"),
6565
transducer.get_positions(units="um")[element_index_to_turn_on],
6666
)
67+
68+
def test_get_standoff_transform_in_units():
69+
standoff_transform_in_mm = np.array([
70+
[-0.1,0.9,0,20],
71+
[0.9,0.1,0,30],
72+
[0,0,1,40],
73+
[0,0,0,1],
74+
])
75+
standoff_transform_in_cm = np.array([
76+
[-0.1,0.9,0,2],
77+
[0.9,0.1,0,3],
78+
[0,0,1,4],
79+
[0,0,0,1],
80+
])
81+
transducer = Transducer(units='mm')
82+
transducer.standoff_transform = standoff_transform_in_mm
83+
assert np.allclose(
84+
transducer.get_standoff_transform_in_units("cm"),
85+
standoff_transform_in_cm,
86+
)
87+
88+
def test_read_data_types(example_transducer:Transducer):
89+
assert isinstance(example_transducer.standoff_transform, np.ndarray)
90+
if len(example_transducer.elements) > 0:
91+
assert isinstance(example_transducer.elements[0], Element)

0 commit comments

Comments
 (0)