Skip to content

Commit dab0cde

Browse files
committed
Add SwiFT Demo V2 (swift_demo_v2)
1 parent 2bfdbd9 commit dab0cde

17 files changed

Lines changed: 2824 additions & 0 deletions
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
generate_cube_viz.py
3+
4+
Animated GIF of 5 representative axial brain slices side-by-side,
5+
cycling through every time point — same style as adni_subject_A/B.gif.
6+
7+
For each GARD subject:
8+
1. Globally normalise the 4-D fMRI volume.
9+
2. Auto-select 5 axial z-positions that best capture the brain.
10+
3. For each time point t, render the 5 slices as a horizontal filmstrip.
11+
4. Save as an animated GIF (80 ms / frame ≈ 12.5 fps).
12+
"""
13+
14+
import os
15+
import numpy as np
16+
import nibabel as nib
17+
from PIL import Image
18+
19+
# ── Config ────────────────────────────────────────────────────────────────────
20+
21+
OUTPUT_DIR = "/pscratch/sd/s/seungju/SwiFT_v2_perlmutter/demo/output"
22+
os.makedirs(OUTPUT_DIR, exist_ok=True)
23+
24+
SUBJECTS = {
25+
"gard_sub-24_hc": (
26+
"/pscratch/sd/s/sjmoon/GARD/derivatives/sub-24/func/"
27+
"sub-24_task-rest_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz"
28+
),
29+
"gard_sub-324_mci": (
30+
"/pscratch/sd/s/sjmoon/GARD/derivatives/sub-324/func/"
31+
"sub-324_task-rest_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz"
32+
),
33+
}
34+
35+
N_SLICES = 5 # number of axial slices
36+
SLICE_PX = 160 # pixel width/height of each slice panel
37+
GAP_PX = 6 # gap between panels
38+
FRAME_MS = 80 # ms per frame (≈12.5 fps)
39+
CLIP_PCT = 99.5 # percentile for intensity clipping
40+
41+
42+
# ── Data loading ──────────────────────────────────────────────────────────────
43+
44+
def load_and_normalize(path: str) -> np.ndarray:
45+
"""Load 4-D fMRI, globally normalise to [0, 1]."""
46+
print(f" Loading {path} ...")
47+
data = np.asarray(nib.load(path).dataobj, dtype=np.float32)
48+
print(f" Shape: {data.shape}")
49+
nonzero = data[data > 0]
50+
if nonzero.size:
51+
p = np.percentile(nonzero, CLIP_PCT)
52+
data = np.clip(data, 0.0, p) / (p + 1e-8)
53+
return data
54+
55+
56+
# ── Z-slice selection ─────────────────────────────────────────────────────────
57+
58+
def pick_z_slices(data_4d: np.ndarray, n: int = N_SLICES) -> list:
59+
"""
60+
Auto-select n axial z-indices that best show the brain.
61+
Strategy:
62+
- Average over time → mean volume
63+
- Count non-zero voxels per z to find brain extent
64+
- Trim 10% margin from each end (noisy edges)
65+
- Evenly space n slices within the trimmed range
66+
"""
67+
mean_vol = data_4d.mean(axis=3) # (nx, ny, nz)
68+
brain_at_z = (mean_vol > 0.05).sum(axis=(0, 1)) # (nz,)
69+
threshold = brain_at_z.max() * 0.10
70+
z_valid = np.where(brain_at_z > threshold)[0]
71+
72+
if len(z_valid) < n:
73+
nz = data_4d.shape[2]
74+
z_valid = np.arange(nz // 4, 3 * nz // 4)
75+
76+
margin = max(1, len(z_valid) // 10)
77+
z_range = z_valid[margin: len(z_valid) - margin]
78+
79+
indices = [
80+
int(z_range[round(i * (len(z_range) - 1) / (n - 1))])
81+
for i in range(n)
82+
]
83+
return indices
84+
85+
86+
# ── Frame generation ──────────────────────────────────────────────────────────
87+
88+
def make_frame(data_4d: np.ndarray, z_indices: list, t: int) -> Image.Image:
89+
"""
90+
Build one GIF frame: n axial slices at time t arranged side-by-side.
91+
Returns a grayscale-converted RGB PIL image.
92+
"""
93+
n = len(z_indices)
94+
width = n * SLICE_PX + (n - 1) * GAP_PX
95+
canvas = np.zeros((SLICE_PX, width), dtype=np.uint8)
96+
97+
for i, z in enumerate(z_indices):
98+
sl = data_4d[:, :, z, t] # (nx, ny) axial slice
99+
sl = np.rot90(sl) # standard orientation
100+
101+
# resize to square panel
102+
pil_sl = Image.fromarray((sl * 255).astype(np.uint8), mode="L")
103+
pil_sl = pil_sl.resize((SLICE_PX, SLICE_PX), Image.LANCZOS)
104+
105+
x0 = i * (SLICE_PX + GAP_PX)
106+
canvas[:, x0 : x0 + SLICE_PX] = np.array(pil_sl)
107+
108+
return Image.fromarray(canvas, mode="L").convert("RGB")
109+
110+
111+
# ── GIF writer ────────────────────────────────────────────────────────────────
112+
113+
def generate_gif(path: str, output_path: str) -> None:
114+
data = load_and_normalize(path)
115+
nt = data.shape[3]
116+
117+
z_indices = pick_z_slices(data)
118+
print(f" Selected z-slices: {z_indices}")
119+
120+
frames = []
121+
for t in range(nt):
122+
if t % 20 == 0:
123+
print(f" Rendering frame {t + 1}/{nt} ...")
124+
frames.append(make_frame(data, z_indices, t))
125+
126+
frames[0].save(
127+
output_path,
128+
save_all = True,
129+
append_images = frames[1:],
130+
duration = FRAME_MS,
131+
loop = 0,
132+
optimize = False,
133+
)
134+
size_mb = os.path.getsize(output_path) / 1024 ** 2
135+
print(f" Saved: {output_path} ({size_mb:.1f} MB, {nt} frames)")
136+
137+
138+
# ── Entry point ───────────────────────────────────────────────────────────────
139+
140+
if __name__ == "__main__":
141+
for name, path in SUBJECTS.items():
142+
print(f"\nProcessing {name} ...")
143+
generate_gif(path, os.path.join(OUTPUT_DIR, f"{name}.gif"))
144+
print("\nDone.")

0 commit comments

Comments
 (0)