@yibeichan/claude-skills 1.0.2

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/cli.js +272 -0
  4. package/install.py +240 -0
  5. package/package.json +44 -0
  6. package/skills/bidsapp-nidm-standards/SKILL.md +202 -0
  7. package/skills/bidsapp-nidm-standards/references/babs_config.md +20 -0
  8. package/skills/bidsapp-nidm-standards/references/cli_arguments.md +76 -0
  9. package/skills/bidsapp-nidm-standards/references/container_patterns.md +53 -0
  10. package/skills/bidsapp-nidm-standards/references/nidm_integration.md +403 -0
  11. package/skills/bidsapp-nidm-standards/references/repo_structure.md +121 -0
  12. package/skills/bidsapp-nidm-standards/references/testing_patterns.md +82 -0
  13. package/skills/dicom2fmriprep/SKILL.md +377 -0
  14. package/skills/dicom2fmriprep/evals/evals.json +26 -0
  15. package/skills/dicom2fmriprep/references/babs-details.md +407 -0
  16. package/skills/dicom2fmriprep/references/fmriprep-details.md +250 -0
  17. package/skills/dicom2fmriprep/references/heudiconv-details.md +243 -0
  18. package/skills/fmri-ssm/SKILL.md +317 -0
  19. package/skills/fmri-ssm/references/code_templates.md +1570 -0
  20. package/skills/fmri-ssm/references/downstream_analysis.md +680 -0
  21. package/skills/fmri-ssm/references/group_inference.md +608 -0
  22. package/skills/fmri-ssm/references/hrf_modeling.md +447 -0
  23. package/skills/fmri-ssm/references/model_catalog.md +436 -0
  24. package/skills/fmri-ssm/references/paradigm_guide.md +406 -0
  25. package/skills/fmri-ssm/references/preprocessing.md +614 -0
  26. package/skills/fmri-ssm.zip +0 -0
  27. package/skills/neuroimaging-qc/SKILL.md +203 -0
  28. package/skills/neuroimaging-qc/references/eeg_qc.md +400 -0
  29. package/skills/neuroimaging-qc/references/fmri_qc.md +343 -0
  30. package/skills/neuroimaging-qc/references/fnirs_qc.md +430 -0
  31. package/skills/neuroimaging-qc/references/structural_qc.md +454 -0
  32. package/skills/neuroimaging-qc/scripts/parse_fmriprep_confounds.py +153 -0
  33. package/skills/neuroimaging-qc/scripts/parse_mriqc.py +114 -0
  34. package/skills/neuroimaging-qc/scripts/qc_report.py +295 -0
  35. package/skills/scientific-writer/SKILL.md +202 -0
  36. package/skills/scientific-writer/references/citation_styles.md +163 -0
  37. package/skills/scientific-writer/references/field_conventions.md +245 -0
  38. package/skills/scientific-writer/references/figures_tables.md +225 -0
  39. package/skills/scientific-writer/references/reporting_guidelines.md +225 -0
  40. package/skills.json +54 -0
@@ -0,0 +1,454 @@
1
+ # Structural MRI QC Reference
2
+
3
+ Comprehensive guide for QC of structural MRI data (T1w, T2w) from MRIQC, FreeSurfer, and related pipelines.
4
+
5
+ ## Table of Contents
6
+ 1. [MRIQC Anatomical IQMs](#mriqc-anatomical-iqms)
7
+ 2. [FreeSurfer QC](#freesurfer-qc)
8
+ 3. [Registration QC](#registration-qc)
9
+ 4. [Segmentation QC](#segmentation-qc)
10
+ 5. [Population Considerations](#population-considerations)
11
+ 6. [Python Examples](#python-examples)
12
+
13
+ ## MRIQC Anatomical IQMs
14
+
15
+ ### Complete T1w/T2w IQM List
16
+
17
+ | Metric | Description | Direction | Typical Range |
18
+ |--------|-------------|-----------|---------------|
19
+ | **Noise Metrics** ||||
20
+ | `snr_csf` | SNR in CSF | Higher better | 5-15 |
21
+ | `snr_gm` | SNR in gray matter | Higher better | 8-20 |
22
+ | `snr_wm` | SNR in white matter | Higher better | 10-25 |
23
+ | `snr_total` | Total SNR | Higher better | Varies |
24
+ | `cnr` | Contrast-to-noise ratio (GM/WM) | Higher better | >2.5 |
25
+ | **Information Metrics** ||||
26
+ | `cjv` | Coefficient of joint variation | Lower better | <0.5 |
27
+ | `efc` | Entropy focus criterion | Lower better | <0.6 |
28
+ | `fber` | Foreground-background energy ratio | Higher better | >100 |
29
+ | **Artifact Metrics** ||||
30
+ | `qi_1` | Artifact detection (Mortamet) | Lower better | <0.05 |
31
+ | `qi_2` | Artifact detection (extended) | Lower better | <0.05 |
32
+ | `inu_med` | INU bias median | ~1.0 | 0.9-1.1 |
33
+ | `inu_range` | INU bias range | Lower better | <0.3 |
34
+ | `wm2max` | WM to max intensity ratio | ~0.5-0.8 | Site-dependent |
35
+ | **Resolution/Size** ||||
36
+ | `fwhm_avg` | Average smoothness (mm) | Context | 2-4mm |
37
+ | `fwhm_x/y/z` | Directional smoothness | Context | |
38
+ | `size_x/y/z` | Matrix dimensions | Context | |
39
+ | `spacing_x/y/z` | Voxel dimensions | Context | |
40
+ | **Tissue Stats** ||||
41
+ | `summary_*_mean` | Mean intensity per tissue | Context | |
42
+ | `summary_*_stdv` | Stdv intensity per tissue | Context | |
43
+ | `summary_*_p05/p95` | 5th/95th percentiles | Context | |
44
+ | `tpm_overlap_*` | TPM overlap per tissue | Higher better | >0.7 |
45
+ | `icvs_*` | Intracranial volume fractions | Context | |
46
+
47
+ ### Key Metrics for Exclusion Decisions
48
+
49
+ **Primary (always check):**
50
+ 1. `qi_1` — artifact detection
51
+ 2. `cnr` — tissue contrast quality
52
+ 3. `snr_gm` / `snr_wm` — signal quality
53
+ 4. `cjv` — GM/WM separability
54
+
55
+ **Secondary:**
56
+ 5. `efc` — ghosting/ringing
57
+ 6. `inu_range` — bias field severity
58
+ 7. `fwhm_avg` — effective resolution
59
+
60
+ ### Threshold Recommendations
61
+
62
+ **Note**: Absolute thresholds don't generalize well across sites/scanners. Use distribution-based outlier detection.
63
+
64
+ **General guidelines:**
65
+ - `qi_1` > 0.1: Potential artifacts
66
+ - `cnr` < 2.5: Poor tissue contrast
67
+ - `cjv` > 0.5: Poor GM/WM separation
68
+ - `efc` > 0.6: Possible ghosting
69
+
70
+ **Recommended approach:**
71
+ ```python
72
+ # Flag outliers beyond 2-3 SD from sample mean
73
+ for metric in ['qi_1', 'cnr', 'cjv', 'efc']:
74
+ z_score = (value - sample_mean) / sample_std
75
+ if abs(z_score) > 3:
76
+ flag_for_review(subject)
77
+ ```
78
+
79
+ ## FreeSurfer QC
80
+
81
+ ### Automated QC Metrics
82
+
83
+ **Euler number:**
84
+ - Topological measure of surface quality
85
+ - More negative = more holes/handles (worse)
86
+ - Threshold: Euler < -200 flagged for review (Rosen et al., 2018)
87
+
88
+ **Surface holes:**
89
+ - Count of topological defects
90
+ - Threshold: >100 holes concerning
91
+
92
+ **Estimated total intracranial volume (eTIV):**
93
+ - Should be within expected range for population
94
+ - Flag outliers (>2 SD from mean)
95
+
96
+ ### Visual QC Checkpoints
97
+
98
+ **1. Skull stripping:**
99
+ ```
100
+ □ Brain fully included
101
+ □ No skull/dura remaining
102
+ □ No brain tissue removed
103
+ ```
104
+
105
+ **2. White matter segmentation:**
106
+ ```
107
+ □ WM follows gyral pattern
108
+ □ No WM in ventricles
109
+ □ No holes in WM
110
+ ```
111
+
112
+ **3. Pial surface:**
113
+ ```
114
+ □ Follows gray matter boundary
115
+ □ No extension into skull/dura
116
+ □ No cuts into brain tissue
117
+ ```
118
+
119
+ **4. Subcortical segmentation:**
120
+ ```
121
+ □ Symmetric structures similar size
122
+ □ No obvious mislabeling
123
+ □ Hippocampus properly delineated
124
+ ```
125
+
126
+ ### Common FreeSurfer Issues
127
+
128
+ | Issue | Visual Sign | Potential Cause |
129
+ |-------|-------------|-----------------|
130
+ | Skull stripping failure | Brain cut off or skull included | Low contrast, unusual anatomy |
131
+ | WM segmentation error | WM extends into GM | Motion blur, bias field |
132
+ | Pial surface error | Surface extends into skull | Dura enhancement, blood vessels |
133
+ | Subcortical mislabel | Asymmetric volumes | Poor contrast, pathology |
134
+
135
+ ### FreeSurfer QC Tools
136
+
137
+ ```bash
138
+ # View segmentation overlaid on T1
139
+ freeview -v $SUBJECTS_DIR/$subj/mri/T1.mgz \
140
+ -v $SUBJECTS_DIR/$subj/mri/aseg.mgz:colormap=lut
141
+
142
+ # View surfaces
143
+ freeview -v $SUBJECTS_DIR/$subj/mri/T1.mgz \
144
+ -f $SUBJECTS_DIR/$subj/surf/lh.white:edgecolor=yellow \
145
+ -f $SUBJECTS_DIR/$subj/surf/lh.pial:edgecolor=red
146
+ ```
147
+
148
+ ## Registration QC
149
+
150
+ ### Checking Normalization Quality
151
+
152
+ **Metrics:**
153
+ - Cost function value (mutual information, correlation ratio)
154
+ - Overlap with template (Dice, Jaccard)
155
+ - Landmark alignment
156
+
157
+ **Visual checks:**
158
+ ```
159
+ □ Major structures aligned (ventricles, sulci)
160
+ □ No gross misalignment
161
+ □ Edges match template
162
+ □ Subcortical structures properly positioned
163
+ ```
164
+
165
+ ### Common Registration Failures
166
+
167
+ 1. **Local minima**: Registration stuck in wrong position
168
+ 2. **Scaling errors**: Brain too large/small
169
+ 3. **Rotation errors**: Tilted relative to template
170
+ 4. **Shearing**: Non-rigid distortion
171
+
172
+ ### Template Considerations
173
+
174
+ **Adults:**
175
+ - MNI152 (most common)
176
+ - Colin27
177
+ - fsaverage (surface)
178
+
179
+ **Pediatric:**
180
+ - Pediatric templates (e.g., NIH Pediatric MRI Database)
181
+ - Age-specific templates
182
+
183
+ **Infants:**
184
+ - UNC infant templates
185
+ - MCRIB
186
+ - dHCP templates (Developing Human Connectome Project)
187
+
188
+ ## Segmentation QC
189
+
190
+ ### Tissue Segmentation Checks
191
+
192
+ **CSF:**
193
+ - Present in ventricles and sulci
194
+ - Not extending into parenchyma
195
+
196
+ **Gray matter:**
197
+ - Follows cortical ribbon
198
+ - Proper thickness (~2-4mm in adults)
199
+
200
+ **White matter:**
201
+ - Connected through centrum semiovale
202
+ - Follows expected pattern
203
+
204
+ ### Volume Plausibility
205
+
206
+ **Adult brain volumes (approximate):**
207
+ | Structure | Typical Volume | Flag if... |
208
+ |-----------|---------------|------------|
209
+ | Total brain | 1200-1500 cm³ | <1000 or >1700 |
210
+ | Cerebral WM | 400-600 cm³ | Outlier |
211
+ | Cortical GM | 550-750 cm³ | Outlier |
212
+ | Hippocampus (each) | 3-4 cm³ | <2 or >5 |
213
+ | Lateral ventricle (each) | 7-20 cm³ | >40 (unless elderly) |
214
+
215
+ **Note**: Volumes vary with age, sex, and pathology.
216
+
217
+ ## Population Considerations
218
+
219
+ ### Infants
220
+
221
+ **Challenges:**
222
+ - Inverted T1 contrast (myelination incomplete)
223
+ - Rapidly changing brain size
224
+ - Different templates needed
225
+
226
+ **QC considerations:**
227
+ - Use age-appropriate templates
228
+ - Expect different metric distributions
229
+ - Visual QC more critical
230
+
231
+ ### Elderly
232
+
233
+ **Considerations:**
234
+ - Atrophy affects volumes and segmentation
235
+ - WM hyperintensities affect contrast
236
+ - Enlarged ventricles normal
237
+
238
+ **Adjusted criteria:**
239
+ - Larger acceptable ventricle volumes
240
+ - Lower GM volumes expected
241
+ - Check for WMH handling
242
+
243
+ ### Clinical Populations
244
+
245
+ **Considerations:**
246
+ - Lesions may affect segmentation
247
+ - Atrophy patterns disease-specific
248
+ - May need lesion masking
249
+
250
+ ## Python Examples
251
+
252
+ ### Parse MRIQC Anatomical Output
253
+
254
+ ```python
255
+ import pandas as pd
256
+ import numpy as np
257
+
258
+ def load_mriqc_anat(group_tsv_path):
259
+ """Load MRIQC T1w group TSV."""
260
+ df = pd.read_csv(group_tsv_path, sep='\t')
261
+ return df
262
+
263
+
264
+ def flag_anat_outliers(df, zscore_threshold=3):
265
+ """
266
+ Flag subjects with outlier anatomical IQMs.
267
+ """
268
+ metrics_to_check = ['qi_1', 'cnr', 'cjv', 'efc', 'snr_gm', 'snr_wm']
269
+
270
+ # Direction: higher is worse for these
271
+ higher_worse = ['qi_1', 'cjv', 'efc']
272
+ # Direction: lower is worse for these
273
+ lower_worse = ['cnr', 'snr_gm', 'snr_wm']
274
+
275
+ flags = pd.DataFrame(index=df.index)
276
+
277
+ for metric in metrics_to_check:
278
+ if metric not in df.columns:
279
+ continue
280
+
281
+ z = (df[metric] - df[metric].mean()) / df[metric].std()
282
+
283
+ if metric in higher_worse:
284
+ flags[f'outlier_{metric}'] = z > zscore_threshold
285
+ else: # lower_worse
286
+ flags[f'outlier_{metric}'] = z < -zscore_threshold
287
+
288
+ flags['any_outlier'] = flags.any(axis=1)
289
+
290
+ return flags
291
+
292
+
293
+ def anat_qc_report(df, flags):
294
+ """Generate anatomical QC summary."""
295
+ n_total = len(df)
296
+ n_outliers = flags['any_outlier'].sum()
297
+
298
+ report = f"""
299
+ Anatomical MRI QC Summary
300
+ =========================
301
+
302
+ Total subjects: {n_total}
303
+ Outliers detected: {n_outliers} ({100*n_outliers/n_total:.1f}%)
304
+
305
+ Metric distributions:
306
+ """
307
+
308
+ for col in ['qi_1', 'cnr', 'cjv', 'snr_gm']:
309
+ if col in df.columns:
310
+ report += f"\n{col}:\n"
311
+ report += f" Mean: {df[col].mean():.3f}\n"
312
+ report += f" Std: {df[col].std():.3f}\n"
313
+ report += f" Range: [{df[col].min():.3f}, {df[col].max():.3f}]\n"
314
+
315
+ return report
316
+ ```
317
+
318
+ ### FreeSurfer QC Extraction
319
+
320
+ ```python
321
+ import os
322
+ import subprocess
323
+
324
+ def get_euler_number(subjects_dir, subject):
325
+ """Extract Euler number from FreeSurfer output."""
326
+ euler_lh = None
327
+ euler_rh = None
328
+
329
+ # Read from log file or compute
330
+ log_file = os.path.join(subjects_dir, subject, 'scripts', 'recon-all.log')
331
+
332
+ if os.path.exists(log_file):
333
+ with open(log_file, 'r') as f:
334
+ for line in f:
335
+ if 'euler' in line.lower():
336
+ # Parse euler number from log
337
+ pass
338
+
339
+ # Alternative: compute directly
340
+ for hemi in ['lh', 'rh']:
341
+ surf_path = os.path.join(subjects_dir, subject, 'surf', f'{hemi}.orig')
342
+ if os.path.exists(surf_path):
343
+ result = subprocess.run(
344
+ ['mris_euler_number', surf_path],
345
+ capture_output=True, text=True
346
+ )
347
+ # Parse output
348
+
349
+ return euler_lh, euler_rh
350
+
351
+
352
+ def get_fs_volumes(subjects_dir, subject):
353
+ """Extract key volumes from FreeSurfer stats."""
354
+ stats_file = os.path.join(subjects_dir, subject, 'stats', 'aseg.stats')
355
+
356
+ volumes = {}
357
+
358
+ if os.path.exists(stats_file):
359
+ with open(stats_file, 'r') as f:
360
+ for line in f:
361
+ if line.startswith('#'):
362
+ continue
363
+ parts = line.strip().split()
364
+ if len(parts) >= 4:
365
+ struct_name = parts[4] if len(parts) > 4 else parts[0]
366
+ volume = float(parts[3])
367
+ volumes[struct_name] = volume
368
+
369
+ return volumes
370
+
371
+
372
+ def check_fs_qc(subjects_dir, subject):
373
+ """Comprehensive FreeSurfer QC check."""
374
+ qc_results = {
375
+ 'subject': subject,
376
+ 'passed': True,
377
+ 'issues': []
378
+ }
379
+
380
+ # Check if recon-all completed
381
+ done_file = os.path.join(subjects_dir, subject, 'scripts', 'recon-all.done')
382
+ if not os.path.exists(done_file):
383
+ qc_results['passed'] = False
384
+ qc_results['issues'].append('recon-all did not complete')
385
+ return qc_results
386
+
387
+ # Check Euler numbers
388
+ euler_lh, euler_rh = get_euler_number(subjects_dir, subject)
389
+ if euler_lh is not None and euler_lh < -200:
390
+ qc_results['issues'].append(f'Low Euler LH: {euler_lh}')
391
+ if euler_rh is not None and euler_rh < -200:
392
+ qc_results['issues'].append(f'Low Euler RH: {euler_rh}')
393
+
394
+ # Check volumes
395
+ volumes = get_fs_volumes(subjects_dir, subject)
396
+
397
+ # Example: check for extreme hippocampal volumes
398
+ for hemi in ['Left', 'Right']:
399
+ hipp_key = f'{hemi}-Hippocampus'
400
+ if hipp_key in volumes:
401
+ vol = volumes[hipp_key]
402
+ if vol < 2000 or vol > 5500: # in mm³
403
+ qc_results['issues'].append(f'Unusual {hemi} hippocampus: {vol:.0f} mm³')
404
+
405
+ if qc_results['issues']:
406
+ qc_results['passed'] = False
407
+
408
+ return qc_results
409
+ ```
410
+
411
+ ### Visual QC Screenshot Generation
412
+
413
+ ```python
414
+ def generate_qc_screenshots(subjects_dir, subject, output_dir):
415
+ """Generate QC screenshots using FreeSurfer tools."""
416
+ import subprocess
417
+
418
+ os.makedirs(output_dir, exist_ok=True)
419
+
420
+ # Axial slices with segmentation overlay
421
+ for slice_num in [80, 100, 120, 140]:
422
+ output_file = os.path.join(output_dir, f'{subject}_axial_{slice_num}.png')
423
+ cmd = [
424
+ 'freeview',
425
+ '-v', f'{subjects_dir}/{subject}/mri/T1.mgz',
426
+ '-v', f'{subjects_dir}/{subject}/mri/aseg.mgz:colormap=lut:opacity=0.3',
427
+ '-slice', '0', '0', str(slice_num),
428
+ '-viewport', 'axial',
429
+ '-ss', output_file,
430
+ '-quit'
431
+ ]
432
+ subprocess.run(cmd)
433
+
434
+ # Surface views
435
+ for hemi in ['lh', 'rh']:
436
+ for view in ['lateral', 'medial']:
437
+ output_file = os.path.join(output_dir, f'{subject}_{hemi}_{view}.png')
438
+ cmd = [
439
+ 'freeview',
440
+ '-f', f'{subjects_dir}/{subject}/surf/{hemi}.pial:overlay={subjects_dir}/{subject}/surf/{hemi}.thickness',
441
+ '-viewport', '3d',
442
+ '-cam', 'azimuth', '0' if view == 'lateral' else '180',
443
+ '-ss', output_file,
444
+ '-quit'
445
+ ]
446
+ subprocess.run(cmd)
447
+ ```
448
+
449
+ ## Key References
450
+
451
+ - Esteban O et al. (2017). MRIQC: Advancing the automatic prediction of image quality in MRI from unseen sites. PLOS ONE 12(9):e0184661.
452
+ - Rosen AFG et al. (2018). Quantitative assessment of structural image quality. NeuroImage 169:407-418.
453
+ - Fischl B. (2012). FreeSurfer. NeuroImage 62(2):774-781.
454
+ - Klapwijk ET et al. (2019). Qoala-T: A supervised-learning tool for quality control of FreeSurfer segmented MRI data. NeuroImage 189:116-129.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Parse fMRIPrep confounds and generate subject-level QC summaries.
4
+
5
+ Usage:
6
+ python parse_fmriprep_confounds.py /path/to/fmriprep/output --fd-thresh 0.5 -o qc.csv
7
+ """
8
+
9
+ import argparse
10
+ import pandas as pd
11
+ import numpy as np
12
+ from pathlib import Path
13
+ import json
14
+
15
+
16
+ def find_confounds_files(fmriprep_dir: Path) -> list:
17
+ """Find all confounds TSV files in fMRIPrep output."""
18
+ return list(fmriprep_dir.rglob('*_desc-confounds_timeseries.tsv'))
19
+
20
+
21
+ def summarize_confounds(confounds_path: Path, fd_thresh: float = 0.5) -> dict:
22
+ """
23
+ Summarize a single confounds file.
24
+
25
+ Parameters
26
+ ----------
27
+ confounds_path : Path
28
+ Path to confounds TSV
29
+ fd_thresh : float
30
+ FD threshold for counting high-motion volumes
31
+
32
+ Returns
33
+ -------
34
+ dict
35
+ Summary statistics
36
+ """
37
+ df = pd.read_csv(confounds_path, sep='\t')
38
+
39
+ # Parse BIDS entities from filename
40
+ name = confounds_path.stem.replace('_desc-confounds_timeseries', '')
41
+
42
+ result = {'bids_name': name, 'confounds_file': str(confounds_path)}
43
+
44
+ # FD statistics
45
+ if 'framewise_displacement' in df.columns:
46
+ fd = df['framewise_displacement'].dropna()
47
+ result['fd_mean'] = fd.mean()
48
+ result['fd_max'] = fd.max()
49
+ result['fd_std'] = fd.std()
50
+ result['n_high_fd'] = (fd > fd_thresh).sum()
51
+ result['fd_perc'] = 100 * result['n_high_fd'] / len(fd)
52
+
53
+ # DVARS statistics
54
+ if 'std_dvars' in df.columns:
55
+ dvars = df['std_dvars'].dropna()
56
+ result['dvars_mean'] = dvars.mean()
57
+ result['dvars_max'] = dvars.max()
58
+
59
+ # Count motion outliers if present
60
+ outlier_cols = [c for c in df.columns if c.startswith('motion_outlier')]
61
+ if outlier_cols:
62
+ result['n_motion_outliers'] = df[outlier_cols].sum().sum()
63
+
64
+ # Non-steady state volumes
65
+ nss_cols = [c for c in df.columns if c.startswith('non_steady_state')]
66
+ result['n_non_steady_state'] = len(nss_cols)
67
+
68
+ # Total volumes
69
+ result['n_volumes'] = len(df)
70
+
71
+ return result
72
+
73
+
74
+ def process_fmriprep_dir(fmriprep_dir: Path, fd_thresh: float = 0.5) -> pd.DataFrame:
75
+ """Process all subjects in fMRIPrep output directory."""
76
+ confounds_files = find_confounds_files(fmriprep_dir)
77
+
78
+ if not confounds_files:
79
+ raise FileNotFoundError(f"No confounds files found in {fmriprep_dir}")
80
+
81
+ summaries = []
82
+ for cf in confounds_files:
83
+ try:
84
+ summary = summarize_confounds(cf, fd_thresh)
85
+ summaries.append(summary)
86
+ except Exception as e:
87
+ print(f"Error processing {cf}: {e}")
88
+
89
+ return pd.DataFrame(summaries)
90
+
91
+
92
+ def flag_subjects(df: pd.DataFrame, fd_mean_thresh: float, fd_perc_thresh: float) -> pd.DataFrame:
93
+ """Add exclusion flags to summary DataFrame."""
94
+ df = df.copy()
95
+
96
+ df['flag_fd_mean'] = df['fd_mean'] > fd_mean_thresh
97
+ df['flag_fd_perc'] = df['fd_perc'] > fd_perc_thresh
98
+
99
+ df['exclude'] = df['flag_fd_mean'] | df['flag_fd_perc']
100
+
101
+ return df
102
+
103
+
104
+ def generate_report(df: pd.DataFrame, fd_mean_thresh: float, fd_perc_thresh: float) -> str:
105
+ """Generate text report."""
106
+ n_total = len(df)
107
+ n_exclude = df['exclude'].sum() if 'exclude' in df.columns else 0
108
+
109
+ report = f"""
110
+ fMRIPrep Confounds QC Summary
111
+ =============================
112
+ Total runs: {n_total}
113
+ Thresholds: fd_mean < {fd_mean_thresh}mm, fd_perc < {fd_perc_thresh}%
114
+
115
+ Exclusions: {n_exclude} ({100*n_exclude/n_total:.1f}%)
116
+
117
+ FD Statistics (all runs):
118
+ Mean: {df['fd_mean'].mean():.3f} mm
119
+ Std: {df['fd_mean'].std():.3f} mm
120
+ Range: [{df['fd_mean'].min():.3f}, {df['fd_mean'].max():.3f}]
121
+
122
+ High-motion volume % (all runs):
123
+ Mean: {df['fd_perc'].mean():.1f}%
124
+ Max: {df['fd_perc'].max():.1f}%
125
+ """
126
+ return report
127
+
128
+
129
+ def main():
130
+ parser = argparse.ArgumentParser(description='Parse fMRIPrep confounds for QC')
131
+ parser.add_argument('fmriprep_dir', help='fMRIPrep output directory')
132
+ parser.add_argument('--fd-thresh', type=float, default=0.5, help='FD threshold (mm)')
133
+ parser.add_argument('--fd-mean-thresh', type=float, default=0.3, help='Max mean FD')
134
+ parser.add_argument('--fd-perc-thresh', type=float, default=30, help='Max FD %')
135
+ parser.add_argument('-o', '--output', help='Output CSV path')
136
+
137
+ args = parser.parse_args()
138
+
139
+ fmriprep_dir = Path(args.fmriprep_dir)
140
+
141
+ print(f"Processing {fmriprep_dir}...")
142
+ df = process_fmriprep_dir(fmriprep_dir, args.fd_thresh)
143
+ df = flag_subjects(df, args.fd_mean_thresh, args.fd_perc_thresh)
144
+
145
+ print(generate_report(df, args.fd_mean_thresh, args.fd_perc_thresh))
146
+
147
+ if args.output:
148
+ df.to_csv(args.output, index=False)
149
+ print(f"Results saved to {args.output}")
150
+
151
+
152
+ if __name__ == '__main__':
153
+ main()