|
| 1 | +import sys |
| 2 | +import argparse |
| 3 | +import numpy as np |
| 4 | +from typing import BinaryIO, Iterable, Union |
| 5 | + |
| 6 | +from ismrmrd import Acquisition, Image, ImageHeader, ProtocolDeserializer, ProtocolSerializer |
| 7 | +from ismrmrd.xsd import ismrmrdHeader |
| 8 | +from ismrmrd.constants import ACQ_IS_NOISE_MEASUREMENT, IMTYPE_MAGNITUDE |
| 9 | +from ismrmrd.serialization import SerializableObject |
| 10 | + |
| 11 | +from numpy.fft import fftshift, ifftshift, fftn, ifftn |
| 12 | + |
| 13 | + |
| 14 | +def kspace_to_image(k: np.ndarray, dim=None, img_shape=None) -> np.ndarray: |
| 15 | + """ Computes the Fourier transform from k-space to image space |
| 16 | + along a given or all dimensions |
| 17 | +
|
| 18 | + :param k: k-space data |
| 19 | + :param dim: vector of dimensions to transform |
| 20 | + :param img_shape: desired shape of output image |
| 21 | + :returns: data in image space (along transformed dimensions) |
| 22 | + """ |
| 23 | + if not dim: |
| 24 | + dim = range(k.ndim) |
| 25 | + img = fftshift(ifftn(ifftshift(k, axes=dim), s=img_shape, axes=dim), axes=dim) |
| 26 | + img *= np.sqrt(np.prod(np.take(img.shape, dim))) |
| 27 | + return img |
| 28 | + |
| 29 | + |
| 30 | +def image_to_kspace(img: np.ndarray, dim=None, k_shape=None) -> np.ndarray: |
| 31 | + """ Computes the Fourier transform from image space to k-space space |
| 32 | + along a given or all dimensions |
| 33 | +
|
| 34 | + :param img: image space data |
| 35 | + :param dim: vector of dimensions to transform |
| 36 | + :param k_shape: desired shape of output k-space data |
| 37 | + :returns: data in k-space (along transformed dimensions) |
| 38 | + """ |
| 39 | + if not dim: |
| 40 | + dim = range(img.ndim) |
| 41 | + k = fftshift(fftn(ifftshift(img, axes=dim), s=k_shape, axes=dim), axes=dim) |
| 42 | + k /= np.sqrt(np.prod(np.take(img.shape, dim))) |
| 43 | + return k |
| 44 | + |
| 45 | + |
| 46 | +def acquisition_reader(input: Iterable[SerializableObject]) -> Iterable[Acquisition]: |
| 47 | + for item in input: |
| 48 | + if not isinstance(item, Acquisition): |
| 49 | + # Skip non-acquisition items |
| 50 | + continue |
| 51 | + if item.flags & ACQ_IS_NOISE_MEASUREMENT: |
| 52 | + # Currently ignoring noise scans |
| 53 | + continue |
| 54 | + yield item |
| 55 | + |
| 56 | +def stream_item_sink(input: Iterable[Union[Acquisition, Image]]) -> Iterable[SerializableObject]: |
| 57 | + for item in input: |
| 58 | + if isinstance(item, Acquisition): |
| 59 | + yield item |
| 60 | + elif isinstance(item, Image) and item.data.dtype == np.float32: |
| 61 | + yield item |
| 62 | + else: |
| 63 | + raise ValueError("Unknown item type") |
| 64 | + |
| 65 | +def remove_oversampling(head: ismrmrdHeader, input: Iterable[Acquisition]) -> Iterable[Acquisition]: |
| 66 | + enc = head.encoding[0] |
| 67 | + |
| 68 | + if enc.encodedSpace and enc.encodedSpace.matrixSize and enc.reconSpace and enc.reconSpace.matrixSize: |
| 69 | + eNx = enc.encodedSpace.matrixSize.x |
| 70 | + rNx = enc.reconSpace.matrixSize.x |
| 71 | + else: |
| 72 | + raise Exception('Encoding information missing from header') |
| 73 | + |
| 74 | + for acq in input: |
| 75 | + if eNx != rNx and acq.number_of_samples == eNx: |
| 76 | + xline = kspace_to_image(acq.data, [1]) |
| 77 | + x0 = (eNx - rNx) // 2 |
| 78 | + x1 = x0 + rNx |
| 79 | + xline = xline[:, x0:x1] |
| 80 | + head = acq.getHead() |
| 81 | + head.center_sample = rNx // 2 |
| 82 | + data = image_to_kspace(xline, [1]) |
| 83 | + acq = Acquisition(head, data) |
| 84 | + yield acq |
| 85 | + |
| 86 | +def accumulate_fft(head: ismrmrdHeader, input: Iterable[Acquisition]) -> Iterable[Image]: |
| 87 | + enc = head.encoding[0] |
| 88 | + |
| 89 | + # Matrix size |
| 90 | + if enc.encodedSpace and enc.reconSpace and enc.encodedSpace.matrixSize and enc.reconSpace.matrixSize: |
| 91 | + eNx = enc.encodedSpace.matrixSize.x |
| 92 | + eNy = enc.encodedSpace.matrixSize.y |
| 93 | + eNz = enc.encodedSpace.matrixSize.z |
| 94 | + rNx = enc.reconSpace.matrixSize.x |
| 95 | + rNy = enc.reconSpace.matrixSize.y |
| 96 | + rNz = enc.reconSpace.matrixSize.z |
| 97 | + else: |
| 98 | + raise Exception('Required encoding information not found in header') |
| 99 | + |
| 100 | + # Field of view |
| 101 | + if enc.reconSpace and enc.reconSpace.fieldOfView_mm: |
| 102 | + rFOVx = enc.reconSpace.fieldOfView_mm.x |
| 103 | + rFOVy = enc.reconSpace.fieldOfView_mm.y |
| 104 | + rFOVz = enc.reconSpace.fieldOfView_mm.z if enc.reconSpace.fieldOfView_mm.z else 1 |
| 105 | + else: |
| 106 | + raise Exception('Required field of view information not found in header') |
| 107 | + |
| 108 | + # Number of Slices, Reps, Contrasts, etc. |
| 109 | + ncoils = 1 |
| 110 | + if head.acquisitionSystemInformation and head.acquisitionSystemInformation.receiverChannels: |
| 111 | + ncoils = head.acquisitionSystemInformation.receiverChannels |
| 112 | + |
| 113 | + nslices = 1 |
| 114 | + if enc.encodingLimits and enc.encodingLimits.slice != None: |
| 115 | + nslices = enc.encodingLimits.slice.maximum + 1 |
| 116 | + |
| 117 | + ncontrasts = 1 |
| 118 | + if enc.encodingLimits and enc.encodingLimits.contrast != None: |
| 119 | + ncontrasts = enc.encodingLimits.contrast.maximum + 1 |
| 120 | + |
| 121 | + ky_offset = 0 |
| 122 | + if enc.encodingLimits and enc.encodingLimits.kspace_encoding_step_1 != None: |
| 123 | + ky_offset = int((eNy+1)/2) - enc.encodingLimits.kspace_encoding_step_1.center |
| 124 | + |
| 125 | + current_rep = -1 |
| 126 | + reference_acquisition = None |
| 127 | + buffer = None |
| 128 | + image_index = 0 |
| 129 | + |
| 130 | + def produce_image(buffer: np.ndarray, ref_acq: Acquisition) -> Iterable[Image]: |
| 131 | + nonlocal image_index |
| 132 | + |
| 133 | + if buffer.shape[-3] > 1: |
| 134 | + img = kspace_to_image(buffer, dim=[-1, -2, -3]) |
| 135 | + else: |
| 136 | + img = kspace_to_image(buffer, dim=[-1, -2]) |
| 137 | + |
| 138 | + for contrast in range(img.shape[0]): |
| 139 | + for islice in range(img.shape[1]): |
| 140 | + slice = img[contrast, islice] |
| 141 | + combined = np.squeeze(np.sqrt(np.abs(np.sum(slice * np.conj(slice), axis=0)).astype('float32'))) |
| 142 | + |
| 143 | + xoffset = (combined.shape[-1] + 1) // 2 - (rNx+1) // 2 |
| 144 | + yoffset = (combined.shape[-2] + 1) // 2 - (rNy+1) // 2 |
| 145 | + if len(combined.shape) == 3: |
| 146 | + zoffset = (combined.shape[-3] + 1) // 2 - (rNz+1) // 2 |
| 147 | + combined = combined[zoffset:(zoffset+rNz), yoffset:(yoffset+rNy), xoffset:(xoffset+rNx)] |
| 148 | + combined = np.reshape(combined, (1, combined.shape[-3], combined.shape[-2], combined.shape[-1])) |
| 149 | + elif len(combined.shape) == 2: |
| 150 | + combined = combined[yoffset:(yoffset+rNy), xoffset:(xoffset+rNx)] |
| 151 | + combined = np.reshape(combined, (1, 1, combined.shape[-2], combined.shape[-1])) |
| 152 | + else: |
| 153 | + raise Exception('Array img_combined should have 2 or 3 dimensions') |
| 154 | + |
| 155 | + imghdr = ImageHeader(image_type=IMTYPE_MAGNITUDE) |
| 156 | + imghdr.version = 1 |
| 157 | + imghdr.measurement_uid = ref_acq.measurement_uid |
| 158 | + imghdr.field_of_view[0] = rFOVx |
| 159 | + imghdr.field_of_view[1] = rFOVy |
| 160 | + imghdr.field_of_view[2] = rFOVz/rNz |
| 161 | + imghdr.position = ref_acq.position |
| 162 | + imghdr.read_dir = ref_acq.read_dir |
| 163 | + imghdr.phase_dir = ref_acq.phase_dir |
| 164 | + imghdr.slice_dir = ref_acq.slice_dir |
| 165 | + imghdr.patient_table_position = ref_acq.patient_table_position |
| 166 | + imghdr.average = ref_acq.idx.average |
| 167 | + imghdr.slice = ref_acq.idx.slice |
| 168 | + imghdr.contrast = contrast |
| 169 | + imghdr.phase = ref_acq.idx.phase |
| 170 | + imghdr.repetition = ref_acq.idx.repetition |
| 171 | + imghdr.set = ref_acq.idx.set |
| 172 | + imghdr.acquisition_time_stamp = ref_acq.acquisition_time_stamp |
| 173 | + imghdr.physiology_time_stamp = ref_acq.physiology_time_stamp |
| 174 | + imghdr.image_index = image_index |
| 175 | + image_index += 1 |
| 176 | + |
| 177 | + mrd_image = Image(head=imghdr, data=combined) |
| 178 | + yield mrd_image |
| 179 | + |
| 180 | + for acq in input: |
| 181 | + if acq.idx.repetition != current_rep: |
| 182 | + # If we have a current buffer pass it on |
| 183 | + if buffer is not None and reference_acquisition is not None: |
| 184 | + yield from produce_image(buffer, reference_acquisition) |
| 185 | + |
| 186 | + # Reset buffer |
| 187 | + if acq.data.shape[-1] == eNx: |
| 188 | + readout_length = eNx |
| 189 | + else: |
| 190 | + readout_length = rNx # Readout oversampling has been removed upstream |
| 191 | + |
| 192 | + buffer = np.zeros((ncontrasts, nslices, ncoils, eNz, eNy, readout_length), dtype=np.complex64) |
| 193 | + current_rep = acq.idx.repetition |
| 194 | + reference_acquisition = acq |
| 195 | + |
| 196 | + # Stuff into the buffer |
| 197 | + if buffer is not None: |
| 198 | + contrast = acq.idx.contrast if acq.idx.contrast is not None else 0 |
| 199 | + slice = acq.idx.slice if acq.idx.slice is not None else 0 |
| 200 | + k1 = acq.idx.kspace_encode_step_1 if acq.idx.kspace_encode_step_1 is not None else 0 |
| 201 | + k2 = acq.idx.kspace_encode_step_2 if acq.idx.kspace_encode_step_2 is not None else 0 |
| 202 | + buffer[contrast, slice, :, k2, k1 + ky_offset, :] = acq.data |
| 203 | + |
| 204 | + if buffer is not None and reference_acquisition is not None: |
| 205 | + yield from produce_image(buffer, reference_acquisition) |
| 206 | + buffer = None |
| 207 | + reference_acquisition = None |
| 208 | + |
| 209 | +def reconstruct_ismrmrd_stream(input: BinaryIO, output: BinaryIO): |
| 210 | + with ProtocolDeserializer(input) as reader, ProtocolSerializer(output) as writer: |
| 211 | + stream = reader.deserialize() |
| 212 | + head = next(stream, None) |
| 213 | + if head is None: |
| 214 | + raise Exception("Could not read ISMRMRD header") |
| 215 | + if not isinstance(head, ismrmrdHeader): |
| 216 | + raise Exception("First item in stream is not an ISMRMRD header") |
| 217 | + writer.serialize(head) |
| 218 | + for item in stream_item_sink( |
| 219 | + accumulate_fft(head, |
| 220 | + remove_oversampling(head, |
| 221 | + acquisition_reader(stream)))): |
| 222 | + writer.serialize(item) |
| 223 | + |
| 224 | +if __name__ == "__main__": |
| 225 | + parser = argparse.ArgumentParser(description="Reconstructs an ISMRMRD stream") |
| 226 | + parser.add_argument('-i', '--input', type=str, required=False, help="Input stream, defaults to stdin") |
| 227 | + parser.add_argument('-o', '--output', type=str, required=False, help="Output stream, defaults to stdout") |
| 228 | + args = parser.parse_args() |
| 229 | + |
| 230 | + input = args.input if args.input is not None else sys.stdin.buffer |
| 231 | + output = args.output if args.output is not None else sys.stdout.buffer |
| 232 | + |
| 233 | + reconstruct_ismrmrd_stream(input, output) |
0 commit comments