ulfsynth 0.1.0__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.
ulfsynth/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ ULF-Synth: Physics-Guided Ultra-Low-Field MRI Enhancement & Simulation.
3
+
4
+ Submodules:
5
+ simulate — Synthesize realistic ULF MRI from high-field NIfTI volumes
6
+ enhance — Enhance ULF MRI using pretrained restoration models
7
+ cli — Command-line interface (ulfsynth simulate, ulfsynth enhance)
8
+ """
9
+
10
+ __version__ = "0.1.0"
ulfsynth/cli.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Command-line interface for ULF-Synth.
3
+
4
+ Usage:
5
+ ulfsynth simulate input output [--seed N] [--quiet]
6
+ ulfsynth enhance input output [--device DEVICE] [--quiet]
7
+ ulfsynth download-weights [--force]
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+
13
+
14
+ def add_simulate_parser(subparsers):
15
+ p = subparsers.add_parser("simulate", help="Synthesize ULF MRI from HF volumes")
16
+ p.add_argument("input", help="Path to .nii/.nii.gz file or folder of NIfTI files")
17
+ p.add_argument("output", help="Output file path or folder")
18
+ p.add_argument("--seed", type=int, default=None, help="Random seed")
19
+ p.add_argument("--quiet", action="store_true", help="Suppress per-file output")
20
+ p.set_defaults(func=_run_simulate)
21
+
22
+
23
+ def _run_simulate(args):
24
+ from ulfsynth.simulate import simulate_file, simulate_folder
25
+ verbose = not args.quiet
26
+ if os.path.isdir(args.input):
27
+ simulate_folder(args.input, args.output, seed=args.seed, verbose=verbose)
28
+ elif os.path.isfile(args.input):
29
+ simulate_file(args.input, args.output, seed=args.seed, verbose=verbose)
30
+ else:
31
+ raise FileNotFoundError(f"Input not found: {args.input}")
32
+
33
+
34
+ def add_enhance_parser(subparsers):
35
+ p = subparsers.add_parser("enhance", help="Enhance ULF MRI using pretrained model")
36
+ p.add_argument("input", help="Path to .nii/.nii.gz file or folder of NIfTI files")
37
+ p.add_argument("output", help="Output file path or folder")
38
+ p.add_argument("--device", type=str, default="cuda",
39
+ choices=["cuda", "cpu", "mps"],
40
+ help="Device for inference (default: cuda)")
41
+ p.add_argument("--quiet", action="store_true", help="Suppress progress output")
42
+ p.set_defaults(func=_run_enhance)
43
+
44
+
45
+ def _run_enhance(args):
46
+ from ulfsynth.enhance import enhance_file, enhance_folder
47
+ verbose = not args.quiet
48
+ if os.path.isdir(args.input):
49
+ enhance_folder(args.input, args.output, device=args.device, verbose=verbose)
50
+ elif os.path.isfile(args.input):
51
+ enhance_file(args.input, args.output, device=args.device, verbose=verbose)
52
+ else:
53
+ raise FileNotFoundError(f"Input not found: {args.input}")
54
+
55
+
56
+ def add_download_parser(subparsers):
57
+ p = subparsers.add_parser("download-weights",
58
+ help="Download pretrained model weights from HuggingFace")
59
+ p.add_argument("--force", action="store_true",
60
+ help="Force re-download even if cached")
61
+ p.set_defaults(func=_run_download)
62
+
63
+
64
+ def _run_download(args):
65
+ from ulfsynth.enhance import _download_weights
66
+ _download_weights(force=args.force)
67
+
68
+
69
+ def main():
70
+ parser = argparse.ArgumentParser(
71
+ description="ULF-Synth: Physics-Guided Ultra-Low-Field MRI Enhancement & Simulation"
72
+ )
73
+ subparsers = parser.add_subparsers(title="commands", dest="command")
74
+ subparsers.required = True
75
+
76
+ add_simulate_parser(subparsers)
77
+ add_enhance_parser(subparsers)
78
+ add_download_parser(subparsers)
79
+
80
+ args = parser.parse_args()
81
+ args.func(args)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
ulfsynth/enhance.py ADDED
@@ -0,0 +1,245 @@
1
+ """
2
+ Enhance ULF MRI volumes using pretrained restoration models.
3
+
4
+ Downloads weights from HuggingFace on first use, then runs nnU-Net
5
+ translation inference to produce enhanced (HF-like) volumes.
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ CACHE_DIR = os.path.join(os.path.expanduser("~"), ".cache", "ulfsynth")
17
+ WEIGHTS_REPO = "https://huggingface.co/toufiqmusah/ulfsynth-weights/resolve/main"
18
+ MODEL_DIR = "Dataset2023/nnUNetTrainerMRCT_kspace__nnResUNetPlans__3d_fullres"
19
+ WEIGHT_FILES = [
20
+ f"{MODEL_DIR}/dataset.json",
21
+ f"{MODEL_DIR}/dataset_fingerprint.json",
22
+ f"{MODEL_DIR}/plans.json",
23
+ f"{MODEL_DIR}/fold_all/checkpoint_best.pth",
24
+ f"{MODEL_DIR}/fold_all/checkpoint_final.pth",
25
+ ]
26
+
27
+
28
+ def _ensure_nnunet():
29
+ """Import nnunetv2 or raise a clear error with install instructions."""
30
+ try:
31
+ import nnunetv2 # noqa: F401
32
+ except ImportError:
33
+ # Look for the bundled fork relative to this package
34
+ here = Path(__file__).resolve().parent
35
+ for candidate in [here.parent / "src" / "nn-translation",
36
+ here / "_nnunet",
37
+ Path.cwd() / "src" / "nn-translation"]:
38
+ if (candidate / "setup.py").exists() or (candidate / "pyproject.toml").exists():
39
+ print(f"Installing nnunetv2 fork from {candidate}...")
40
+ subprocess.check_call(
41
+ [sys.executable, "-m", "pip", "install", "-e", str(candidate)],
42
+ )
43
+ import nnunetv2 # noqa: F401
44
+ return
45
+ raise ImportError(
46
+ "nnunetv2 is required for enhancement. Install it with:\n\n"
47
+ f" pip install -e {here.parent / 'src' / 'nn-translation'}\n\n"
48
+ "Or if you installed ulfsynth from PyPI:\n\n"
49
+ " pip install ulfsynth[enhance]\n"
50
+ " pip install nnunetv2\n"
51
+ )
52
+
53
+
54
+ def _download_weights(force=False):
55
+ """Download model weights from HuggingFace to cache directory."""
56
+ weights_dir = os.path.join(CACHE_DIR, "weights")
57
+ os.makedirs(weights_dir, exist_ok=True)
58
+
59
+ missing = []
60
+ for fname in WEIGHT_FILES:
61
+ fpath = os.path.join(weights_dir, fname)
62
+ if not os.path.isfile(fpath) or force:
63
+ missing.append(fname)
64
+
65
+ if missing:
66
+ for fname in missing:
67
+ url = f"{WEIGHTS_REPO}/{fname}"
68
+ fpath = os.path.join(weights_dir, fname)
69
+ os.makedirs(os.path.dirname(fpath), exist_ok=True)
70
+ print(f"Downloading {fname}...")
71
+ urllib.request.urlretrieve(url, fpath)
72
+ print("Weights downloaded successfully.")
73
+ else:
74
+ print("Weights already cached.")
75
+
76
+ return weights_dir
77
+
78
+
79
+ def enhance_file(input_path, output_path, device="cuda", verbose=True):
80
+ """Enhance a single ULF NIfTI file.
81
+
82
+ Args:
83
+ input_path: Path to input .nii or .nii.gz file.
84
+ output_path: Path for the enhanced output file.
85
+ device: Device for inference ('cuda', 'cpu', 'mps').
86
+ verbose: Print progress information.
87
+ """
88
+ input_path = os.path.abspath(input_path)
89
+ output_path = os.path.abspath(output_path)
90
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
91
+
92
+ with tempfile.TemporaryDirectory(prefix="ulfsynth_") as tmpdir:
93
+ # nnUNet expects files named {case}_0000.nii.gz
94
+ basename = os.path.basename(input_path)
95
+ if basename.endswith(".nii.gz"):
96
+ stem = basename[:-7]
97
+ else:
98
+ stem = basename[:-4]
99
+ nnunet_input = os.path.join(tmpdir, "input")
100
+ nnunet_output = os.path.join(tmpdir, "output")
101
+ os.makedirs(nnunet_input, exist_ok=True)
102
+ os.makedirs(nnunet_output, exist_ok=True)
103
+
104
+ # Copy input with _0000 suffix
105
+ shutil.copy2(input_path, os.path.join(nnunet_input, f"{stem}_0000.nii.gz"))
106
+
107
+ # Load model
108
+ weights_dir = _download_weights()
109
+ model_folder = os.path.join(weights_dir, MODEL_DIR)
110
+ _ensure_nnunet()
111
+ from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
112
+ import torch
113
+
114
+ predictor = nnUNetPredictor(
115
+ tile_step_size=0.5,
116
+ use_gaussian=True,
117
+ use_mirroring=True,
118
+
119
+ perform_everything_on_device=True,
120
+ device=torch.device(device),
121
+ verbose=verbose,
122
+ allow_tqdm=verbose,
123
+ verbose_preprocessing=verbose,
124
+ )
125
+ predictor.initialize_from_trained_model_folder(
126
+ model_folder,
127
+ use_folds=["all"],
128
+ checkpoint_name="checkpoint_best.pth",
129
+ )
130
+
131
+ # Run inference
132
+ if verbose:
133
+ print(f"Enhancing {basename}...")
134
+ predictor.predict_from_files(
135
+ nnunet_input,
136
+ nnunet_output,
137
+ save_probabilities=False,
138
+ overwrite=True,
139
+ num_processes_preprocessing=1,
140
+ num_processes_segmentation_export=1,
141
+ reconstruction_mode="mean",
142
+ )
143
+
144
+ # Find the NIfTI output (filter out JSON summaries added by nnUNet)
145
+ out_candidates = [f for f in os.listdir(nnunet_output)
146
+ if f.endswith('.nii.gz')]
147
+ if not out_candidates:
148
+ raise RuntimeError(
149
+ f"Enhancement produced no NIfTI output. Found: {os.listdir(nnunet_output)}"
150
+ )
151
+
152
+ out_file = os.path.join(nnunet_output, out_candidates[0])
153
+ shutil.move(out_file, output_path)
154
+
155
+ if verbose:
156
+ print(f" -> {output_path}")
157
+
158
+
159
+ def enhance_folder(input_dir, output_dir, device="cuda", verbose=True):
160
+ """Enhance all NIfTI files in a directory.
161
+
162
+ Args:
163
+ input_dir: Directory containing .nii/.nii.gz files.
164
+ output_dir: Directory for enhanced outputs.
165
+ device: Device for inference.
166
+ verbose: Print progress information.
167
+ """
168
+ input_dir = os.path.abspath(input_dir)
169
+ output_dir = os.path.abspath(output_dir)
170
+ os.makedirs(output_dir, exist_ok=True)
171
+
172
+ files = sorted(
173
+ f for f in os.listdir(input_dir)
174
+ if f.endswith(".nii") or f.endswith(".nii.gz")
175
+ )
176
+ if not files:
177
+ raise FileNotFoundError(f"No NIfTI files found in {input_dir}")
178
+
179
+ with tempfile.TemporaryDirectory(prefix="ulfsynth_") as tmpdir:
180
+ nnunet_input = os.path.join(tmpdir, "input")
181
+ nnunet_output = os.path.join(tmpdir, "output")
182
+ os.makedirs(nnunet_input, exist_ok=True)
183
+ os.makedirs(nnunet_output, exist_ok=True)
184
+
185
+ # Copy all inputs with _0000 suffix
186
+ name_map = {}
187
+ for fname in files:
188
+ if fname.endswith(".nii.gz"):
189
+ stem = fname[:-7]
190
+ else:
191
+ stem = fname[:-4]
192
+ src = os.path.join(input_dir, fname)
193
+ dst = os.path.join(nnunet_input, f"{stem}_0000.nii.gz")
194
+ shutil.copy2(src, dst)
195
+ name_map[stem] = fname
196
+
197
+ # Load model
198
+ weights_dir = _download_weights()
199
+ model_folder = os.path.join(weights_dir, MODEL_DIR)
200
+
201
+ _ensure_nnunet()
202
+ from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
203
+ import torch
204
+
205
+ predictor = nnUNetPredictor(
206
+ tile_step_size=0.5,
207
+ use_gaussian=True,
208
+ use_mirroring=True,
209
+ perform_everything_on_device=True,
210
+ device=torch.device(device),
211
+ verbose=verbose,
212
+ allow_tqdm=verbose,
213
+ verbose_preprocessing=verbose,
214
+ )
215
+ predictor.initialize_from_trained_model_folder(
216
+ model_folder,
217
+ use_folds=["all"],
218
+ checkpoint_name="checkpoint_best.pth",
219
+ )
220
+
221
+ if verbose:
222
+ print(f"Enhancing {len(files)} files...")
223
+ predictor.predict_from_files(
224
+ nnunet_input,
225
+ nnunet_output,
226
+ save_probabilities=False,
227
+ overwrite=True,
228
+ num_processes_preprocessing=1,
229
+ num_processes_segmentation_export=1,
230
+ reconstruction_mode="mean",
231
+ )
232
+
233
+ # Rename outputs back (skip JSON summaries)
234
+ for of in os.listdir(nnunet_output):
235
+ if not of.endswith(".nii.gz"):
236
+ continue
237
+ stem = of[:-7].replace("_0000", "")
238
+ original_name = name_map.get(stem, of)
239
+ shutil.move(
240
+ os.path.join(nnunet_output, of),
241
+ os.path.join(output_dir, original_name),
242
+ )
243
+
244
+ if verbose:
245
+ print(f"Done. {len(files)} files written to {output_dir}")
ulfsynth/simulate.py ADDED
@@ -0,0 +1,177 @@
1
+ """
2
+ Physics-guided ULF MRI simulation from high-field volumes.
3
+
4
+ Simulates signal loss, noise, k-space degradation, and B0
5
+ inhomogeneity effects characteristic of ultra-low-field MRI.
6
+ """
7
+
8
+ import numpy as np
9
+ import nibabel as nib
10
+ from scipy.ndimage import gaussian_filter
11
+ from numpy.fft import fftn, ifftn, fftshift, ifftshift
12
+ import os
13
+
14
+ B_HF = 1.5
15
+ B_ULF = 0.064
16
+ POLAR_SCALE = (B_ULF / B_HF) ** 2
17
+
18
+
19
+ def to_kspace(vol):
20
+ return fftshift(fftn(vol))
21
+
22
+
23
+ def to_ispace(K):
24
+ return np.real(ifftn(ifftshift(K)))
25
+
26
+
27
+ def to_complex_ispace(K):
28
+ return ifftn(ifftshift(K))
29
+
30
+
31
+ def object_mask(vol, thresh=0.05):
32
+ normed = (vol - vol.min()) / (vol.max() - vol.min() + 1e-8)
33
+ return normed > thresh
34
+
35
+
36
+ def apply_b0_t2star_decay(I_complex, T2, TE, b0_strength, smoothness, k, eps=1e-6):
37
+ H, W, D = I_complex.shape
38
+ B0_map = gaussian_filter(np.random.randn(H, W, D), sigma=smoothness) * b0_strength
39
+ gx, gy, gz = np.gradient(B0_map)
40
+ grad_B0 = np.sqrt(gx**2 + gy**2 + gz**2)
41
+ grad_B0 /= (np.percentile(grad_B0, 95) + eps)
42
+ T2star = 1.0 / ((1.0 / T2) + k * grad_B0 + eps)
43
+ decay = np.exp(-TE / T2star)
44
+ return I_complex * decay
45
+
46
+
47
+ def noisy_kspace(K, I_ref, signal_target):
48
+ alpha = 0.05
49
+ K_scaled = alpha * K
50
+ mask = object_mask(I_ref)
51
+ I_scaled = np.abs(ifftn(ifftshift(K_scaled)))
52
+ signal_power = np.mean(I_scaled[mask] ** 2)
53
+ sigma = np.sqrt(signal_power / (signal_target + 1e-8))
54
+ noise_img = sigma * np.ones_like(mask, dtype=float) * (
55
+ np.random.randn(*I_scaled.shape) + 1j * np.random.randn(*I_scaled.shape)
56
+ )
57
+ K_noisy = K_scaled + fftshift(fftn(noise_img))
58
+ return K_noisy
59
+
60
+
61
+ def kspace_crop(K, crop_ratio):
62
+ H, W, D = K.shape
63
+ ch, cw, cd = int(H * crop_ratio), int(W * crop_ratio), int(D * crop_ratio)
64
+ h0, w0, d0 = (H - ch) // 2, (W - cw) // 2, (D - cd) // 2
65
+ K_cropped = np.zeros_like(K, dtype=complex)
66
+ K_cropped[h0:h0+ch, w0:w0+cw, d0:d0+cd] = K[h0:h0+ch, w0:w0+cw, d0:d0+cd]
67
+ return K_cropped
68
+
69
+
70
+ def undersample_kspace(K, acceleration, center_fraction):
71
+ H, W, D = K.shape
72
+ mask = np.zeros((H, W, D), dtype=bool)
73
+ ch, cw, cd = int(H * center_fraction), int(W * center_fraction), int(D * center_fraction)
74
+ h0, w0, d0 = (H - ch) // 2, (W - cw) // 2, (D - cd) // 2
75
+ mask[h0:h0+ch, w0:w0+cw, d0:d0+cd] = True
76
+ outer = ~mask
77
+ n_sample = int(np.sum(outer) / acceleration)
78
+ coords = np.where(outer)
79
+ idx = np.random.choice(len(coords[0]), n_sample, replace=False)
80
+ mask[coords[0][idx], coords[1][idx], coords[2][idx]] = True
81
+ K_us = K * mask
82
+ I_original = np.abs(ifftn(ifftshift(K)))
83
+ I_us = np.abs(ifftn(ifftshift(K_us))) * object_mask(I_original, thresh=0.05)
84
+ return fftshift(fftn(I_us))
85
+
86
+
87
+ def apply_b0_inhomogeneity(K, distortion_strength, smoothness):
88
+ I_complex = ifftn(ifftshift(K))
89
+ H, W, D = I_complex.shape
90
+ field_map = gaussian_filter(np.random.randn(H, W, D), sigma=smoothness) * distortion_strength
91
+ I_distorted = I_complex * np.exp(2j * np.pi * field_map)
92
+ return fftshift(fftn(I_distorted))
93
+
94
+
95
+ def sample_params(rng=None):
96
+ if rng is None:
97
+ rng = np.random
98
+ return {
99
+ "T2": rng.uniform(0.070, 0.090),
100
+ "TE": rng.uniform(0.100, 0.130),
101
+ "b0_strength": rng.uniform(0.020, 0.040),
102
+ "smoothness": rng.uniform(30, 45),
103
+ "k": rng.uniform(10, 15),
104
+ "signal_target": rng.uniform(15, 50),
105
+ "crop_ratio": rng.uniform(0.45, 0.55),
106
+ "acceleration": int(rng.choice([2, 3], p=[0.7, 0.3])),
107
+ "center_fraction": rng.uniform(0.20, 0.30),
108
+ }
109
+
110
+
111
+ def simulate_ulf(hf_path, seed=None, params=None):
112
+ if seed is not None:
113
+ np.random.seed(seed)
114
+ if params is None:
115
+ params = sample_params()
116
+
117
+ hf_img = nib.load(hf_path)
118
+ I_hf = hf_img.get_fdata()
119
+ affine = hf_img.affine
120
+ header = hf_img.header.copy()
121
+
122
+ I_scaled = POLAR_SCALE * I_hf
123
+ K_hf = to_kspace(I_scaled)
124
+ I_complex = to_complex_ispace(K_hf)
125
+
126
+ I_b0 = apply_b0_t2star_decay(
127
+ I_complex,
128
+ T2=params["T2"],
129
+ TE=params["TE"],
130
+ b0_strength=params["b0_strength"],
131
+ smoothness=params["smoothness"],
132
+ k=params["k"],
133
+ )
134
+
135
+ K_b0 = to_kspace(I_b0)
136
+ K_noisy = noisy_kspace(K_b0, I_scaled, signal_target=params["signal_target"])
137
+
138
+ K_crop = kspace_crop(K_noisy, crop_ratio=params["crop_ratio"])
139
+
140
+ K_under = undersample_kspace(
141
+ K_crop,
142
+ acceleration=params["acceleration"],
143
+ center_fraction=params["center_fraction"],
144
+ )
145
+
146
+ K_final = apply_b0_inhomogeneity(
147
+ K_under,
148
+ distortion_strength=params["b0_strength"],
149
+ smoothness=max(1, int(params["smoothness"] // 10)),
150
+ )
151
+
152
+ I_ulf = np.abs(to_ispace(K_final)).astype(np.float32)
153
+ return I_ulf, affine, header, params
154
+
155
+
156
+ def simulate_file(hf_path, out_path, seed=None, verbose=True):
157
+ I_ulf, affine, header, params = simulate_ulf(hf_path, seed=seed)
158
+ os.makedirs(os.path.dirname(os.path.abspath(out_path)) or ".", exist_ok=True)
159
+ nib.save(nib.Nifti1Image(I_ulf, affine, header), out_path)
160
+ if verbose:
161
+ print(f" {os.path.basename(hf_path)} -> {out_path} shape={I_ulf.shape} range=[{I_ulf.min():.3f}, {I_ulf.max():.3f}]")
162
+ return params
163
+
164
+
165
+ def simulate_folder(in_dir, out_dir, seed=None, verbose=True):
166
+ os.makedirs(out_dir, exist_ok=True)
167
+ files = sorted(f for f in os.listdir(in_dir) if f.endswith(('.nii', '.nii.gz')))
168
+ if not files:
169
+ raise FileNotFoundError(f"No NIfTI files found in {in_dir}")
170
+ results = []
171
+ for fname in files:
172
+ hf_path = os.path.join(in_dir, fname)
173
+ out_path = os.path.join(out_dir, fname)
174
+ file_seed = None if seed is None else seed + abs(hash(fname)) % (2**31)
175
+ params = simulate_file(hf_path, out_path, seed=file_seed, verbose=verbose)
176
+ results.append(params)
177
+ return results
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: ulfsynth
3
+ Version: 0.1.0
4
+ Summary: Physics-Guided Ultra-Low-Field MRI Enhancement & Simulation
5
+ Author: Toufiq Musah
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Toufiq Musah
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: homepage, https://github.com/toufiqmusah/ULF-Synth
29
+ Project-URL: repository, https://github.com/toufiqmusah/ULF-Synth
30
+ Project-URL: documentation, https://github.com/toufiqmusah/ULF-Synth#readme
31
+ Project-URL: Bug Tracker, https://github.com/toufiqmusah/ULF-Synth/issues
32
+ Keywords: mri,ultra-low-field,ulf,medical-image-enhancement,image-synthesis,deep-learning,neuroimaging,nnunet,image-translation
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: Intended Audience :: Healthcare Industry
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
43
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
44
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
45
+ Classifier: Operating System :: OS Independent
46
+ Requires-Python: >=3.10
47
+ Description-Content-Type: text/markdown
48
+ License-File: LICENSE
49
+ Requires-Dist: numpy>=1.21.0
50
+ Requires-Dist: nibabel>=3.2.0
51
+ Requires-Dist: scipy>=1.7.0
52
+ Requires-Dist: torch>=2.1.0
53
+ Provides-Extra: enhance
54
+ Requires-Dist: nnunetv2>=2.5; extra == "enhance"
55
+ Dynamic: license-file
56
+ Dynamic: provides-extra
57
+ Dynamic: requires-dist
58
+ Dynamic: summary
59
+
60
+ <p align="center">
61
+ <img src="assets/method.png" alt="ULF-Synth" width="100%">
62
+ </p>
63
+
64
+ <h1 align="center">ULF-Synth: Physics-Guided Ultra-Low-Field MRI Enhancement for Pediatric Neuroimaging</h1>
65
+
66
+ <p align="center">
67
+ <a href="https://arxiv.org/abs/2605.24625v1"><img src="https://img.shields.io/badge/arXiv-2605.24625-b31b1b.svg" alt="arXiv"></a>
68
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
69
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python"></a>
70
+ </p>
71
+
72
+ ## Abstract
73
+
74
+ Ultra-low-field (ULF) MRI enables portable and accessible neuroimaging, but suffers from low signal-to-noise ratio and limited spatial resolution relative to high-field (HF) systems. Acquiring paired ULF–HF data for supervised enhancement is often infeasible, particularly in resource-limited settings. We introduce **ULF-Synth**, a framework combining: (i) acquisition-based synthesis of realistic ULF images from HF volumes for large-scale paired training, and (ii) a spatial-frequency domain objective that prioritizes recovery of high-frequency anatomical detail. The formulation is architecture-agnostic, consistently improving structural similarity and perceptual fidelity across encoder-decoder, adversarial, and diffusion-based translation models. Trained exclusively on synthetic data, our models generalize to real 64 mT ULF acquisitions, improving multiclass brain segmentation and achieving higher radiologist preference and diagnostic acceptability in a blinded reader study.
75
+
76
+ ---
77
+
78
+ ## Simulation Pipeline
79
+
80
+ The ULF synthesis module models the key physical phenomena distinguishing ULF from HF acquisitions:
81
+
82
+ | | Effect | Implementation |
83
+ |:---:|---|---|
84
+ | 1 | **Signal scaling** | $(B_{{ULF}}/B_{{HF}})^2$ polarization ratio |
85
+ | 2 | **T2\* decay & B0 inhomogeneity** | Spatially-varying exponential decay from random B0 field maps |
86
+ | 3 | **Thermal noise** | Gaussian noise scaled to SNR 15–50 |
87
+ | 4 | **k-space cropping** | Reduced resolution (45–55%) |
88
+ | 5 | **k-space undersampling** | Accelerated acquisition (2×–3×) with center-out sampling |
89
+ | 6 | **B0 off-resonance distortion** | Phase distortion from random B0 field maps |
90
+
91
+ ---
92
+
93
+ ## Qualitative Results
94
+
95
+ <p align="center">
96
+ <img src="assets/results.png" alt="Sample results" width="95%">
97
+ </p>
98
+
99
+ ---
100
+
101
+ ## Installation
102
+
103
+ ```bash
104
+ git clone https://github.com/toufiqmusah/ULF-Synth.git
105
+ cd ULF-Synth
106
+ pip install -e .
107
+ ```
108
+
109
+ This installs `ulfsynth` and all core dependencies (including PyTorch).
110
+ The bundled [nnUNet translation fork](src/nn-translation/) is automatically
111
+ discovered and installed during setup — no extra steps needed.
112
+
113
+ ### Install from PyPI (future)
114
+
115
+ ```bash
116
+ pip install ulfsynth # simulation only
117
+ pip install ulfsynth[full] # with enhancement support
118
+ ```
119
+
120
+ ---
121
+
122
+ ## CLI
123
+
124
+ The `ulfsynth` package provides three commands:
125
+
126
+ ### `ulfsynth simulate` — ULF synthesis from HF volumes
127
+
128
+ ```bash
129
+ # Single volume
130
+ ulfsynth simulate input.nii.gz output.nii.gz
131
+
132
+ # Folder of NIfTI files
133
+ ulfsynth simulate /path/to/hf/scans/ /path/to/ulf/scans/
134
+
135
+ # Reproducible seed
136
+ ulfsynth simulate input.nii.gz output.nii.gz --seed 42
137
+ ```
138
+
139
+ ### `ulfsynth enhance` — ULF→HF restoration (requires nnUNet)
140
+
141
+ ```bash
142
+ # Single volume (CPU)
143
+ ulfsynth enhance --device cpu input.nii.gz output.nii.gz
144
+
145
+ # Folder of NIfTI files (GPU)
146
+ ulfsynth enhance /path/to/ulf/scans/ /path/to/enhanced/scans/
147
+
148
+ # Weights are downloaded from HuggingFace on first use.
149
+ # Pre-download: ulfsynth download-weights
150
+ ```
151
+
152
+ ### `ulfsynth download-weights` — cache pretrained weights
153
+
154
+ ```bash
155
+ ulfsynth download-weights
156
+ ```
157
+
158
+ Caches model weights from [HuggingFace](https://huggingface.co/toufiqmusah/ulfsynth-weights) to `~/.cache/ulfsynth/`.
159
+
160
+ ---
161
+
162
+ ## Python API
163
+
164
+ ### Simulation
165
+
166
+ ```python
167
+ from ulfsynth.simulate import simulate_ulf, simulate_file, simulate_folder, sample_params
168
+
169
+ # Generate one ULF volume with random parameters
170
+ ulf_volume, affine, header, params = simulate_ulf("hf_input.nii.gz")
171
+
172
+ # With a fixed seed
173
+ ulf_volume, affine, header, params = simulate_ulf("hf_input.nii.gz", seed=42)
174
+
175
+ # Custom parameters
176
+ params = sample_params()
177
+ params["signal_target"] = 30
178
+ ulf_volume, affine, header, params = simulate_ulf("hf_input.nii.gz", params=params)
179
+
180
+ # Single-file convenience (returns params dict)
181
+ params = simulate_file("hf_input.nii.gz", "ulf_output.nii.gz", seed=42)
182
+
183
+ # Batch folder processing (returns list of params)
184
+ results = simulate_folder("hf_scans/", "ulf_scans/", seed=42)
185
+ ```
186
+
187
+ Output preserves the input affine and header metadata.
188
+
189
+ ### Enhancement
190
+
191
+ ```python
192
+ from ulfsynth.enhance import enhance_file, enhance_folder
193
+
194
+ # Single file
195
+ enhance_file("ulf_input.nii.gz", "enhanced_output.nii.gz", device="cpu")
196
+
197
+ # Batch folder processing
198
+ enhance_folder("ulf_scans/", "enhanced_scans/", device="cuda")
199
+ ```
200
+
201
+ Requires nnUNet (`pip install -e src/nn-translation/`). Weights are auto-downloaded on first call.
202
+
203
+ ---
204
+
205
+ ## Roadmap
206
+
207
+ - [x] Physics-guided ULF synthesis pipeline
208
+ - [x] Pre-trained enhancement weights — ULF→HF restoration models
209
+ - [x] Python package — `pip install ulfsynth`
210
+ - [ ] Docker image — zero-config containerized pipeline
211
+
212
+ ---
213
+
214
+ ## Citation
215
+
216
+ ```bibtex
217
+ @misc{musah2026ulfsynth,
218
+ title = {ULF-Synth: Physics-Guided Ultra-Low-Field MRI Enhancement for Pediatric Neuroimaging},
219
+ author = {Toufiq Musah and Salvatore Calcagno and Federica Proietto Salanitri and Xiaomeng Li and Maruf Adewole and Marawan Elbatel},
220
+ year = {2026},
221
+ eprint = {2605.24625},
222
+ archivePrefix = {arXiv},
223
+ url = {https://arxiv.org/abs/2605.24625}
224
+ }
225
+ ```
226
+
227
+ ---
228
+
229
+ ## License
230
+
231
+ [MIT](LICENSE)
@@ -0,0 +1,10 @@
1
+ ulfsynth/__init__.py,sha256=7TizWjaILVaKCkQLQohnE9eGFreZPBv4sDzX_o6A3pw,339
2
+ ulfsynth/cli.py,sha256=TdJWiCegEJmvkYham5X3SUOpsoiPE2ucrebfUE_4_Eo,3044
3
+ ulfsynth/enhance.py,sha256=jmagU42ZCYwH3EjmQt1lkhGILNKrZupEyaaY8vBIPkw,8553
4
+ ulfsynth/simulate.py,sha256=E883-8CglBY5Spk-9sUHweEWemV4IfpQ02m9yIEg7GQ,5900
5
+ ulfsynth-0.1.0.dist-info/licenses/LICENSE,sha256=CUoI62adLPW53edyqgN_E66KMIAORk5QWw4IoGTKvao,1069
6
+ ulfsynth-0.1.0.dist-info/METADATA,sha256=AYL--4afR33iFA0HUiUumx6nH12DesB6z1ycXSFHaSE,8545
7
+ ulfsynth-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ ulfsynth-0.1.0.dist-info/entry_points.txt,sha256=24J4SOhUWyQMBq6Ru42FPmxK8QgWiaI_kXL2MadP_Qo,47
9
+ ulfsynth-0.1.0.dist-info/top_level.txt,sha256=QWzvK5IuFg-8wm6GZVgLl4gIyLRNkaqxoo3ON45Smr0,9
10
+ ulfsynth-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ulfsynth = ulfsynth.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Toufiq Musah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ulfsynth