diff --git a/Dockerfile b/Dockerfile index 07e7156..54cb00f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu${UBUNTU_VERSION} # Python RUN apt-get update && apt-get install --no-install-recommends -y \ - python3-pip && \ - apt-get clean && rm -rf /var/lib/apt/lists/* + python3-pip git && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # Torch ARG TORCH_VERSION=2.5 @@ -14,15 +14,14 @@ ARG TORCH_VARIANT=cpu RUN python3 -m pip --no-cache-dir install \ --index-url https://download.pytorch.org/whl/${TORCH_VARIANT}\ torch==${TORCH_VERSION} - + # MRpro ARG MRPRO_VERSION=0.250107 -RUN python3 -m pip --no-cache-dir install \ - mrpro==${MRPRO_VERSION} +RUN python3 -m pip --no-cache-dir install git+https://github.com/PTB-MR/mrpro@cMRF # MRpro Server WORKDIR /home -COPY *.py /home/mrpro_server/ +COPY * /home/mrpro_server/ CMD ["python3", "/home/mrpro_server/server.py"] # Metadata diff --git a/mrpro_server/combine_mrd_header.py b/mrpro_server/combine_mrd_header.py new file mode 100644 index 0000000..50a35f7 --- /dev/null +++ b/mrpro_server/combine_mrd_header.py @@ -0,0 +1,152 @@ +"""Functions to combine ismrmrd data files and meta files.""" + +from pathlib import Path + +import ismrmrd + + +def read_ismrmrd_dataset(fname: Path) -> tuple: + """Read ismrmrd dataset from file. + + Parameters + ---------- + fname + file path to the ismrmrd dataset. + + Returns + ------- + ismrmrd header and list of acquisitions. + """ + with ismrmrd.File(str(fname), "r") as file: + ds = file[list(file.keys())[-1]] + header = ds.header + acqs = ds.acquisitions[:] + + return header, acqs + + +def insert_traj_from_meta( + data_acqs: list[ismrmrd.acquisition.Acquisition], + meta_acqs: list[ismrmrd.acquisition.Acquisition], +) -> list[ismrmrd.acquisition.Acquisition]: + """ + Insert trajectory information from the meta file into the data file. + + Parameters + ---------- + data_acqs : list + list of acquisitions from the data file. + meta_acqs : list + list of acquisitions from the meta file. + + Returns + ------- + list of acquisitions with the trajectory information from the meta file. + """ + if not len(data_acqs) == len(meta_acqs): + raise ValueError("Number of acquisitions in data and meta file do not match.") + + for i, (acq_d, acq_m) in enumerate(zip(data_acqs, meta_acqs, strict=False)): + if not acq_d.number_of_samples == acq_m.number_of_samples: + raise ValueError(f"Number of samples in acquisition {i} do not match.") + + # insert trajectory information from meta file + acq_d.resize( + number_of_samples=acq_d.number_of_samples, + active_channels=acq_d.active_channels, + trajectory_dimensions=acq_m.trajectory_dimensions, + ) + acq_d.traj[:] = acq_m.traj[:] + data_acqs[i] = acq_d + + return data_acqs + + +def update_header_from_meta( + data_header: ismrmrd.xsd.ismrmrdHeader, + meta_header: ismrmrd.xsd.ismrmrdHeader, + enc_idx: int = 0, +) -> ismrmrd.xsd.ismrmrdHeader: + """Update the header of the data file with the information from the meta file. + + Parameters + ---------- + data_header : ismrmrd.xsd.ismrmrdHeader + Header of the ISMRMRD data file. + meta_header : ismrmrd.xsd.ismrmrdHeader + Header of the ISMRMRD meta file created with the seq-file. + enc_idx : int, optional + Encoding index, by default 0 + + Returns + ------- + ismrmrd.xsd.ismrmrdHeader + Updated header. + """ + + # Helper function to copy attributes if they are not None + def copy_attributes(source, target, attributes): + for attr in attributes: + value = getattr(source, attr, None) + if value is not None: + setattr(target, attr, value) + + # Define the attributes to update for encodedSpace and reconSpace + attributes_to_update = ["matrixSize", "fieldOfView_mm"] + + # Update encodedSpace + copy_attributes( + meta_header.encoding[enc_idx].encodedSpace, + data_header.encoding[enc_idx].encodedSpace, + attributes_to_update, + ) + + # Update reconSpace + copy_attributes( + meta_header.encoding[enc_idx].reconSpace, + data_header.encoding[enc_idx].reconSpace, + attributes_to_update, + ) + + # Update trajectory type + if meta_header.encoding[enc_idx].trajectory is not None: + data_header.encoding[enc_idx].trajectory = meta_header.encoding[enc_idx].trajectory + + return data_header + + +def combine_ismrmrd_files(data_file: Path, meta_file: Path, filename_ext: str = "_with_traj.h5") -> ismrmrd.Dataset: + """Combine ismrmrd data file and meta file. + + Parameters + ---------- + data_file + path to the ismrmrd data file + meta_file + path to the ismrmrd meta file + filename_ext, optional + filename extension of the output file, by default '_with_traj.h5' + + Returns + ------- + combined ismrmrd file from data and meta file. + """ + filename_out = data_file.parent / (data_file.stem + filename_ext) + + data_header, data_acqs = read_ismrmrd_dataset(data_file) + meta_header, meta_acqs = read_ismrmrd_dataset(meta_file) + + new_acqs = insert_traj_from_meta(data_acqs, meta_acqs) + new_header = update_header_from_meta(data_header, meta_header) + + # Create new file + ds = ismrmrd.Dataset(filename_out) + ds.write_xml_header(new_header.toXML()) + + # add acquisitions with trajectory information + for acq in new_acqs: + ds.append_acquisition(acq) + + ds.close() + + return ds diff --git a/mrpro_server/flip_angle_pattern.txt b/mrpro_server/flip_angle_pattern.txt new file mode 100644 index 0000000..eb6e4f0 --- /dev/null +++ b/mrpro_server/flip_angle_pattern.txt @@ -0,0 +1,705 @@ +4.00 +4.57 +5.13 +5.70 +6.27 +6.83 +7.40 +7.97 +8.53 +9.10 +9.67 +10.23 +10.80 +11.37 +11.93 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +4.00 +4.98 +5.97 +6.95 +7.93 +8.92 +9.90 +10.88 +11.87 +12.85 +13.83 +14.82 +15.80 +16.78 +17.77 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +4.57 +5.13 +5.70 +6.27 +6.83 +7.40 +7.97 +8.53 +9.10 +9.67 +10.23 +10.80 +11.37 +11.93 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +4.00 +4.98 +5.97 +6.95 +7.93 +8.92 +9.90 +10.88 +11.87 +12.85 +13.83 +14.82 +15.80 +16.78 +17.77 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +4.57 +5.13 +5.70 +6.27 +6.83 +7.40 +7.97 +8.53 +9.10 +9.67 +10.23 +10.80 +11.37 +11.93 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +12.50 +4.00 +4.98 +5.97 +6.95 +7.93 +8.92 +9.90 +10.88 +11.87 +12.85 +13.83 +14.82 +15.80 +16.78 +17.77 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +18.75 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +4.00 +5.40 +6.80 +8.20 +9.60 +11.00 +12.40 +13.80 +15.20 +16.60 +18.00 +19.40 +20.80 +22.20 +23.60 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 +25.00 \ No newline at end of file diff --git a/mrpro_server/header.h5 b/mrpro_server/header.h5 new file mode 100644 index 0000000..1878a20 Binary files /dev/null and b/mrpro_server/header.h5 differ diff --git a/mrpro_server/process.py b/mrpro_server/process.py index eb16000..b7c5d24 100644 --- a/mrpro_server/process.py +++ b/mrpro_server/process.py @@ -6,11 +6,16 @@ import tempfile from pathlib import Path +from einops import rearrange +import mrpro.operators.models.EPG + import constants import ismrmrd import mrpro import torch +from combine_mrd_header import combine_ismrmrd_files + def process( acquisitions: list[ismrmrd.Acquisition], @@ -22,6 +27,11 @@ def process( """Process ISMRMRD Acquisitions to ISMRMRD Images.""" reference = acquisitions[0] # Only needed until we can build header from IData + + # read flip angle pattern + with open(Path("flip_angle_pattern.txt"), "r") as file: + fa = torch.as_tensor([float(line) for line in file.readlines()]) / 180 * torch.pi + with tempfile.TemporaryDirectory() as tmpdir: # TODO: create KData directly from ISMRMRD Acquisitions file = Path(tmpdir) / "data.mrd" @@ -29,24 +39,106 @@ def process( dset.write_xml_header(metadata) while acquisitions: dset.append_acquisition(acquisitions.pop()) + + # combine data and header to data_with_traj.h5 + _ = combine_ismrmrd_files(file, Path("header.h5")) + logging.info(f"KData written to {file!s}. Size: {file.stat().st_size}") gc.collect() ################################################## - kdata = mrpro.data.KData.from_file(file, ktrajectory=mrpro.data.traj_calculators.KTrajectoryCartesian()) - recon = mrpro.algorithms.reconstruction.DirectReconstruction(kdata, csm=None) - image = recon(kdata) + # Define the T1 and T2 values to be included in the dictionaries + t1 = torch.cat( + (torch.arange(50, 2000 + 10, 10), torch.arange(2020, 3000 + 20, 20), torch.arange(3050, 5000 + 50, 50)) + ) + t2 = torch.cat((torch.arange(6, 100 + 2, 2), torch.arange(105, 200 + 5, 5), torch.arange(220, 500 + 20, 20))) + + n_lines_per_img = 20 + n_lines_overlap = 10 + + kdata = mrpro.data.KData.from_file( + Path(tmpdir) / "data_with_traj.h5", mrpro.data.traj_calculators.KTrajectoryIsmrmrd() + ) + logging.info("KData created") + avg_recon = mrpro.algorithms.reconstruction.DirectReconstruction(kdata) + logging.info("avg img created") + + # Split data into dynamics and reconstruct + dyn_idx = mrpro.utils.split_idx(torch.arange(0, 47), n_lines_per_img, n_lines_overlap) + dyn_idx = torch.cat([dyn_idx + ind * 47 for ind in range(15)], dim=0) + + kdata_dyn = kdata.split_k1_into_other(dyn_idx, other_label="repetition") + + dyn_recon = mrpro.algorithms.reconstruction.DirectReconstruction(kdata_dyn, csm=avg_recon.csm) + logging.info("dyn img reco done") + dcf_data_dyn = rearrange(avg_recon.dcf.data, "k2 k1 other k0->other k2 k1 k0") + dcf_data_dyn = rearrange( + dcf_data_dyn[dyn_idx.flatten(), ...], "(other k1) 1 k2 k0->other k2 k1 k0", k1=dyn_idx.shape[-1] + ) + dyn_recon.dcf = mrpro.data.DcfData(dcf_data_dyn) + + img = dyn_recon(kdata_dyn).rss()[:, 0, :, :] + logging.info("dyn images created") + + # Dictionary settings + t1, t2 = torch.broadcast_tensors(t1[None, :], t2[:, None]) + t1_all = t1.flatten().to(dtype=torch.float32) + t2_all = t2.flatten().to(dtype=torch.float32) + + t1 = t1_all[t1_all >= t2_all] + t2 = t2_all[t1_all >= t2_all] + m0 = torch.ones_like(t1) + + # Dictionary calculation + n_rf_pulses_per_block = 47 # 47 RF pulses in each block + acq_t_ms = kdata.header.acq_info.acquisition_time_stamp[0, 0, :, 0] * 2.5 + delay_between_blocks = [ + acq_t_ms[n_block * n_rf_pulses_per_block] - acq_t_ms[n_block * n_rf_pulses_per_block - 1] + for n_block in range(1, 3 * 5) + ] + delay_between_blocks.append(delay_between_blocks[-1]) # last delay is not needed but makes computations easier + + flip_angles = fa + rf_phases = 0.0 + te = 1.52 + tr = 6.6 + inv_prep_ti = [21, None, None, None, None] * 3 # 21 ms delay after inversion pulse in block 0 + t2_prep_te = [None, None, 30, 50, 100] * 3 # T2-preparation pulse with TE = 30, 50, 100 + delay_due_to_prep = [0, 30, 50, 100, 21] * 3 + delay_after_block = [ + trig_delay - prep_delay for prep_delay, trig_delay in zip(delay_due_to_prep, delay_between_blocks) + ] + epg_mrf_fisp = mrpro.operators.models.EPG.EpgMrfFispWithPreparation( + flip_angles, rf_phases, te, tr, inv_prep_ti, t2_prep_te, n_rf_pulses_per_block, delay_after_block + ) + (signal_dictionary,) = epg_mrf_fisp.forward(m0, t1, t2) + + signal_dictionary = rearrange( + signal_dictionary[dyn_idx.flatten(), ...], "(other k1) t->other t k1", k1=dyn_idx.shape[-1] + ) + signal_dictionary = torch.mean(signal_dictionary, dim=-1) + signal_dictionary = signal_dictionary.abs() + + # Normalise dictionary entries + vector_norm = torch.linalg.vector_norm(signal_dictionary, dim=0) + signal_dictionary /= vector_norm + + # Dictionary matching + n_y, n_x = img.shape[-2:] + dot_product = torch.mm(rearrange(img.abs(), "other y x->(y x) other"), signal_dictionary) + idx_best_match = torch.argmax(torch.abs(dot_product), dim=1) + data = rearrange(t1[idx_best_match], "(y x)->1 1 1 y x", y=n_y, x=n_x).to(torch.complex64) + # t2_match = rearrange(t2[idx_best_match], "(y x)->y x", y=n_y, x=n_x) logging.info("Reconstruction done") - logging.debug(f"Memory used by Python process: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024:.2f} MB") + logging.debug( + f"Memory used by Python process: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024:.2f} MB" + ) ################################################## # TODO: create ISMRMRD Image from IData - data = image.rss(keepdim=True).detach().cpu().flatten(end_dim=-5).to(torch.complex64) - scale = (data.shape[-3:].numel()) ** 0.5 - data = data * scale # ICE uses no scaling in backward fov = kdata.header.recon_fov.apply(mrpro.utils.unit_conversion.m_to_mm) new_images = [] diff --git a/settings.json b/settings.json index 350ba3f..319c624 100644 --- a/settings.json +++ b/settings.json @@ -1,8 +1,8 @@ { - "name": "MRpro-RSS", + "name": "MRpro-MRF", "version": "1.{{ mrpro_minor }}.1", "vendor": "PTB", - "information": "MRpro RSS reconstruction", + "information": "MRpro MRF reconstruction", "can_process_adjustment_data": false, "can_use_gpu": false, "min_count_required_gpus": 0,