brkraw-sordino 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brkraw_sordino/__init__.py +7 -0
- brkraw_sordino/brkraw_hook.yaml +8 -0
- brkraw_sordino/docs.md +99 -0
- brkraw_sordino/helper.py +81 -0
- brkraw_sordino/hook.py +164 -0
- brkraw_sordino/orientation.py +45 -0
- brkraw_sordino/recon.py +125 -0
- brkraw_sordino/rules/sordino.yaml +35 -0
- brkraw_sordino/specs/info_spec.yaml +164 -0
- brkraw_sordino/specs/metadata_spec.yaml +167 -0
- brkraw_sordino/specs/prune4recon.yaml +44 -0
- brkraw_sordino/specs/recon_spec.yaml +103 -0
- brkraw_sordino/specs/utils.py +212 -0
- brkraw_sordino/spoketiming.py +168 -0
- brkraw_sordino/traj.py +316 -0
- brkraw_sordino/typing.py +25 -0
- brkraw_sordino-0.1.3.dist-info/METADATA +114 -0
- brkraw_sordino-0.1.3.dist-info/RECORD +21 -0
- brkraw_sordino-0.1.3.dist-info/WHEEL +5 -0
- brkraw_sordino-0.1.3.dist-info/entry_points.txt +2 -0
- brkraw_sordino-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from .helper import progressbar
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
from scipy.interpolate import interp1d
|
|
7
|
+
from .recon import get_num_frames, parse_fid_info
|
|
8
|
+
from .typing import Options
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("brkraw.sordino")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_fid_filesize_in_gb(fid_fobj,
|
|
15
|
+
recon_info: Dict[str, Any],
|
|
16
|
+
options: Options) -> int:
|
|
17
|
+
fid_fobj.seek(0, os.SEEK_END)
|
|
18
|
+
file_size = fid_fobj.tell()
|
|
19
|
+
fid_fobj.seek(0)
|
|
20
|
+
|
|
21
|
+
if options.num_frames is not None:
|
|
22
|
+
file_size *= get_num_frames(recon_info, options) / options.num_frames
|
|
23
|
+
file_size_gb = file_size / (1024 ** 3)
|
|
24
|
+
return file_size_gb
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_num_segment(file_size_gb, recon_info: Dict[str, Any], options: Options):
|
|
28
|
+
npro = recon_info['NPro']
|
|
29
|
+
num_segs = int(np.ceil(file_size_gb / options.mem_limit)) if options.mem_limit > 0 else 1
|
|
30
|
+
npro_per_seg = int(npro / num_segs)
|
|
31
|
+
|
|
32
|
+
if residual_pro := npro % npro_per_seg:
|
|
33
|
+
segs = [npro_per_seg for _ in range(num_segs - 1)] + [residual_pro]
|
|
34
|
+
else:
|
|
35
|
+
segs = [npro_per_seg for _ in range(num_segs)]
|
|
36
|
+
segs = np.asarray(segs, dtype=int)
|
|
37
|
+
return segs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def prep_fid_segmentation(fid_fobj,
|
|
41
|
+
recon_info: Dict[str, Any],
|
|
42
|
+
options: Options):
|
|
43
|
+
file_size = get_fid_filesize_in_gb(fid_fobj, recon_info, options)
|
|
44
|
+
segs = get_num_segment(file_size, recon_info, options)
|
|
45
|
+
return segs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_timestamps(repetition_time, npro, num_frames):
|
|
49
|
+
vol_scantime = float(repetition_time) * npro
|
|
50
|
+
base_timestamps = np.arange(num_frames) * vol_scantime
|
|
51
|
+
target_timestamps = base_timestamps + (vol_scantime / 2)
|
|
52
|
+
return {'base': base_timestamps,
|
|
53
|
+
'target': target_timestamps}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_segmented_data(fid_f, fid_shape, fid_dtype,
|
|
57
|
+
buff_size, num_frames,
|
|
58
|
+
seg_size, pro_loc,
|
|
59
|
+
options):
|
|
60
|
+
|
|
61
|
+
seg = []
|
|
62
|
+
|
|
63
|
+
stc_buffer_size = int(buff_size / fid_shape[3])
|
|
64
|
+
pro_offset = pro_loc * stc_buffer_size
|
|
65
|
+
seg_buffer_size = stc_buffer_size * seg_size # total buffer size for current segment
|
|
66
|
+
|
|
67
|
+
for t in range(num_frames):
|
|
68
|
+
frame_offset = t * buff_size
|
|
69
|
+
seek_loc = int(options.offset or 0) * buff_size + frame_offset + pro_offset
|
|
70
|
+
fid_f.seek(seek_loc)
|
|
71
|
+
seg.append(fid_f.read(seg_buffer_size))
|
|
72
|
+
|
|
73
|
+
seg_data = np.frombuffer(b''.join(seg), dtype=fid_dtype)
|
|
74
|
+
seg_data = seg_data.reshape([2, np.prod(fid_shape[1:3]), seg_size, num_frames], order='F')
|
|
75
|
+
return {
|
|
76
|
+
'data': seg_data,
|
|
77
|
+
'offset': pro_offset,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def interpolate_spoketiming(complex_feed, sample_id, pro_id, ref_timestamps, timestamps, corrected_data):
|
|
82
|
+
""" interpolation step
|
|
83
|
+
each projection interpolated the timing at the middle of projection
|
|
84
|
+
number of spokes * receivers processed together
|
|
85
|
+
therefore, the spoke timing within a projection will not be corrected
|
|
86
|
+
instead this actually corrected the spoke timing for each projection
|
|
87
|
+
as single TR represent time of one projection
|
|
88
|
+
|
|
89
|
+
:param complex_feed: Description
|
|
90
|
+
:param sample_id: Description
|
|
91
|
+
:param pro_id: Description
|
|
92
|
+
:param ref_timestamps: Description
|
|
93
|
+
:param timestamps: Description
|
|
94
|
+
:param corrected_data: Description
|
|
95
|
+
"""
|
|
96
|
+
mag = np.abs(complex_feed)
|
|
97
|
+
phase = np.angle(complex_feed)
|
|
98
|
+
phase_unw = np.unwrap(phase)
|
|
99
|
+
interp_mag = interp1d(ref_timestamps, mag, kind='linear',
|
|
100
|
+
bounds_error=False, fill_value='extrapolate') # pyright: ignore[reportArgumentType]
|
|
101
|
+
interp_phase = interp1d(ref_timestamps, phase_unw, kind='linear',
|
|
102
|
+
bounds_error=False, fill_value='extrapolate') # pyright: ignore[reportArgumentType]
|
|
103
|
+
|
|
104
|
+
mag_t = interp_mag(timestamps['target'])
|
|
105
|
+
phase_t = interp_phase(timestamps['target'])
|
|
106
|
+
|
|
107
|
+
phase_wrap = (phase_t + np.pi) % (2 * np.pi) - np.pi
|
|
108
|
+
|
|
109
|
+
z_t = mag_t * np.exp(1j * phase_wrap)
|
|
110
|
+
corrected_data[0, sample_id, pro_id, :] = z_t.real
|
|
111
|
+
corrected_data[1, sample_id, pro_id, :] = z_t.imag
|
|
112
|
+
return corrected_data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def correct_spoketiming(segs, fid_f, stc_f, recon_info, options):
|
|
116
|
+
""" Correct timing of each spoke to align center of scan time
|
|
117
|
+
(Same concept as slice timing correction, but applied to FID signal)
|
|
118
|
+
"""
|
|
119
|
+
logger.debug("+ Spoketiming correction")
|
|
120
|
+
fid_shape, fid_dtype = parse_fid_info(recon_info)
|
|
121
|
+
buff_size = int(np.prod(fid_shape) * fid_dtype.itemsize)
|
|
122
|
+
num_frames = get_num_frames(recon_info, options)
|
|
123
|
+
repetition_time = recon_info['RepetitionTime_ms'] / 1000.0
|
|
124
|
+
timestamps = get_timestamps(repetition_time, fid_shape[3], num_frames)
|
|
125
|
+
|
|
126
|
+
pro_loc = 0
|
|
127
|
+
results = {
|
|
128
|
+
'buffer_size': 0,
|
|
129
|
+
'dtype': None,
|
|
130
|
+
}
|
|
131
|
+
for seg_size in progressbar(segs, desc=' - Segments', ncols=100):
|
|
132
|
+
# load data
|
|
133
|
+
segmented = get_segmented_data(
|
|
134
|
+
fid_f, fid_shape, fid_dtype, buff_size, num_frames, seg_size, pro_loc, options
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
seg_data = segmented['data']
|
|
138
|
+
corrected_seg_data = np.empty_like(seg_data)
|
|
139
|
+
for pro_id in range(seg_size):
|
|
140
|
+
cur_pro = pro_loc + pro_id
|
|
141
|
+
ref_timestamps = timestamps['base'] + (cur_pro * repetition_time)
|
|
142
|
+
for sample_id in range(np.prod(fid_shape[1:3])):
|
|
143
|
+
complex_feed = seg_data[0, sample_id, pro_id, :] + 1j * seg_data[1, sample_id, pro_id, :]
|
|
144
|
+
try:
|
|
145
|
+
corrected_seg_data = interpolate_spoketiming(complex_feed, sample_id, pro_id,
|
|
146
|
+
ref_timestamps, timestamps,
|
|
147
|
+
corrected_seg_data)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.debug("+ Exception occured during spoketiming correction")
|
|
150
|
+
logger.debug(f" - RefTimeStamps: {ref_timestamps}")
|
|
151
|
+
logger.debug(f" - DataFeed: {complex_feed}")
|
|
152
|
+
raise e
|
|
153
|
+
|
|
154
|
+
# Store data
|
|
155
|
+
for t in range(num_frames):
|
|
156
|
+
frame_offset = t * buff_size
|
|
157
|
+
stc_f.seek(frame_offset + segmented['offset'])
|
|
158
|
+
stc_f.write(corrected_seg_data[:,:,:, t].flatten(order='F').tobytes())
|
|
159
|
+
|
|
160
|
+
if results['dtype'] == None:
|
|
161
|
+
results['dtype'] = corrected_seg_data.dtype
|
|
162
|
+
else:
|
|
163
|
+
assert results['dtype'] == corrected_seg_data.dtype
|
|
164
|
+
|
|
165
|
+
pro_loc += seg_size
|
|
166
|
+
|
|
167
|
+
results['buffer_size'] = np.prod([fid_shape]) * results['dtype'].itemsize
|
|
168
|
+
return results
|
brkraw_sordino/traj.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import numpy as np
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
from .helper import progressbar
|
|
7
|
+
from .typing import Options
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("brkraw.sordino")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def radial_angles(n: int, factor: float) -> int:
|
|
13
|
+
return int(np.ceil((np.pi * n * factor) / 2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def radial_angle(i: int, n: int) -> float:
|
|
17
|
+
return np.pi * (i + 0.5) / n
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def recon_output_shape(matrix_size, ext_factors) -> list[int]:
|
|
21
|
+
output_shape = (matrix_size * ext_factors).astype(int).tolist()
|
|
22
|
+
return output_shape
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def recon_n_frames(total_frames: int,
|
|
26
|
+
offset: int = 0,
|
|
27
|
+
num_frame: Optional[int] = None) -> int:
|
|
28
|
+
avail_frames = total_frames - offset
|
|
29
|
+
set_frames = num_frame or total_frames
|
|
30
|
+
if set_frames > avail_frames:
|
|
31
|
+
set_frames = avail_frames
|
|
32
|
+
return int(set_frames)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def recon_buffer_offset(buffer_size: int, offset: Optional[int] = 0) -> int:
|
|
36
|
+
return offset or 0 * buffer_size
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_vol_scantime(repetition_time: float, fid_shape: np.ndarray) -> float:
|
|
40
|
+
return repetition_time * float(fid_shape[3])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def calc_npro(matrix_size: int, under_sampling: float) -> int:
|
|
44
|
+
usamp = np.sqrt(under_sampling)
|
|
45
|
+
n_theta = radial_angles(matrix_size, 1 / usamp)
|
|
46
|
+
n_pro = 0
|
|
47
|
+
for i_theta in range(n_theta):
|
|
48
|
+
theta = radial_angle(i_theta, n_theta)
|
|
49
|
+
n_phi = radial_angles(matrix_size, np.sin(theta) / usamp)
|
|
50
|
+
n_pro += n_phi
|
|
51
|
+
return int(n_pro)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_undersamp(matrix_size: int, n_pro_target: int) -> float:
|
|
55
|
+
from scipy.optimize import brentq
|
|
56
|
+
|
|
57
|
+
def func(under_sampling: float) -> float:
|
|
58
|
+
n_pro = calc_npro(matrix_size, under_sampling)
|
|
59
|
+
return float(n_pro - n_pro_target)
|
|
60
|
+
|
|
61
|
+
max_val = calc_npro(matrix_size, 1)
|
|
62
|
+
start = 1e-6
|
|
63
|
+
end = max_val / matrix_size
|
|
64
|
+
if func(start) * func(end) > 0:
|
|
65
|
+
raise ValueError("The function does not change sign over the interval.")
|
|
66
|
+
undersamp_solution = brentq(func, start, end, xtol=1e-6)
|
|
67
|
+
if isinstance(undersamp_solution, tuple):
|
|
68
|
+
return float(undersamp_solution[0])
|
|
69
|
+
return float(undersamp_solution)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def calc_radial_traj3d(
|
|
73
|
+
grad_array: np.ndarray,
|
|
74
|
+
matrix_size: int,
|
|
75
|
+
use_origin: bool,
|
|
76
|
+
over_sampling: float,
|
|
77
|
+
correct_ramptime: bool = False,
|
|
78
|
+
traj_offset: Optional[float] = None,
|
|
79
|
+
) -> np.ndarray:
|
|
80
|
+
"""
|
|
81
|
+
Calculate trajectory for SORDINO imaging.
|
|
82
|
+
For each projection, a vector with sampling positions (trajectory) is created.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
grad_array (ndarray): Gradient vector profile for each projection, sized (3 x n_pro).
|
|
86
|
+
matrix_size (int): Matrix size of the final image.
|
|
87
|
+
over_sampling (float): Oversampling factor.
|
|
88
|
+
bandwidth (float): Bandwidth of the imaging process.
|
|
89
|
+
traj_offset (float, optional): Trajectory offset ratio compared to ADC sampling.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
ndarray: Calculated trajectory for each projection.
|
|
93
|
+
"""
|
|
94
|
+
pro_offset = 1 if use_origin else 0
|
|
95
|
+
g = grad_array.copy()
|
|
96
|
+
npro = g.shape[-1]
|
|
97
|
+
traj_offset = traj_offset or 0
|
|
98
|
+
num_samples = int(matrix_size / 2 * over_sampling)
|
|
99
|
+
traj = np.zeros([npro, num_samples, 3])
|
|
100
|
+
scale_factor = (num_samples - 1 + traj_offset) / (num_samples-1)
|
|
101
|
+
|
|
102
|
+
logger.debug('++ Processing trajectory calculation...')
|
|
103
|
+
logger.debug(' + Input arguments')
|
|
104
|
+
logger.debug(f' - Size of Matrix: {matrix_size}')
|
|
105
|
+
logger.debug(f' - OverSampling: {over_sampling}')
|
|
106
|
+
logger.debug(f' - Trajectory offset ratio: {traj_offset}')
|
|
107
|
+
logger.debug(f' - Ramp-time Correction: {str(correct_ramptime)}')
|
|
108
|
+
logger.debug(f' - Size of Output Trajectory: {traj.shape}')
|
|
109
|
+
logger.debug(f' - Image Scailing Factor (*Subject to be corrected in future version): {scale_factor}')
|
|
110
|
+
|
|
111
|
+
for i_pro in progressbar(range(pro_offset, npro + pro_offset), desc="traj", ncols=100):
|
|
112
|
+
for i_samp in range(num_samples):
|
|
113
|
+
samp = ((i_samp + traj_offset) / (num_samples - 1)) / 2
|
|
114
|
+
if not correct_ramptime or i_pro == (npro + pro_offset) - 1:
|
|
115
|
+
correction = np.zeros(3)
|
|
116
|
+
traj[i_pro, i_samp, :] = samp * (g[:, i_pro] + correction)
|
|
117
|
+
else:
|
|
118
|
+
correction = (g[:, i_pro] - g[:, i_pro - 1]) / num_samples * i_samp
|
|
119
|
+
traj[i_pro, i_samp, :] = samp * (g[:, i_pro - 1] + correction)
|
|
120
|
+
return traj
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def calc_radial_grad3d(
|
|
124
|
+
matrix_size: int,
|
|
125
|
+
npro_target: int,
|
|
126
|
+
half_sphere: bool,
|
|
127
|
+
use_origin: bool,
|
|
128
|
+
reorder: bool,
|
|
129
|
+
) -> np.ndarray:
|
|
130
|
+
"""
|
|
131
|
+
Generate 3D radial gradient profile based on input parameters.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
matrix_size (int): Target matrix size.
|
|
135
|
+
n_pro_target (int): Target number of projections.
|
|
136
|
+
half_sphere (bool): If True, only generate for half the sphere.
|
|
137
|
+
use_origin (bool): If True, add center points at the start.
|
|
138
|
+
reorder (bool): Use reorder scheme provided by Bruker ZTE sequence.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
ndarray: The gradient profile as an array.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
n_pro = int(npro_target / (1 if half_sphere else 2) - (1 if use_origin else 0))
|
|
145
|
+
usamp = np.sqrt(find_undersamp(matrix_size, n_pro))
|
|
146
|
+
|
|
147
|
+
logger.debug('\n++ Processing SORDINO 3D Radial Gradient Calculation...')
|
|
148
|
+
logger.debug(' + Input arguments')
|
|
149
|
+
logger.debug(f' - Matrix size: {matrix_size}')
|
|
150
|
+
logger.debug(f' - Undersampling factor: {usamp}')
|
|
151
|
+
logger.debug(f' - Number of Projections: {npro_target}')
|
|
152
|
+
logger.debug(f' - Half sphere only: {half_sphere}')
|
|
153
|
+
logger.debug(f' - Use origin: {use_origin}')
|
|
154
|
+
logger.debug(f' - Reorder Gradient: {reorder}')
|
|
155
|
+
|
|
156
|
+
grad = {"r": [], "p": [], "s": []}
|
|
157
|
+
radial_n_phi: list[int] = []
|
|
158
|
+
|
|
159
|
+
logger.debug(' + Start Calculating Gradient Vectors...')
|
|
160
|
+
n_theta = radial_angles(matrix_size, 1.0 / usamp)
|
|
161
|
+
for i_theta in range(n_theta):
|
|
162
|
+
theta = radial_angle(i_theta, n_theta)
|
|
163
|
+
n_phi = radial_angles(matrix_size, float(np.sin(theta) / usamp))
|
|
164
|
+
radial_n_phi.append(n_phi)
|
|
165
|
+
for i_phi in range(n_phi):
|
|
166
|
+
phi = radial_angle(i_phi, n_phi)
|
|
167
|
+
grad["r"].append(np.sin(theta) * np.cos(phi))
|
|
168
|
+
grad["p"].append(np.sin(theta) * np.sin(phi))
|
|
169
|
+
grad["s"].append(np.cos(theta))
|
|
170
|
+
logger.debug('done')
|
|
171
|
+
|
|
172
|
+
grad_array = np.stack([grad["r"], grad["p"], grad["s"]], axis=0)
|
|
173
|
+
n_pro_created = grad_array.shape[-1] * (1 if half_sphere else 2) + (1 if use_origin else 0)
|
|
174
|
+
if not usamp:
|
|
175
|
+
if n_pro_created != npro_target:
|
|
176
|
+
raise ValueError("Target number of projections can't be reached.")
|
|
177
|
+
grad_array = reorder_projections(n_theta, radial_n_phi, grad_array, reorder)
|
|
178
|
+
if not half_sphere:
|
|
179
|
+
grad_array = np.concatenate([grad_array, -1 * grad_array], axis=1)
|
|
180
|
+
if use_origin:
|
|
181
|
+
grad_array = np.concatenate([[[0, 0, 0]], grad_array.T], axis=0).T
|
|
182
|
+
return grad_array
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def reorder_projections(
|
|
186
|
+
n_theta: int,
|
|
187
|
+
radial_n_phi: list[int],
|
|
188
|
+
grad_array: np.ndarray,
|
|
189
|
+
reorder: bool,
|
|
190
|
+
) -> np.ndarray:
|
|
191
|
+
"""
|
|
192
|
+
Reorder radial projections for improved image spoiling.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
n_theta (int): Number of theta angles.
|
|
196
|
+
radial_n_phi (list): Number of phi angles for each theta.
|
|
197
|
+
grad_array (ndarray): Gradient array.
|
|
198
|
+
reorder (bool): Whether to apply the reordering scheme.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ndarray: Reordered gradient array.
|
|
202
|
+
"""
|
|
203
|
+
g = grad_array.copy()
|
|
204
|
+
if reorder:
|
|
205
|
+
logger.debug(' + Reordering projections...')
|
|
206
|
+
def reorder_incr_index(n: int, i: int, d: int) -> tuple[int, int]:
|
|
207
|
+
if (i + d > n - 1) or (i + d < 0):
|
|
208
|
+
d *= -1
|
|
209
|
+
i += d
|
|
210
|
+
return i, d
|
|
211
|
+
|
|
212
|
+
n_pro = g.shape[-1]
|
|
213
|
+
n_phi_max = max(radial_n_phi)
|
|
214
|
+
r_g = np.zeros_like(g)
|
|
215
|
+
r_mask = np.zeros([n_theta, n_phi_max])
|
|
216
|
+
|
|
217
|
+
for i_theta in range(n_theta):
|
|
218
|
+
for i_phi in range(radial_n_phi[i_theta], n_phi_max):
|
|
219
|
+
r_mask[i_theta][i_phi] = 1
|
|
220
|
+
|
|
221
|
+
i_theta = 0
|
|
222
|
+
d_theta = 1
|
|
223
|
+
i_phi = 0
|
|
224
|
+
d_phi = 1
|
|
225
|
+
|
|
226
|
+
for i in range(n_pro):
|
|
227
|
+
while not any(r_mask[i_theta] == 0):
|
|
228
|
+
i_theta, d_theta = reorder_incr_index(n_theta, i_theta, d_theta)
|
|
229
|
+
|
|
230
|
+
while r_mask[i_theta][i_phi] == 1:
|
|
231
|
+
i_phi, d_phi = reorder_incr_index(n_phi_max, i_phi, d_phi)
|
|
232
|
+
new_i = sum(radial_n_phi[:i_theta]) + i_phi
|
|
233
|
+
r_g[:, i] = g[:, new_i]
|
|
234
|
+
r_mask[i_theta][i_phi] = 1
|
|
235
|
+
|
|
236
|
+
i_theta, d_theta = reorder_incr_index(n_theta, i_theta, d_theta)
|
|
237
|
+
i_phi, d_phi = reorder_incr_index(n_phi_max, i_phi, d_phi)
|
|
238
|
+
logger.debug('done')
|
|
239
|
+
return r_g
|
|
240
|
+
|
|
241
|
+
i = 0
|
|
242
|
+
for i_theta in range(n_theta):
|
|
243
|
+
if i_theta % 2 == 1:
|
|
244
|
+
for i_phi in range(int(radial_n_phi[i_theta] / 2)):
|
|
245
|
+
i0 = i + i_phi
|
|
246
|
+
i1 = i + radial_n_phi[i_theta] - 1 - i_phi
|
|
247
|
+
g[:, i0], g[:, i1] = g[:, i1].copy(), g[:, i0].copy()
|
|
248
|
+
i += radial_n_phi[i_theta]
|
|
249
|
+
return g
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def generate_hash(*args: Any) -> str:
|
|
253
|
+
"""Generate a hash from the input arguments."""
|
|
254
|
+
hash_input = "".join(str(arg) for arg in args)
|
|
255
|
+
return hashlib.md5(hash_input.encode()).hexdigest()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_trajectory(recon_info: Dict[str, Any],
|
|
259
|
+
options: Options) -> np.ndarray:
|
|
260
|
+
|
|
261
|
+
correct_ramptime = getattr(options, "correct_ramptime", True)
|
|
262
|
+
ext_factors = getattr(options, "ext_factors", [1.0, 1.0, 1.0])
|
|
263
|
+
logger.debug(f' + Extension factors applied to matrix: {ext_factors}')
|
|
264
|
+
|
|
265
|
+
sample_size = recon_info['Matrix'][0]
|
|
266
|
+
npro = recon_info['NPro']
|
|
267
|
+
half_acquisition = recon_info['HalfAcquisition']
|
|
268
|
+
use_origin = recon_info['UseOrigin']
|
|
269
|
+
reorder = recon_info['Reorder']
|
|
270
|
+
|
|
271
|
+
eff_bandwidth = recon_info['EffBandwidth_Hz']
|
|
272
|
+
over_sampling = recon_info['OverSampling']
|
|
273
|
+
traj_offset = recon_info['AcqDelayTotal_us']
|
|
274
|
+
|
|
275
|
+
grad = calc_radial_grad3d(sample_size,
|
|
276
|
+
npro,
|
|
277
|
+
half_acquisition,
|
|
278
|
+
use_origin,
|
|
279
|
+
reorder)
|
|
280
|
+
offset_factor = traj_offset * (10 ** -6) * eff_bandwidth * over_sampling
|
|
281
|
+
|
|
282
|
+
option_for_hash = (
|
|
283
|
+
float(traj_offset),
|
|
284
|
+
sample_size,
|
|
285
|
+
eff_bandwidth,
|
|
286
|
+
over_sampling,
|
|
287
|
+
int(npro),
|
|
288
|
+
float(np.prod(ext_factors)),
|
|
289
|
+
bool(half_acquisition),
|
|
290
|
+
bool(use_origin),
|
|
291
|
+
bool(reorder),
|
|
292
|
+
correct_ramptime,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
digest = generate_hash(*option_for_hash)
|
|
296
|
+
traj_path = options.cache_dir / f"{digest}.npy"
|
|
297
|
+
if traj_path.exists():
|
|
298
|
+
logger.debug("Trajectory cache hit: %s", traj_path)
|
|
299
|
+
traj = np.load(traj_path)
|
|
300
|
+
else:
|
|
301
|
+
logger.info("Computing trajectory (matrix=%s, n_pro=%s).", sample_size, npro)
|
|
302
|
+
traj = calc_radial_traj3d(
|
|
303
|
+
grad,
|
|
304
|
+
sample_size,
|
|
305
|
+
use_origin,
|
|
306
|
+
over_sampling,
|
|
307
|
+
correct_ramptime=correct_ramptime,
|
|
308
|
+
traj_offset=offset_factor,
|
|
309
|
+
)
|
|
310
|
+
np.save(traj_path, traj)
|
|
311
|
+
logger.debug("Saved trajectory cache: %s", traj_path)
|
|
312
|
+
return traj
|
|
313
|
+
|
|
314
|
+
__all__ = [
|
|
315
|
+
'get_trajectory'
|
|
316
|
+
]
|
brkraw_sordino/typing.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Tuple, Optional
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Options:
|
|
8
|
+
ext_factors: Tuple[float, float, float]
|
|
9
|
+
ignore_samples: int
|
|
10
|
+
offset: int
|
|
11
|
+
num_frames: Optional[int]
|
|
12
|
+
correct_spoketiming: bool
|
|
13
|
+
correct_ramptime: bool
|
|
14
|
+
offreso_ch: Optional[int]
|
|
15
|
+
offreso_freq: float
|
|
16
|
+
mem_limit: float
|
|
17
|
+
clear_cache: bool
|
|
18
|
+
split_ch: bool
|
|
19
|
+
cache_dir: Path
|
|
20
|
+
as_complex: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'Options'
|
|
25
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brkraw-sordino
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: BrkRaw converter hook for SORDINO-ZTE reconstruction
|
|
5
|
+
Author-email: SungHo Lee <shlee@unc.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: brkraw
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: scipy
|
|
12
|
+
Requires-Dist: mri-nufft[finufft]
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: tomli; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# brkraw-sordino
|
|
17
|
+
|
|
18
|
+
SORDINO-ZTE reconstruction hook for BrkRaw.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Hook install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
brkraw hook install brkraw-sordino
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This installs the hook rule from the package manifest (`brkraw_hook.yaml`).
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Once installed, `brkraw` applies the hook automatically when a dataset matches the rule.
|
|
37
|
+
|
|
38
|
+
Basic conversion:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
brkraw convert /path/to/study --scan-id 3 --reco-id 1
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The hook behaves the same whether invoked via the CLI or via the Python API (the same hook entrypoint and arguments are used).
|
|
45
|
+
|
|
46
|
+
To explicitly pass hook options (or override defaults), use `--hook-arg` / `--hook-args-yaml` below.
|
|
47
|
+
|
|
48
|
+
## Hook options
|
|
49
|
+
|
|
50
|
+
Hook arguments can be passed via `brkraw convert` using `--hook-arg` with the
|
|
51
|
+
entrypoint name (`sordino`):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
brkraw convert /path/to/study -s 3 -r 1 \
|
|
55
|
+
--hook-arg sordino:ext_factors=1.2 \
|
|
56
|
+
--hook-arg sordino:offset=2 \
|
|
57
|
+
--hook-arg sordino:split_ch=false
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Pass hook options via YAML (`--hook-args-yaml`)
|
|
61
|
+
|
|
62
|
+
BrkRaw can also load hook arguments from YAML. Generate a template like this:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
brkraw hook preset sordino -o hook_args.yaml
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Edit the generated YAML, then pass it to `brkraw convert` (repeatable):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
brkraw convert /path/to/study -s 3 -r 1 --hook-args-yaml hook_args.yaml
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
hooks:
|
|
78
|
+
sordino:
|
|
79
|
+
ext_factors: 1.2
|
|
80
|
+
offset: 2
|
|
81
|
+
split_ch: false
|
|
82
|
+
# as_complex: true # optional, return (real, imag)
|
|
83
|
+
# cache_dir: ~/.brkraw/cache/sordino # optional (add manually if needed)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Notes:
|
|
87
|
+
|
|
88
|
+
- CLI `--hook-arg` values override YAML.
|
|
89
|
+
- YAML supports both `{hooks: {sordino: {...}}}` and `{sordino: {...}}` shapes.
|
|
90
|
+
- You can also set `BRKRAW_CONVERT_HOOK_ARGS_YAML` (comma-separated paths).
|
|
91
|
+
|
|
92
|
+
Supported keys:
|
|
93
|
+
|
|
94
|
+
- `ext_factors`: scalar or 3-item sequence (default: 1.0)
|
|
95
|
+
- `ignore_samples`: int (default: 1)
|
|
96
|
+
- `offset`: int (default: 0)
|
|
97
|
+
- `num_frames`: int or null (default: None)
|
|
98
|
+
- `correct_spoketiming`: bool (default: false)
|
|
99
|
+
- `correct_ramptime`: bool (default: true)
|
|
100
|
+
- `offreso_ch`: int or null (default: None)
|
|
101
|
+
- `offreso_freq`: float (default: 0.0)
|
|
102
|
+
- `mem_limit`: float (default: 0.5)
|
|
103
|
+
- `clear_cache`: bool (default: true)
|
|
104
|
+
- `split_ch`: bool (default: false, merge channels)
|
|
105
|
+
- `as_complex`: bool (default: false, return complex as (real, imag))
|
|
106
|
+
- `cache_dir`: string path (default: ~/.brkraw/cache/sordino)
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
|
|
110
|
+
- The hook reconstructs data using an adjoint NUFFT and returns magnitude images by default.
|
|
111
|
+
- Multi-channel data defaults to merged channels; set `split_ch=true` to keep channels split.
|
|
112
|
+
- When `split_ch=false`, magnitude uses RSS while complex uses coherent sum.
|
|
113
|
+
- Orientation is normalized when the first 3D axes are spatial; see `notebooks/orientation.ipynb`.
|
|
114
|
+
- Cache files live under `~/.brkraw/cache/sordino` (or `BRKRAW_CONFIG_HOME`).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
brkraw_sordino/__init__.py,sha256=4wLtPWxTOpwnXT6EA6_k23iayCUY4SxuAlSYuxR7jhk,145
|
|
2
|
+
brkraw_sordino/brkraw_hook.yaml,sha256=NA5j1wGUuakEsB49ATJuFXwYgUnH4RfEcqGES3x_PCU,136
|
|
3
|
+
brkraw_sordino/docs.md,sha256=ue04GqwuFc6-gTYwvvNPz8mspUQ8NSrb8_knD9HqD-0,2802
|
|
4
|
+
brkraw_sordino/helper.py,sha256=1_egvhiVFHU7fdjbLwlC5bxLJz6Ak9JtkClz-Ftn9t4,2210
|
|
5
|
+
brkraw_sordino/hook.py,sha256=1GAdfkPg6j6YftjZEQH3JsOe4Gh92gL_0V0hHCOazu4,6274
|
|
6
|
+
brkraw_sordino/orientation.py,sha256=A3znLacwAIXmqsEdd59qam4aYC_qGhoJ1g894zzZUv4,1082
|
|
7
|
+
brkraw_sordino/recon.py,sha256=2ERseWg_ga-HfugAbQzr4G0-m0dA_Buomc23RpWaKOs,4535
|
|
8
|
+
brkraw_sordino/spoketiming.py,sha256=cbHRZRw_N7tYPqAFwmppzwjUuB4scQYfBEYicFfBP4E,6569
|
|
9
|
+
brkraw_sordino/traj.py,sha256=GkxTWFubcPsiyh3doo3lskR2ozEJSFW_inaesMcRstA,11050
|
|
10
|
+
brkraw_sordino/typing.py,sha256=gh5w1HOZAAyHquFg0Wz-ps7V0bXkcESE5te7zV_FRL0,480
|
|
11
|
+
brkraw_sordino/rules/sordino.yaml,sha256=wb_KeOm50tWGUzfmN9BKPLa9sQYLRq-BnRNWOObmgE4,753
|
|
12
|
+
brkraw_sordino/specs/info_spec.yaml,sha256=NmiUuRuGOvwPEc8GJdrXDOMz3W5u8_loqG1_Ho5769c,2607
|
|
13
|
+
brkraw_sordino/specs/metadata_spec.yaml,sha256=8TynDvfqMIqP2U2dA5u5sauapbNmFh3S8NVIg-v8rDU,2854
|
|
14
|
+
brkraw_sordino/specs/prune4recon.yaml,sha256=qGAf-3Gz-4AfFgefHKR4x_rTLDyoN2a3_gD1RKD2kEE,1042
|
|
15
|
+
brkraw_sordino/specs/recon_spec.yaml,sha256=mBEbRniSIwTpWG_B9I_x1DmxVfUTsLqKK9PFBCk_XGo,1707
|
|
16
|
+
brkraw_sordino/specs/utils.py,sha256=BZA_0-He5hKlmEPF8tpDxq0zc74NelODdlPiFrH-ldk,4963
|
|
17
|
+
brkraw_sordino-0.1.3.dist-info/METADATA,sha256=6qZY2sIuIkrGpj0PyVXwauGQNxCgbhq_0FeaXn40LFI,3196
|
|
18
|
+
brkraw_sordino-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
brkraw_sordino-0.1.3.dist-info/entry_points.txt,sha256=6T_Lc6gPoonrjSSJ2fWRRzl966_ubEiKcvH6mFQBuU8,59
|
|
20
|
+
brkraw_sordino-0.1.3.dist-info/top_level.txt,sha256=i-aGVA4GDdQmfLl4H_ZsgEOhhIWVnSROojNLGgmK7G4,15
|
|
21
|
+
brkraw_sordino-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brkraw_sordino
|