def test_shepp_logan(self): '''The much-abused Shepp-Logan.''' ph = shepp_logan(self.N) ph /= np.max(ph.flatten()) phs = ph[..., None] * self.mps kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(phs, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) kspace_u = np.array(np.zeros_like(kspace)) # wrap for pylint kspace_u[:, ::2, :] = kspace[:, ::2, :] ctr = int(self.N / 2) pd = 5 calib = kspace[:, ctr - pd:ctr + pd, :].copy() recon = grappa(kspace_u, calib, (5, 5), coil_axis=-1) recon = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(recon, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) recon = np.abs(np.sqrt(np.sum(recon * np.conj(recon), axis=-1))) recon /= np.max(recon.flatten()) # Make sure less than 4% NRMSE # print(compare_nrmse(ph, recon)) self.assertTrue(compare_nrmse(ph, recon) < .04)
def vcgrappa(kspace, calib, *args, coil_axis=-1, **kwargs): '''Virtual Coil GRAPPA. See pygrappa.grappa() for argument list. Notes ----- Implements modifications to GRAPPA as described in [1]_. The only change I can see is stacking the conjugate coils in the coil dimension. For best results, make sure there is a suitably chosen background phase variation as described in the paper. This function is a wrapper of pygrappa.cgrappa(). The existing coils are conjugated, added to the coil dimension, and passed through along with all other arguments. References ---------- .. [1] Blaimer, Martin, et al. "Virtual coil concept for improved parallel MRI employing conjugate symmetric signals." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 61.1 (2009): 93-102. ''' # Move coil axis to end kspace = np.moveaxis(kspace, coil_axis, -1) calib = np.moveaxis(calib, coil_axis, -1) ax = (0, 1) # remember the type we started out with, np.fft will change # to complex128 regardless of what we started with tipe = kspace.dtype # We will return twice the number of coils we started with nc = kspace.shape[-1] # In and out of kspace to get conjugate coils vc_kspace = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( kspace, axes=ax), axes=ax), axes=ax) vc_kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( np.conj(vc_kspace), axes=ax), axes=ax), axes=ax) # Same deal for calib... vc_calib = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( calib, axes=ax), axes=ax), axes=ax) vc_calib = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( np.conj(vc_calib), axes=ax), axes=ax), axes=ax) # Put all our ducks in a row... kspace = np.concatenate((kspace, vc_kspace), axis=-1) calib = np.concatenate((calib, vc_calib), axis=-1) # Pass through to GRAPPA return grappa( kspace, calib, coil_axis=-1, nc_desired=2*nc, *args, **kwargs).astype(tipe)
def test_validP_issue(self): '''Replication.''' # Pull in data we know will break grappa pc = np.load('pc0.npy') print('Loaded PC0') _sx, _sy, sz, _sc = pc.shape[:] pd = 12 ctr = int(pc.shape[1] / 2) # I think it happens somewhere in this dataset: slice 35 for ii in range(35, sz): calib = pc[:, ctr - pd:ctr + pd + 1, ii, :].copy() _recon = grappa(pc[:, :, ii, :], calib, (5, 5), coil_axis=-1) print('done with slice %d' % ii)
def vcgrappa(kspace, calib, *args, coil_axis=-1, **kwargs): '''Virtual Coil GRAPPA. See pygrappa.grappa() for argument list. Notes ----- Implements modifications to GRAPPA as described in [1]_. The only change I can see is stacking the conjugate coils in the coil dimension. This function is a wrapper of pygrappa.cgrappa(). The existing coils are conjugated, added to the coil dimension, and passed through along with all other arguments. References ---------- .. [1] Blaimer, Martin, et al. "Virtual coil concept for improved parallel MRI employing conjugate symmetric signals." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 61.1 (2009): 93-102. ''' # Move coil axis to end kspace = np.moveaxis(kspace, coil_axis, -1) calib = np.moveaxis(calib, coil_axis, -1) # Create conjugate virtual coils for kspace ax = (0, 1) vc_kspace = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( kspace, axes=ax), axes=ax), axes=ax) vc_kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( np.conj(vc_kspace), axes=ax), axes=ax), axes=ax) # Make sure zeros stay the same through FFTs: vc_kspace = vc_kspace*(np.abs(kspace) > 0) kspace = np.concatenate((kspace, vc_kspace), axis=-1) # Create conjugate virtual coils for calib vc_calib = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( calib, axes=ax), axes=ax), axes=ax) vc_calib = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( np.conj(vc_calib), axes=ax), axes=ax), axes=ax) calib = np.concatenate((calib, vc_calib), axis=-1) # Pass through to GRAPPA return grappa(kspace, calib, coil_axis=-1, *args, **kwargs)
# ========================================================== # # Open up a new readonly memmap -- this is where you would # likely start with data you really wanted to process kspace = np.memmap(kspace_file, mode='r', shape=(N, N, ncoils), dtype='complex') # calibrate a kernel kernel_size = (5, 5) # reconstruct, write res out to a memmap with name res_file grappa(kspace, calib, kernel_size, coil_axis=-1, lamda=0.01, memmap=True, memmap_filename=res_file) # Take a look by opening up the memmap res = np.memmap(res_file, mode='r', shape=(N, N, ncoils), dtype='complex') res = np.abs( np.sqrt(N**2) * np.fft.fftshift(np.fft.ifft2( np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) res0 = np.zeros((2 * N, 2 * N))
# crop 20x20 window from the center of k-space for calibration pd = 10 ctr = int(N / 2) calib = kspace[ctr - pd:ctr + pd, ctr - pd:ctr + pd, :].copy() # calibrate a kernel kernel_size = (5, 5) # undersample by a factor of 2 in both kx and ky kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # reconstruct: res = grappa(kspace, calib, kernel_size, coil_axis=-1, lamda=0.01, memmap=False) # Take a look res = np.abs( np.sqrt(N**2) * np.fft.fftshift( np.fft.ifft2(np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) res0 = np.zeros((2 * N, 2 * N)) kk = 0 for idx in np.ndindex((2, 2)): ii, jj = idx[:] res0[ii * N:(ii + 1) * N, jj * N:(jj + 1) * N] = res[..., kk] kk += 1 plt.imshow(res0, cmap='gray') plt.show()
def process(self, recondata): # receive kspace data and # extract acq_data and acq_header array_acq_headers = recondata[0].data.headers kspace_data = recondata[0].data.data try: if recondata[0].ref.data is not None: print("reference data exist") print( np.shape(recondata[0].ref.data) ) # only for repetition 0 # il faut creer le bucket recon grappa reference = recondata[0].ref.data data = recondata[0].data.data print(np.shape(reference)) print(np.shape(data)) self.array_calib = reference np.save('/tmp/gadgetron/reference', reference) np.save('/tmp/gadgetron/data', data) except: print("reference data not exist") # grappa array_data = recondata[0].data.data dims = np.shape(recondata[0].data.data) kspace_data_tmp = np.ndarray(dims, dtype=np.complex64) for slc in range(0, dims[6]): for n in range(0, dims[5]): for s in range(0, dims[4]): kspace = array_data[:, :, :, :, s, n, slc] calib = self.array_calib[:, :, :, :, s, n, slc] calib = np.squeeze(calib, axis=2) kspace = np.squeeze(kspace, axis=2) sx, sy, ncoils = kspace.shape[:] cx, cy, ncoils = calib.shape[:] # Here's the actual reconstruction res = grappa(kspace, calib, kernel_size=(5, 5), coil_axis=-1) # Here's the resulting shape of the reconstruction. The coil # axis will end up in the same place you provided it in sx, sy, ncoils = res.shape[:] kspace_data_tmp[:, :, 0, :, s, n, slc] = res # ifft, this is necessary for the next gadget #image = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2)) image = transform.transform_kspace_to_image(kspace_data_tmp, dim=(0, 1, 2)) # create a new IsmrmrdImageArray array_data = IsmrmrdImageArray() # attache the images to the IsmrmrdImageArray array_data.data = image # get dimension for the acq_headers dims_header = np.shape(recondata[0].data.headers) # get one header with typical info acq = np.ravel(array_acq_headers)[0] print("acq.idx.repetition", acq.idx.repetition) if (acq.idx.repetition == 0): np.save('/tmp/gadgetron/image', image) headers_list = [] base_header = ismrmrd.ImageHeader() base_header.version = 2 ndims_image = np.shape(image) base_header.channels = ndims_image[3] base_header.matrix_size = (image.shape[0], image.shape[1], image.shape[2]) print((image.shape[0], image.shape[1], image.shape[2])) base_header.position = acq.position base_header.read_dir = acq.read_dir base_header.phase_dir = acq.phase_dir base_header.slice_dir = acq.slice_dir base_header.patient_table_position = acq.patient_table_position base_header.acquisition_time_stamp = acq.acquisition_time_stamp base_header.image_index = 0 base_header.image_series_index = 0 base_header.data_type = ismrmrd.DATATYPE_CXFLOAT base_header.image_type = ismrmrd.IMTYPE_MAGNITUDE print("ready to list") for slc in range(0, dims_header[4]): for n in range(0, dims_header[3]): for s in range(0, dims_header[2]): #for e2 in range(0, dims_header[1]): # for e1 in range(0, dims_header[0]): headers_list.append(base_header) array_headers_test = np.array(headers_list, dtype=np.dtype(object)) print(type(array_headers_test)) print(np.shape(array_headers_test)) array_headers_test = np.reshape( array_headers_test, (dims_header[2], dims_header[3], dims_header[4])) print(type(array_headers_test)) print(np.shape(array_headers_test)) print("---> ok 0") # how to copy acquisition header into image header in python ? for slc in range(0, dims_header[4]): for n in range(0, dims_header[3]): for s in range(0, dims_header[2]): # for e2 in range(0, dims_header[1]): # for e1 in range(0, dims_header[0]): array_headers_test[s, n, slc].slice = slc #print(s,n,slc) #print(type(array_headers_test[s,n,slc])) #print(array_headers_test[s,n,slc].slice) #print("---> ok 1") # print(np.shape(array_image_header)) # attache the image headers to the IsmrmrdImageArray array_data.headers = array_headers_test #print("---> ok 2") # Return image to Gadgetron #print(np.shape(array_data.data)) #print(np.shape(array_data.headers)) #print(type(array_data.data)) #print(type(array_data.headers)) #print(type(array_data)) # send the data to the next gadget for slc in range(0, dims_header[4]): for n in range(0, dims_header[3]): for s in range(0, dims_header[2]): #print("send out image %d-%d-%d" % (s, n, slc)) a = array_data.data[:, :, :, :, s, n, slc] #array_data.headers[s,n,slc].slice=slc #print(a.shape, array_data.headers[s,n,slc].slice) self.put_next(array_data.headers[s, n, slc], a) #self.put_next( [IsmrmrdReconBit(array_data.headers, array_data.data)] , ) print("----------------------------------------------") return 0
ax = (0, 1) kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(ph, axes=ax), axes=ax), axes=ax) # 20x20 ACS region pad = 10 ctr = int(N / 2) calib = kspace[ctr - pad:ctr + pad, ctr - pad:ctr + pad, :].copy() # R=2x2 kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # Reconstruct using both GRAPPA and VC-GRAPPA res_grappa = grappa(kspace, calib) res_vcgrappa = vcgrappa(kspace, calib) # Bring back to image space imspace_vcgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( res_vcgrappa, axes=ax), axes=ax), axes=ax) imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(res_grappa, axes=ax), axes=ax), axes=ax) # Coil combine (sum-of-squares) cc_vcgrappa = np.sqrt(np.sum(np.abs(imspace_vcgrappa)**2, axis=-1)) cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1))
def PyGrappaBufferedDataPythonGadget(connection): logging.info("Python reconstruction running - reading readout data") start = time.time() counter=0 reference=[] for acquisition in connection: #acquisition is a vector of structure called reconBit print(type(acquisition[0])) for reconBit in acquisition: print(type(reconBit)) # reconBit.ref is the calibration for parallel imaging # reconBit.data is the undersampled dataset print('-----------------------') # each of them include a specific header and the kspace data print(type(reconBit.data.headers)) print(type(reconBit.data.data)) print(reconBit.data.headers.shape) print(reconBit.data.data.shape) index= get_first_index_of_non_empty_header(reconBit.data.headers.flat) repetition=reconBit.data.headers.flat[index].idx.repetition print(repetition) reference_header=reconBit.data.headers.flat[0] try: if reconBit.ref.data is not None: print("reference data exist") np.save('/tmp/gadgetron/reference', reconBit.ref.data) reference=reconBit.ref.data else: print("reference data not exist") except: print("issue with reference data") dims=reconBit.data.data.shape kspace_data_tmp=np.zeros(dims, reconBit.data.data.dtype) for slc in range(0, dims[6]): for n in range(0, dims[5]): for s in range(0, dims[4]): kspace=reconBit.data.data[:,:,:,:,s,n,slc] calib=reference[:,:,:,:,s,n,slc] calib=np.squeeze(calib,axis=2) kspace=np.squeeze(kspace,axis=2) sx, sy, ncoils = kspace.shape[:] cx, cy, ncoils = calib.shape[:] # Here's the actual reconstruction res = grappa(kspace, calib, kernel_size=(5, 5), coil_axis=-1) # Here's the resulting shape of the reconstruction. The coil # axis will end up in the same place you provided it in sx, sy, ncoils = res.shape[:] kspace_data_tmp[:,:,0,:,s,n,slc]=res # ifft, this is necessary for the next gadget #image = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2)) im = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2)) plt.subplot(121) plt.imshow(np.abs(np.squeeze(reconBit.data.data[:,:,0,0,0,0,0]))) plt.subplot(122) plt.imshow(np.abs(np.squeeze(im[:,:,0,0,0,0,0]))) plt.show() send_reconstructed_images(connection,im,reference_header) #connection.send(acquisition) logging.info(f"Python reconstruction done. Duration: {(time.time() - start):.2f} s")
def tgrappa( kspace, calib_size=(20, 20), kernel_size=(5, 5), coil_axis=-2, time_axis=-1): '''Temporal GRAPPA. Parameters ---------- kspace : array_like 2+1D multi-coil k-space data to reconstruct from (total of 4 dimensions). Missing entries should have exact zeros in them. calib_size : array_like, optional Size of calibration region at the center of kspace. kernel_size : tuple, optional Desired shape of the in-plane calibration regions: (kx, ky). coil_axis : int, optional Dimension holding coil data. time_axis : int, optional Dimension holding time data. Returns ------- res : array_like Reconstructed k-space data. Raises ------ ValueError When no complete ACS region can be found. Notes ----- Implementation of the method proposed in [1]_. The idea is to form ACS regions using data from adjacent time frames. For example, in the case of 1D undersampling using undersampling factor R, at least R time frames must be merged to form a completely sampled ACS. Then we can simply supply the undersampled data and the synthesized ACS to GRAPPA. Thus the heavy lifting of this function will be in determining the ACS calibration region at each time frame. References ---------- .. [1] Breuer, Felix A., et al. "Dynamic autocalibrated parallel imaging using temporal GRAPPA (TGRAPPA)." Magnetic Resonance in Medicine: An Official Journal of the International Society for Magnetic Resonance in Medicine 53.4 (2005): 981-985. ''' # Move coil and time axes to a place we can find them kspace = np.moveaxis(kspace, (coil_axis, time_axis), (-2, -1)) sx, sy, _sc, st = kspace.shape[:] cx, cy = calib_size[:] sx2, sy2 = int(sx/2), int(sy/2) cx2, cy2 = int(cx/2), int(cy/2) adjx, adjy = int(np.mod(cx, 2)), int(np.mod(cy, 2)) # Make sure that it's even possible to create complete ACS, we'll # have problems in the loop if we don't catch it here! if not np.all(np.sum(np.abs(kspace[ sx2-cx2:sx2+cx2+adjx, sy2-cy2:sy2+cy2+adjy, ...]), axis=-1)): raise ValueError('Full ACS region cannot be found!') # To avoid running GRAPPA more than once on one time frame, # we'll keep track of which frames have been reconstruced: completed_tframes = np.zeros(st, dtype=bool) # Initialize the progress bar pbar = tqdm(total=st, leave=False, desc='TGRAPPA') # Iterate through all time frames, construct ACS regions, and # run GRAPPA on each time slice res = np.zeros(kspace.shape, dtype=kspace.dtype) tt = 0 # time frame index done = False # True when all time frames have been consumed from_end = False # start at the end and go backwards while not done: # Find next feasible kernel -- Strategy: consume time frames # until all kernel elements have been filled. We won't # assume that overlaps will not happen frame to frame, so we # will appropriately average each kernel position by keeping # track of how many samples are in a position with 'counts' got_kernel = False calib = [] counts = np.zeros((cx, cy), dtype=int) tframes = [] # time frames over which the ACS is valid while not got_kernel: if not completed_tframes[tt]: tframes.append(tt) calib.append(kspace[ sx2-cx2:sx2+cx2+adjx, sy2-cy2:sy2+cy2+adjy, :, tt].copy()) counts += np.abs(calib[-1][..., 0]) > 0 if np.all(counts > 0): got_kernel = True # Go to next time frame except maybe for the last ACS if not from_end: tt += 1 # Consume the next time frame else: tt -= 1 # Consume previous time frame # If we need more time frames than we have, then we need # to start from the end and come forward. This can only # happen on the last iteration of the outer loop if not got_kernel and tt == st: # Start at the end tt = st-1 # Reset ACS, counts, and tframes calib = [] counts = np.zeros((cx, cy), dtype=int) tframes = [] # Let the loop know we want to reverse directions from_end = True # Now average over all time frames to get a single ACS calib = np.sum(calib, axis=0)/counts[..., None] # This ACS region is valid over all time frames used to # create it. Run GRAPPA on each valid time frame with calib: for t0 in tframes: res[..., t0] = grappa( kspace[..., t0], calib, kernel_size) completed_tframes[t0] = True pbar.update(1) # Stopping condition: end when all time frames are consumed if np.all(completed_tframes): done = True # Close out the progress bar pbar.close() # Move axes back to where the user had them return np.moveaxis(res, (-1, -2), (time_axis, coil_axis))
# 20x20 ACS region pad = 10 ctr = int(N / 2) calib = kspace[ctr - pad:ctr + pad, ctr - pad:ctr + pad, :].copy() # R=2x2 kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # Find Tikhonov param that minimizes NRMSE nlam = 20 lamdas = np.linspace(1e-9, 5e-4, nlam) mse = np.zeros(lamdas.shape) akspace = np.abs(kspace_orig) for ii, lamda in tqdm(enumerate(lamdas), total=nlam, leave=False): recon = grappa(kspace, calib, lamda=lamda) mse[ii] = compare_nrmse(akspace, np.abs(recon)) # Optimal param minimizes NRMSE idx = np.argmin(mse) # Take a look plt.plot(lamdas, mse) plt.plot(lamdas[idx], mse[idx], 'rx', label='Optimal lamda') plt.title('Tikhonov param vs NRMSE') plt.xlabel('lamda') plt.ylabel('NRMSE') plt.legend() plt.show()
# generate 4 coil phantom ph = shepp_logan(N) imspace = ph[..., None] * mps imspace = imspace.astype('complex') ax = (0, 1) kspace = 1 / np.sqrt(N**2) * np.fft.fftshift( np.fft.fft2(np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) # crop 20x20 window from the center of k-space for calibration pd = 10 ctr = int(N / 2) calib = kspace[ctr - pd:ctr + pd, ctr - pd:ctr + pd, :].copy() # calibrate a kernel kernel_size = (5, 5) # undersample by a factor of 2 in both x and y kspace[::2, 1::2, :] = 0 kspace[1::2, ::2, :] = 0 # Time both implementations t0 = time() recon0 = grappa(kspace, calib, (5, 5)) print(' GRAPPA: %g' % (time() - t0)) t0 = time() recon1 = cgrappa(kspace, calib, (5, 5)) print('CGRAPPA: %g' % (time() - t0)) assert np.allclose(recon0, recon1)