From 4793d2b8cc7c0bb3450915ab2dbc175eafe49e18 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 11:16:29 +0100 Subject: [PATCH 01/38] enh: initialise fieldprep --- nmriprep/argprep/fieldprep.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 nmriprep/argprep/fieldprep.py diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py new file mode 100644 index 0000000..cbac821 --- /dev/null +++ b/nmriprep/argprep/fieldprep.py @@ -0,0 +1,34 @@ +import numpy as np + +from ..image import convert_nef_to_grey, save_slice +from ..parser import get_fieldprep_parser +from ..utils import find_files, parse_kv + + +def fieldprep(): + args = get_fieldprep_parser().parse_args() + + if args.dark_field: + raise NotImplementedError("Dark field processing coming soon...") + + if args.flat_field: + ff_dir = args.flat_field + fnames = find_files(ff_dir.glob('*flatfield*.nef')) + if len(fnames) < 1: + raise FileNotFoundError( + f'No flat field files found in {ff_dir.absolute()}' + ) + out_dir = ff_dir.parent / 'preproc' if not args.output else args.output + out_dir.mkdir(parents=True, exist_ok=True) + + data = np.stack( + [ convert_nef_to_grey(fname) for fname in fnames] , + axis=2, + ).mean(axis=2) + out_stem = '_'.join( + f'{k}-{v}' for k, v + in parse_kv(fnames[0]).items() + if "flatfield" not in k + ) + save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) + return From 0eb98af4efbab0ac899498a35d90c6087948c031 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 11:17:04 +0100 Subject: [PATCH 02/38] enh: add cli options --- nmriprep/parser.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nmriprep/parser.py b/nmriprep/parser.py index 1a85efb..d4728ab 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -48,6 +48,19 @@ def get_argprep_parser(): return parser +def get_fieldprep_parser(): + """Build parser object.""" + + parser = ArgumentParser( + description='Convert dark/flatfield .nef images for processing with argprep', + formatter_class=ArgumentDefaultsHelpFormatter, + ) + parser.add_argument('--flat-field', help='Path to directory containing flat field image', type=Path, nargs='*') + parser.add_argument('--dark-field', help='Path to directory containing dark field image', type=Path, nargs='*') + parser.add_argument('--output', help='Optionally specify output directory', type=Path, nargs='*') + return parser + + def get_roiextract_parser(): """Build parser object.""" From 800fedc9a4651c36da25cfea3853da0bdc45009f Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 11:18:23 +0100 Subject: [PATCH 03/38] enh: add fieldprep script to pyproject --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5e6b66c..0d17c58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ Homepage = "https://github.com/brainkcl/nmriprep" [project.scripts] argprep = "nmriprep.argprep.argprep:main" +fieldprep = "nmriprep.argprep.fieldprep:fieldprep" roi_extract = "nmriprep.measure:roi_extract" [tool.hatch.build] From 630116272ab256fbb00c8cdc7a8b9d89291750c6 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 11:25:12 +0100 Subject: [PATCH 04/38] fix: number of args for fieldprep --- nmriprep/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nmriprep/parser.py b/nmriprep/parser.py index d4728ab..fcd121b 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -55,9 +55,9 @@ def get_fieldprep_parser(): description='Convert dark/flatfield .nef images for processing with argprep', formatter_class=ArgumentDefaultsHelpFormatter, ) - parser.add_argument('--flat-field', help='Path to directory containing flat field image', type=Path, nargs='*') - parser.add_argument('--dark-field', help='Path to directory containing dark field image', type=Path, nargs='*') - parser.add_argument('--output', help='Optionally specify output directory', type=Path, nargs='*') + parser.add_argument('--flat-field', help='Path to directory containing flat field image', type=Path) + parser.add_argument('--dark-field', help='Path to directory containing dark field image', type=Path) + parser.add_argument('--output', help='Optionally specify output directory', type=Path) return parser From 74c6e0845d8169817eabc67aca6bb32bc316248a Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 11:25:49 +0100 Subject: [PATCH 05/38] fix: specify filename stem for parsing --- nmriprep/argprep/fieldprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index cbac821..776b1f0 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -27,7 +27,7 @@ def fieldprep(): ).mean(axis=2) out_stem = '_'.join( f'{k}-{v}' for k, v - in parse_kv(fnames[0]).items() + in parse_kv(fnames[0].stem).items() if "flatfield" not in k ) save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) From d1460837220f4fdc88cf0c209a1582f4afcc1692 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 12:01:37 +0100 Subject: [PATCH 06/38] enh: use kv in name for output --- nmriprep/argprep/fieldprep.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 776b1f0..f34f6b7 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -18,17 +18,19 @@ def fieldprep(): raise FileNotFoundError( f'No flat field files found in {ff_dir.absolute()}' ) - out_dir = ff_dir.parent / 'preproc' if not args.output else args.output + + fname_parts = parse_kv(fnames[0].stem) + out_stem = '_'.join( + f'{k}-{v}' for k, v + in fname_parts.items() + if "flatfield" not in k + ) + out_dir = ff_dir.parents[1] / 'preproc' / f"sub-{fname_parts['sub']}" if not args.output else args.output out_dir.mkdir(parents=True, exist_ok=True) data = np.stack( [ convert_nef_to_grey(fname) for fname in fnames] , axis=2, ).mean(axis=2) - out_stem = '_'.join( - f'{k}-{v}' for k, v - in parse_kv(fnames[0].stem).items() - if "flatfield" not in k - ) save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) return From 434e46d76a09b50bf55618344be869b094771b3d Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 12:02:05 +0100 Subject: [PATCH 07/38] fix: change mean to median --- nmriprep/argprep/fieldprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index f34f6b7..0c4b1b7 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -31,6 +31,6 @@ def fieldprep(): data = np.stack( [ convert_nef_to_grey(fname) for fname in fnames] , axis=2, - ).mean(axis=2) + ).median(axis=2) save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) return From 88dcb76bade46e2b6b7162ecb27ddf310468c14f Mon Sep 17 00:00:00 2001 From: Eilidh MacNicol Date: Thu, 17 Apr 2025 12:10:12 +0100 Subject: [PATCH 08/38] sty: ruff --- nmriprep/argprep/fieldprep.py | 22 +++++++++++----------- nmriprep/parser.py | 12 +++++++++--- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 0c4b1b7..4a750bf 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -9,28 +9,28 @@ def fieldprep(): args = get_fieldprep_parser().parse_args() if args.dark_field: - raise NotImplementedError("Dark field processing coming soon...") - + raise NotImplementedError('Dark field processing coming soon...') + if args.flat_field: ff_dir = args.flat_field fnames = find_files(ff_dir.glob('*flatfield*.nef')) if len(fnames) < 1: - raise FileNotFoundError( - f'No flat field files found in {ff_dir.absolute()}' - ) + raise FileNotFoundError(f'No flat field files found in {ff_dir.absolute()}') fname_parts = parse_kv(fnames[0].stem) out_stem = '_'.join( - f'{k}-{v}' for k, v - in fname_parts.items() - if "flatfield" not in k + f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k + ) + out_dir = ( + ff_dir.parents[1] / 'preproc' / f'sub-{fname_parts["sub"]}' + if not args.output + else args.output ) - out_dir = ff_dir.parents[1] / 'preproc' / f"sub-{fname_parts['sub']}" if not args.output else args.output out_dir.mkdir(parents=True, exist_ok=True) data = np.stack( - [ convert_nef_to_grey(fname) for fname in fnames] , + [convert_nef_to_grey(fname) for fname in fnames], axis=2, ).median(axis=2) - save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) + save_slice(data, out_dir / f'{out_stem}_flatfield.tif') return diff --git a/nmriprep/parser.py b/nmriprep/parser.py index fcd121b..05075ad 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -55,9 +55,15 @@ def get_fieldprep_parser(): description='Convert dark/flatfield .nef images for processing with argprep', formatter_class=ArgumentDefaultsHelpFormatter, ) - parser.add_argument('--flat-field', help='Path to directory containing flat field image', type=Path) - parser.add_argument('--dark-field', help='Path to directory containing dark field image', type=Path) - parser.add_argument('--output', help='Optionally specify output directory', type=Path) + parser.add_argument( + '--flat-field', help='Path to directory containing flat field image', type=Path + ) + parser.add_argument( + '--dark-field', help='Path to directory containing dark field image', type=Path + ) + parser.add_argument( + '--output', help='Optionally specify output directory', type=Path + ) return parser From 1b2a52f0944a53dcc140289c108d10cc87276632 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:12:19 +0100 Subject: [PATCH 09/38] Revert "fix: change mean to median" This reverts commit 434e46d76a09b50bf55618344be869b094771b3d. --- nmriprep/argprep/fieldprep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 4a750bf..8b094df 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -31,6 +31,6 @@ def fieldprep(): data = np.stack( [convert_nef_to_grey(fname) for fname in fnames], axis=2, - ).median(axis=2) - save_slice(data, out_dir / f'{out_stem}_flatfield.tif') + ).mean(axis=2) + save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) return From d1a5599064ad358476832834935bf0b598f167e6 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:13:48 +0100 Subject: [PATCH 10/38] fix: change from mean to median --- nmriprep/argprep/fieldprep.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 8b094df..4a1d4af 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -28,9 +28,12 @@ def fieldprep(): ) out_dir.mkdir(parents=True, exist_ok=True) - data = np.stack( - [convert_nef_to_grey(fname) for fname in fnames], + data = np.median( + np.stack( + [convert_nef_to_grey(fname) for fname in fnames], + axis=2, + ), axis=2, - ).mean(axis=2) + ) save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) return From e961d88f6359935a91806ab9d3ba91698a71a9dc Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:24:26 +0100 Subject: [PATCH 11/38] sty: ruff --- nmriprep/argprep/fieldprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 4a1d4af..74c09dc 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -35,5 +35,5 @@ def fieldprep(): ), axis=2, ) - save_slice(data, out_dir / f"{out_stem}_flatfield.tif" ) + save_slice(data, out_dir / f'{out_stem}_flatfield.tif') return From c0024194b5fcb80aa965777334d52188f3cdc563 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:56:08 +0100 Subject: [PATCH 12/38] enh: add recursive grouping --- nmriprep/argprep/fieldprep.py | 39 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 74c09dc..f1e645f 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -1,4 +1,6 @@ import numpy as np +from collections import defaultdict +from pathlib import Path from ..image import convert_nef_to_grey, save_slice from ..parser import get_fieldprep_parser @@ -16,24 +18,27 @@ def fieldprep(): fnames = find_files(ff_dir.glob('*flatfield*.nef')) if len(fnames) < 1: raise FileNotFoundError(f'No flat field files found in {ff_dir.absolute()}') + + subdirs = defaultdict(list) + [subdirs[p.parent].append(p) for p in fnames] - fname_parts = parse_kv(fnames[0].stem) - out_stem = '_'.join( - f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k - ) - out_dir = ( - ff_dir.parents[1] / 'preproc' / f'sub-{fname_parts["sub"]}' - if not args.output - else args.output - ) - out_dir.mkdir(parents=True, exist_ok=True) + for subdir in subdirs.keys(): + sub_files = subdirs[subdir] + fname_parts = parse_kv(sub_files[0].stem) + out_stem = '_'.join( + f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k + ) + out_dir = Path( + subdir.str.lower().replace('sourcedata', 'preproc') + ) if not args.output else args.output + out_dir.mkdir(parents=True, exist_ok=True) - data = np.median( - np.stack( - [convert_nef_to_grey(fname) for fname in fnames], + data = np.median( + np.stack( + [convert_nef_to_grey(fname) for fname in sub_files], + axis=2, + ), axis=2, - ), - axis=2, - ) - save_slice(data, out_dir / f'{out_stem}_flatfield.tif') + ) + save_slice(data, out_dir / f'{out_stem}_flatfield.tif') return From 21d45e2feea0dd5b6f1f77b6638161f10bc3af16 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:58:02 +0100 Subject: [PATCH 13/38] fix: add recursive glob --- nmriprep/argprep/fieldprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index f1e645f..5285d8b 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -15,7 +15,7 @@ def fieldprep(): if args.flat_field: ff_dir = args.flat_field - fnames = find_files(ff_dir.glob('*flatfield*.nef')) + fnames = find_files(ff_dir.rglob('*flatfield*.nef')) if len(fnames) < 1: raise FileNotFoundError(f'No flat field files found in {ff_dir.absolute()}') From 7bfca3a00f22d37dd38a2889151f891d31bf8d6e Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 13:58:33 +0100 Subject: [PATCH 14/38] fix: explicit subdir string conversion --- nmriprep/argprep/fieldprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 5285d8b..9610f58 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -29,7 +29,7 @@ def fieldprep(): f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k ) out_dir = Path( - subdir.str.lower().replace('sourcedata', 'preproc') + str(subdir).lower().replace('sourcedata', 'preproc') ) if not args.output else args.output out_dir.mkdir(parents=True, exist_ok=True) From 8e7cd28f6212fbfae00a5a03e8e89b11fdfc5e98 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 14:18:41 +0100 Subject: [PATCH 15/38] bug: changing whole path to lower --- nmriprep/argprep/fieldprep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 9610f58..8650beb 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -29,8 +29,8 @@ def fieldprep(): f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k ) out_dir = Path( - str(subdir).lower().replace('sourcedata', 'preproc') - ) if not args.output else args.output + str(subdir).replace('sourcedata', 'preproc') + ).resolve() if not args.output else args.output out_dir.mkdir(parents=True, exist_ok=True) data = np.median( From c30a9ee83f001308a869279a94fcd08cb39e55a9 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 15:05:02 +0100 Subject: [PATCH 16/38] ref: allow flatfield and darkfield to be specified separately --- nmriprep/argprep/argprep.py | 21 +++++++-------------- nmriprep/argprep/fieldprep.py | 8 +++++++- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 8967d67..6c671e9 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -1,11 +1,12 @@ import nibabel as nb import numpy as np -from ..image import convert_nef_to_grey, read_tiff, save_slice +from ..image import convert_nef_to_grey, save_slice from ..parser import get_argprep_parser from ..plotting import plot_curve, plot_mosaic, plot_single_slice from ..utils import find_files, inverse_rodbard, rodbard from .calibration import calibrate_standard +from .fieldprep import find_fields def main(): @@ -16,19 +17,11 @@ def main(): # attempt to find flat field info flatfield_correction = {} - if args.dark_field and args.flat_field: - flatfield_correction['dark'] = read_tiff(args.dark_field) - flatfield_correction['flat'] = read_tiff(args.flat_field) - else: - # try searching for a preprocessed darkfield - latest_ff = find_files(out_dir.glob('*flatfield.tiff')) - latest_df = find_files(out_dir.glob('*darkfield.tiff')) - if latest_ff and latest_df: - flatfield_correction['dark'] = read_tiff(latest_df[-1]) - flatfield_correction['flat'] = read_tiff(latest_ff[-1]) - else: - print('Skipping flat field correction...') - flatfield_correction = None + flatfield_correction['dark'] = find_fields(args.dark_field, out_dir.glob('*darkfield.tif*')) + flatfield_correction['flat'] = find_fields(args.flat_field, out_dir.glob('*flatfield.tif*')) + if not all(flatfield_correction.values()): + print('Skipping flat field correction...') + flatfield_correction = None # identify subjects for pipeline all_subject_ids = set( diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index 8650beb..e2652fe 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -2,11 +2,17 @@ from collections import defaultdict from pathlib import Path -from ..image import convert_nef_to_grey, save_slice +from ..image import convert_nef_to_grey, read_tiff, save_slice from ..parser import get_fieldprep_parser from ..utils import find_files, parse_kv +def find_fields(user=None, search_map=None): + fieldpath = user if user is not None else find_files(search_map)[-1] + field = read_tiff(fieldpath) if fieldpath else None + return field + + def fieldprep(): args = get_fieldprep_parser().parse_args() From cffa1c71ef386fde92b4b7074eb95cbc32bdbfcd Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 15:07:13 +0100 Subject: [PATCH 17/38] ref: allow flatfield specification on a subjectwise basis --- nmriprep/argprep/argprep.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 6c671e9..f6fe46b 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -15,14 +15,6 @@ def main(): out_dir = src_dir.parent / 'preproc' if not args.output else args.output verbose = args.save_intermediate - # attempt to find flat field info - flatfield_correction = {} - flatfield_correction['dark'] = find_fields(args.dark_field, out_dir.glob('*darkfield.tif*')) - flatfield_correction['flat'] = find_fields(args.flat_field, out_dir.glob('*flatfield.tif*')) - if not all(flatfield_correction.values()): - print('Skipping flat field correction...') - flatfield_correction = None - # identify subjects for pipeline all_subject_ids = set( [ @@ -48,6 +40,14 @@ def main(): fig_dir = sub_dir / 'figures' fig_dir.mkdir(exist_ok=True) + # attempt to find flat field info + flatfield_correction = {} + flatfield_correction['dark'] = find_fields(args.dark_field, sub_dir.glob('*darkfield.tif*')) + flatfield_correction['flat'] = find_fields(args.flat_field, sub_dir.glob('*flatfield.tif*')) + if not all(flatfield_correction.values()): + print('Skipping flat field correction...') + flatfield_correction = None + # calibrate standards to transform GV to radioactivity popt, std_rad, std_gv, std_stem = calibrate_standard( sub_id, From 5a669f61308e1db29b0f2ee1c475c83e7d7b125f Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 15:09:51 +0100 Subject: [PATCH 18/38] sty: ruff --- nmriprep/argprep/argprep.py | 8 ++++++-- nmriprep/argprep/fieldprep.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index f6fe46b..4d01b49 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -42,8 +42,12 @@ def main(): # attempt to find flat field info flatfield_correction = {} - flatfield_correction['dark'] = find_fields(args.dark_field, sub_dir.glob('*darkfield.tif*')) - flatfield_correction['flat'] = find_fields(args.flat_field, sub_dir.glob('*flatfield.tif*')) + flatfield_correction['dark'] = find_fields( + args.dark_field, sub_dir.glob('*darkfield.tif*') + ) + flatfield_correction['flat'] = find_fields( + args.flat_field, sub_dir.glob('*flatfield.tif*') + ) if not all(flatfield_correction.values()): print('Skipping flat field correction...') flatfield_correction = None diff --git a/nmriprep/argprep/fieldprep.py b/nmriprep/argprep/fieldprep.py index e2652fe..b305e36 100644 --- a/nmriprep/argprep/fieldprep.py +++ b/nmriprep/argprep/fieldprep.py @@ -1,7 +1,8 @@ -import numpy as np from collections import defaultdict from pathlib import Path +import numpy as np + from ..image import convert_nef_to_grey, read_tiff, save_slice from ..parser import get_fieldprep_parser from ..utils import find_files, parse_kv @@ -24,7 +25,7 @@ def fieldprep(): fnames = find_files(ff_dir.rglob('*flatfield*.nef')) if len(fnames) < 1: raise FileNotFoundError(f'No flat field files found in {ff_dir.absolute()}') - + subdirs = defaultdict(list) [subdirs[p.parent].append(p) for p in fnames] @@ -34,9 +35,11 @@ def fieldprep(): out_stem = '_'.join( f'{k}-{v}' for k, v in fname_parts.items() if 'flatfield' not in k ) - out_dir = Path( - str(subdir).replace('sourcedata', 'preproc') - ).resolve() if not args.output else args.output + out_dir = ( + Path(str(subdir).replace('sourcedata', 'preproc')).resolve() + if not args.output + else args.output + ) out_dir.mkdir(parents=True, exist_ok=True) data = np.median( From b3995124e1dc1b47f771f206d1f5f0cfdd87f6d2 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:42:35 +0100 Subject: [PATCH 19/38] fix: boolean dict value checking --- nmriprep/argprep/argprep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 4d01b49..18aed96 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -48,7 +48,7 @@ def main(): flatfield_correction['flat'] = find_fields( args.flat_field, sub_dir.glob('*flatfield.tif*') ) - if not all(flatfield_correction.values()): + if any(v is None for v in flatfield_correction.values()): print('Skipping flat field correction...') flatfield_correction = None From 65960e759a63f4a09f28a02e5f678880a3be8122 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:47:22 +0100 Subject: [PATCH 20/38] ref: modularise subject workflow --- nmriprep/argprep/argprep.py | 251 ++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 124 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 18aed96..d399ef2 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -8,6 +8,132 @@ from .calibration import calibrate_standard from .fieldprep import find_fields +def subject_workflow(sub_id, src_dir, out_dir, args, verbose): + sub_dir = out_dir / sub_id + sub_dir.mkdir(exist_ok=True, parents=True) + fig_dir = sub_dir / 'figures' + fig_dir.mkdir(exist_ok=True) + + # attempt to find flat field info + flatfield_correction = {} + flatfield_correction['dark'] = find_fields( + args.dark_field, sub_dir.glob('*darkfield.tif*') + ) + flatfield_correction['flat'] = find_fields( + args.flat_field, sub_dir.glob('*flatfield.tif*') + ) + if any(v is None for v in flatfield_correction.values()): + print('Skipping flat field correction...') + flatfield_correction = None + + # calibrate standards to transform GV to radioactivity + popt, std_rad, std_gv, std_stem = calibrate_standard( + sub_id, + src_dir, + args.standard_type, + flatfield_correction=flatfield_correction, + out_dir=sub_dir if verbose else None, + ) + + if verbose: + plot_curve(std_rad, std_gv, rodbard(std_rad, *popt), sub_dir, std_stem) + + print(f'success! fitting data with parameters: {popt}') + + # find subject files + slide_files = find_files(src_dir.glob(f'*subj-{sub_id}*slide*.nef')) + if len(slide_files) < 1: + raise FileNotFoundError(f'No slide files found for {sub_id} in {src_dir}') + + # convert nefs to grey value + data_gv = np.stack( + [ + convert_nef_to_grey( + fname, + flatfield_correction=flatfield_correction, + crop_row=args.crop_height, + crop_col=args.crop_width, + invert=True, + ) + for fname in slide_files + ], + axis=2, + ) + + if args.rotate > 0: + data_gv = np.rot90(data_gv, k=args.rotate, axes=(0, 1)) + + # calibrate slice data + print('Converting slide data to radioactivity') + data_radioactivity = inverse_rodbard(data_gv, *popt) + # clip "negative" values + data_radioactivity[np.isnan(data_radioactivity)] = 0 + print('Success! Generating output...') + + out_stem = std_stem.split('_standard')[0] + + if args.save_nii: + # write out nii image (e.g. for Jim) + # find information on number of slides from last file name + last_section_parts = slide_files[-1].stem.split('_') + max_slide = int( + [ + last_section_parts[idx].split('-')[-1] + for idx, val in enumerate(last_section_parts) + if 'slide' in val + ][0] + ) + + # iterate through slides and collect sections to concatenate + for slide_no in range(max_slide): + slide_val = str(slide_no + 1).zfill(2) + slide_sections = { + fname: idx + for idx, fname in enumerate(slide_files) + if f'slide-{str(slide_val).zfill(2)}' in str(fname.stem) + } + + nb.Nifti1Image( + data_radioactivity[..., sorted(slide_sections.values())], + affine=None, + ).to_filename( + sub_dir / f'{out_stem}_slide-{slide_val}_desc-preproc_ARG.nii.gz' + ) + + if args.save_tif: + print('Saving individual files...') + for idx, fname in enumerate(slide_files): + print(f'{fname.stem}') + save_slice( + data_radioactivity[..., idx], + sub_dir / f'{fname.stem}_desc-preproc_ARG.tif', + ) + + if verbose: + # plot image + plot_single_slice( + data_radioactivity[..., idx], + fig_dir / f'{fname.stem}_desc-preproc_ARG.png', + ) + + # plot fits + plot_curve( + std_rad=std_rad, + std_gv=std_gv, + fitted_gv=rodbard(std_rad, *popt), + out_dir=fig_dir, + out_stem=fname.stem, + data_rad=data_radioactivity[..., idx].ravel(), + data_gv=data_gv[..., idx].ravel(), + ) + if args.mosaic_slices: + sliced = ( + data_radioactivity + if -1 in args.mosaic_slices + else data_radioactivity[..., args.mosaic_slices] + ) + plot_mosaic(sliced, out_dir / f'{out_stem}_desc-preproc_ARG.png') + def main(): args = get_argprep_parser().parse_args() @@ -35,130 +161,7 @@ def main(): subjects_to_process = all_subject_ids for sub_id in subjects_to_process: - sub_dir = out_dir / sub_id - sub_dir.mkdir(exist_ok=True, parents=True) - fig_dir = sub_dir / 'figures' - fig_dir.mkdir(exist_ok=True) - - # attempt to find flat field info - flatfield_correction = {} - flatfield_correction['dark'] = find_fields( - args.dark_field, sub_dir.glob('*darkfield.tif*') - ) - flatfield_correction['flat'] = find_fields( - args.flat_field, sub_dir.glob('*flatfield.tif*') - ) - if any(v is None for v in flatfield_correction.values()): - print('Skipping flat field correction...') - flatfield_correction = None - - # calibrate standards to transform GV to radioactivity - popt, std_rad, std_gv, std_stem = calibrate_standard( - sub_id, - src_dir, - args.standard_type, - flatfield_correction=flatfield_correction, - out_dir=sub_dir if verbose else None, - ) - - if verbose: - plot_curve(std_rad, std_gv, rodbard(std_rad, *popt), sub_dir, std_stem) - - print(f'success! fitting data with parameters: {popt}') - - # find subject files - slide_files = find_files(src_dir.glob(f'*subj-{sub_id}*slide*.nef')) - if len(slide_files) < 1: - raise FileNotFoundError(f'No slide files found for {sub_id} in {src_dir}') - - # convert nefs to grey value - data_gv = np.stack( - [ - convert_nef_to_grey( - fname, - flatfield_correction=flatfield_correction, - crop_row=args.crop_height, - crop_col=args.crop_width, - invert=True, - ) - for fname in slide_files - ], - axis=2, - ) - - if args.rotate > 0: - data_gv = np.rot90(data_gv, k=args.rotate, axes=(0, 1)) - - # calibrate slice data - print('Converting slide data to radioactivity') - data_radioactivity = inverse_rodbard(data_gv, *popt) - # clip "negative" values - data_radioactivity[np.isnan(data_radioactivity)] = 0 - print('Success! Generating output...') - - out_stem = std_stem.split('_standard')[0] - - if args.save_nii: - # write out nii image (e.g. for Jim) - # find information on number of slides from last file name - last_section_parts = slide_files[-1].stem.split('_') - max_slide = int( - [ - last_section_parts[idx].split('-')[-1] - for idx, val in enumerate(last_section_parts) - if 'slide' in val - ][0] - ) - - # iterate through slides and collect sections to concatenate - for slide_no in range(max_slide): - slide_val = str(slide_no + 1).zfill(2) - slide_sections = { - fname: idx - for idx, fname in enumerate(slide_files) - if f'slide-{str(slide_val).zfill(2)}' in str(fname.stem) - } - - nb.Nifti1Image( - data_radioactivity[..., sorted(slide_sections.values())], - affine=None, - ).to_filename( - sub_dir / f'{out_stem}_slide-{slide_val}_desc-preproc_ARG.nii.gz' - ) - - if args.save_tif: - print('Saving individual files...') - for idx, fname in enumerate(slide_files): - print(f'{fname.stem}') - save_slice( - data_radioactivity[..., idx], - sub_dir / f'{fname.stem}_desc-preproc_ARG.tif', - ) - - if verbose: - # plot image - plot_single_slice( - data_radioactivity[..., idx], - fig_dir / f'{fname.stem}_desc-preproc_ARG.png', - ) - - # plot fits - plot_curve( - std_rad=std_rad, - std_gv=std_gv, - fitted_gv=rodbard(std_rad, *popt), - out_dir=fig_dir, - out_stem=fname.stem, - data_rad=data_radioactivity[..., idx].ravel(), - data_gv=data_gv[..., idx].ravel(), - ) - if args.mosaic_slices: - sliced = ( - data_radioactivity - if -1 in args.mosaic_slices - else data_radioactivity[..., args.mosaic_slices] - ) - plot_mosaic(sliced, out_dir / f'{out_stem}_desc-preproc_ARG.png') + subject_workflow(sub_id, src_dir, out_dir, args, verbose) if __name__ == '__main__': From ac1bf258b2733af9a7b87a45a81e345f9f55bb81 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:49:33 +0100 Subject: [PATCH 21/38] ref: assume src/sub/data hierarchy and group according to sub --- nmriprep/argprep/argprep.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index d399ef2..7c8ff7b 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -1,3 +1,4 @@ +from collections import defaultdict import nibabel as nb import numpy as np @@ -142,14 +143,10 @@ def main(): verbose = args.save_intermediate # identify subjects for pipeline - all_subject_ids = set( - [ - part.split('-')[1] - for fpath in src_dir.glob('*.nef') - for part in fpath.stem.split('_') - if 'subj' in part - ] - ) + fnames = find_files(src_dir.rglob('*.nef')) + subdirs = defaultdict(list) + [subdirs[p.parent].append(p) for p in fnames] + all_subject_ids = set([subdir.stem for subdir in subdirs.keys()]) if args.subject_id: print(f'Processing subjects: {args.subject_id}') From f4a8ab23f775fa23ceed4caa7bd23ede86cdee71 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:54:32 +0100 Subject: [PATCH 22/38] ref: add previously found files rather than searching each time --- nmriprep/argprep/argprep.py | 22 ++++++++++++++-------- nmriprep/argprep/calibration.py | 3 +-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 7c8ff7b..0748938 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -9,7 +9,8 @@ from .calibration import calibrate_standard from .fieldprep import find_fields -def subject_workflow(sub_id, src_dir, out_dir, args, verbose): +def subject_workflow(sub_files, out_dir, args, verbose): + sub_id = sub_files[0].parent.stem sub_dir = out_dir / sub_id sub_dir.mkdir(exist_ok=True, parents=True) fig_dir = sub_dir / 'figures' @@ -28,9 +29,12 @@ def subject_workflow(sub_id, src_dir, out_dir, args, verbose): flatfield_correction = None # calibrate standards to transform GV to radioactivity + std_files = [fname for fname in sub_files if "standard" in fname.stem] + if len(std_files) < 1: + raise FileNotFoundError(f'No standard files found for {sub_id}') popt, std_rad, std_gv, std_stem = calibrate_standard( sub_id, - src_dir, + std_files, args.standard_type, flatfield_correction=flatfield_correction, out_dir=sub_dir if verbose else None, @@ -42,9 +46,9 @@ def subject_workflow(sub_id, src_dir, out_dir, args, verbose): print(f'success! fitting data with parameters: {popt}') # find subject files - slide_files = find_files(src_dir.glob(f'*subj-{sub_id}*slide*.nef')) + slide_files = [fname for fname in sub_files if "slide" in fname.stem] if len(slide_files) < 1: - raise FileNotFoundError(f'No slide files found for {sub_id} in {src_dir}') + raise FileNotFoundError(f'No slide files found for {sub_id}') # convert nefs to grey value data_gv = np.stack( @@ -146,19 +150,21 @@ def main(): fnames = find_files(src_dir.rglob('*.nef')) subdirs = defaultdict(list) [subdirs[p.parent].append(p) for p in fnames] - all_subject_ids = set([subdir.stem for subdir in subdirs.keys()]) + all_subject_directories = [subdir for subdir in subdirs.keys()] if args.subject_id: print(f'Processing subjects: {args.subject_id}') subjects_to_process = [ - subj for subj in all_subject_ids if subj in args.subject_id + subj for subj in all_subject_directories + if subj.stem.replace("sub-","") in args.subject_id ] else: print('Processing all subjects') - subjects_to_process = all_subject_ids + subjects_to_process = all_subject_directories for sub_id in subjects_to_process: - subject_workflow(sub_id, src_dir, out_dir, args, verbose) + sub_files = subdirs[sub_id] + subject_workflow(sub_files, out_dir, args, verbose) if __name__ == '__main__': diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index cfa73df..5d3eeb1 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -100,7 +100,7 @@ def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=N def calibrate_standard( - sub_id, src_dir, standard_type, flatfield_correction=None, out_dir=None + standard_files, standard_type, flatfield_correction=None, out_dir=None ): from scipy.optimize import curve_fit @@ -110,7 +110,6 @@ def calibrate_standard( with importlib.resources.open_text(data, 'standards.json') as f: standard_vals = pd.read_json(f)[standard_type].dropna() - standard_files = find_files(src_dir.glob(f'*subj-{sub_id}*standard*.nef')) assert len(standard_files) == len(standard_vals) standards_df = pd.DataFrame( From a5b27b2a2a5d33b5d0d0582302596d57ed4010b7 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:58:56 +0100 Subject: [PATCH 23/38] ref: standard stem definition using parse_kv --- nmriprep/argprep/argprep.py | 6 ++---- nmriprep/argprep/calibration.py | 10 +++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 0748938..c23d453 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -75,8 +75,6 @@ def subject_workflow(sub_files, out_dir, args, verbose): data_radioactivity[np.isnan(data_radioactivity)] = 0 print('Success! Generating output...') - out_stem = std_stem.split('_standard')[0] - if args.save_nii: # write out nii image (e.g. for Jim) # find information on number of slides from last file name @@ -102,7 +100,7 @@ def subject_workflow(sub_files, out_dir, args, verbose): data_radioactivity[..., sorted(slide_sections.values())], affine=None, ).to_filename( - sub_dir / f'{out_stem}_slide-{slide_val}_desc-preproc_ARG.nii.gz' + sub_dir / f'{std_stem}_slide-{slide_val}_desc-preproc_ARG.nii.gz' ) if args.save_tif: @@ -137,7 +135,7 @@ def subject_workflow(sub_files, out_dir, args, verbose): if -1 in args.mosaic_slices else data_radioactivity[..., args.mosaic_slices] ) - plot_mosaic(sliced, out_dir / f'{out_stem}_desc-preproc_ARG.png') + plot_mosaic(sliced, out_dir / f'{std_stem}_desc-preproc_ARG.png') def main(): diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 5d3eeb1..0dc13b4 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -6,7 +6,7 @@ from ..image import convert_nef_to_grey from ..plotting import plot_roi -from ..utils import find_files, rodbard +from ..utils import parse_kv, rodbard def get_image_patch(center_coord, square_apothem: int = 100): @@ -111,6 +111,11 @@ def calibrate_standard( standard_vals = pd.read_json(f)[standard_type].dropna() assert len(standard_files) == len(standard_vals) + out_stem = '_'.join( + f'{k}-{v}' for k, v + in parse_kv(standard_files[0]).items() + if 'standard' not in k + ) standards_df = pd.DataFrame( { @@ -141,7 +146,7 @@ def calibrate_standard( X = standards_df['radioactivity (uCi/g)'] y = standards_df['median grey'] # https://www.myassays.com/four-parameter-logistic-regression.html - print(f'fitting Rodbard curve for {standard_files[0].stem[:-3]}') + print(f'fitting Rodbard curve for {out_stem}') popt, _ = curve_fit( rodbard, X, @@ -152,7 +157,6 @@ def calibrate_standard( ) # save for calibrating slice images - out_stem = standard_files[0].stem[:-3] if out_dir: standards_df.to_json(f'{out_dir / out_stem}_standards.json') with (out_dir / f'{out_stem}_calibration.json').open(mode='w') as f: From 315eb9fc5d81b035c9b60989c1e53959e8946143 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 16:59:59 +0100 Subject: [PATCH 24/38] sty: inconsistent path definiton --- nmriprep/argprep/calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 0dc13b4..dd53d43 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -158,7 +158,7 @@ def calibrate_standard( # save for calibrating slice images if out_dir: - standards_df.to_json(f'{out_dir / out_stem}_standards.json') + standards_df.to_json(out_dir / f'{out_stem}_standards.json') with (out_dir / f'{out_stem}_calibration.json').open(mode='w') as f: json.dump(dict(zip(['min', 'slope', 'ED50', 'max'], popt)), f) return popt, X, y, out_stem From 929d3481631af69bc839026aade3c048b3599b0c Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 17:38:07 +0100 Subject: [PATCH 25/38] fix: remove sub_id from calibrate_standard call --- nmriprep/argprep/argprep.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index c23d453..1d8a97f 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -33,7 +33,6 @@ def subject_workflow(sub_files, out_dir, args, verbose): if len(std_files) < 1: raise FileNotFoundError(f'No standard files found for {sub_id}') popt, std_rad, std_gv, std_stem = calibrate_standard( - sub_id, std_files, args.standard_type, flatfield_correction=flatfield_correction, From 9b06a61b91e644c2d2a246d54bdf8fe9772616f6 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Thu, 17 Apr 2025 17:40:19 +0100 Subject: [PATCH 26/38] fix: pass parse_kv str --- nmriprep/argprep/calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index dd53d43..b2777af 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -113,7 +113,7 @@ def calibrate_standard( assert len(standard_files) == len(standard_vals) out_stem = '_'.join( f'{k}-{v}' for k, v - in parse_kv(standard_files[0]).items() + in parse_kv(standard_files[0].stem).items() if 'standard' not in k ) From 89b6af9f600b3032d20df7b544cda74be92ba217 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 22 Apr 2025 14:04:41 +0100 Subject: [PATCH 27/38] ref: rely on dataset_description.json for isotope --- nmriprep/argprep/argprep.py | 6 ++--- nmriprep/argprep/calibration.py | 14 +++++++++-- nmriprep/data/standards.json | 41 ++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 1d8a97f..c7704d7 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -9,7 +9,7 @@ from .calibration import calibrate_standard from .fieldprep import find_fields -def subject_workflow(sub_files, out_dir, args, verbose): +def subject_workflow(sub_files, src_dir, out_dir, args, verbose): sub_id = sub_files[0].parent.stem sub_dir = out_dir / sub_id sub_dir.mkdir(exist_ok=True, parents=True) @@ -34,7 +34,7 @@ def subject_workflow(sub_files, out_dir, args, verbose): raise FileNotFoundError(f'No standard files found for {sub_id}') popt, std_rad, std_gv, std_stem = calibrate_standard( std_files, - args.standard_type, + src_dir, flatfield_correction=flatfield_correction, out_dir=sub_dir if verbose else None, ) @@ -161,7 +161,7 @@ def main(): for sub_id in subjects_to_process: sub_files = subdirs[sub_id] - subject_workflow(sub_files, out_dir, args, verbose) + subject_workflow(sub_files, src_dir, out_dir, args, verbose) if __name__ == '__main__': diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index b2777af..26cebd6 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -13,6 +13,15 @@ def get_image_patch(center_coord, square_apothem: int = 100): return slice(center_coord - square_apothem, center_coord + square_apothem) +def get_dataset_standard(source_dir): + if not (source_dir / 'dataset_description.json').exists(): + raise FileNotFoundError(f"Dataset description file missing from {source_dir.resolve()}") + with (source_dir / 'dataset_description.json').open('r') as fname: + standard_info = json.load(fname)['standard'] + range_type = standard_info['range'] if 'range' in standard_info.keys().lower() else 'default' + return (standard_info['isotope'], range_type) + + def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=None): import statistics @@ -100,15 +109,16 @@ def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=N def calibrate_standard( - standard_files, standard_type, flatfield_correction=None, out_dir=None + standard_files, src_dir, flatfield_correction=None, out_dir=None ): from scipy.optimize import curve_fit from .. import data # load standard information + isotope, range_type = get_dataset_standard(src_dir) with importlib.resources.open_text(data, 'standards.json') as f: - standard_vals = pd.read_json(f)[standard_type].dropna() + standard_vals = pd.Series(json.load(f)[isotope][range_type]) assert len(standard_files) == len(standard_vals) out_stem = '_'.join( diff --git a/nmriprep/data/standards.json b/nmriprep/data/standards.json index 7c10819..99b8449 100644 --- a/nmriprep/data/standards.json +++ b/nmriprep/data/standards.json @@ -1 +1,40 @@ -{"C14": {"background": 0.0, "1": 39.0, "2": 78.0, "3": 159.0, "4": 292.0, "5": 446.0, "6": 656.0, "7": 869.0, "8": 1098.0}, "H3": {"background": 0.0, "1": 2.0, "2": 3.7, "3": 8.0, "4": 16.6, "5": 36.6, "6": 63.1, "7": 138.1, "8": 243.1, "9": 489.1}} \ No newline at end of file +{ + "C14": { + "default": { + "background": 0.0, + "1": 39.0, + "2": 78.0, + "3": 159.0, + "4": 292.0, + "5": 446.0, + "6": 656.0, + "7": 869.0, + "8": 1098.0 + } + }, + "H3": { + "wide" : { + "background": 0.0, + "1": 2.0, + "2": 3.7, + "3": 8.0, + "4": 16.6, + "5": 36.6, + "6": 63.1, + "7": 138.1, + "8": 243.1, + "9": 489.1 + }, + "narrow" : { + "background": 0.0, + "1": 3.0, + "2": 4.7, + "3": 8.7, + "4": 23.6, + "5": 33.5, + "6": 40.2, + "7": 66.7, + "8": 109.4 + } + } +} \ No newline at end of file From 260fafe892c731ec9bc7a30856b3f99ce79d8852 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 22 Apr 2025 14:05:05 +0100 Subject: [PATCH 28/38] chore: remove isotope option from cli --- nmriprep/parser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nmriprep/parser.py b/nmriprep/parser.py index 05075ad..ea00851 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -9,9 +9,6 @@ def get_argprep_parser(): description='Convert .nef image to .nii with uCi/g voxels', formatter_class=ArgumentDefaultsHelpFormatter, ) - parser.add_argument( - 'standard_type', help='radioisotope of standards', choices=['H3', 'C14'] - ) parser.add_argument('source_directory', help='raw data directory', type=Path) parser.add_argument('--flat-field', help='Path to flat field image', type=Path) parser.add_argument('--dark-field', help='Path to dark field image', type=Path) From c8ea20bae5c6c7f83380745a9cd4e7ef26a29507 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 22 Apr 2025 14:18:13 +0100 Subject: [PATCH 29/38] ref: search for standard images in function --- nmriprep/argprep/argprep.py | 5 +---- nmriprep/argprep/calibration.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index c7704d7..ff30830 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -29,11 +29,8 @@ def subject_workflow(sub_files, src_dir, out_dir, args, verbose): flatfield_correction = None # calibrate standards to transform GV to radioactivity - std_files = [fname for fname in sub_files if "standard" in fname.stem] - if len(std_files) < 1: - raise FileNotFoundError(f'No standard files found for {sub_id}') popt, std_rad, std_gv, std_stem = calibrate_standard( - std_files, + sub_files, src_dir, flatfield_correction=flatfield_correction, out_dir=sub_dir if verbose else None, diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 26cebd6..723592a 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -109,7 +109,7 @@ def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=N def calibrate_standard( - standard_files, src_dir, flatfield_correction=None, out_dir=None + sub_files, src_dir, flatfield_correction=None, out_dir=None ): from scipy.optimize import curve_fit @@ -120,7 +120,14 @@ def calibrate_standard( with importlib.resources.open_text(data, 'standards.json') as f: standard_vals = pd.Series(json.load(f)[isotope][range_type]) - assert len(standard_files) == len(standard_vals) + standard_files = [fname for fname in sub_files if f"standard-{isotope}" in fname.stem] + if len(standard_files) < 1: + raise FileNotFoundError(f'No {isotope} standard files found in {sub_files[0].parent}') + if len(standard_files) != len(standard_vals): + # try filtering standard files by range type in case there are two sets of standard images + standard_files = [fname for fname in standard_files if f"range-{range_type}" in fname.stem] + if len(standard_files) != len(standard_vals): + raise RuntimeError(f"Number of standard files {len(standard_files)} does not match standard values") out_stem = '_'.join( f'{k}-{v}' for k, v in parse_kv(standard_files[0].stem).items() From 5ae0520924e25a9a1fcfda1be32a26a72a58f48a Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 22 Apr 2025 17:41:24 +0100 Subject: [PATCH 30/38] fix: assume standard keys is lower --- nmriprep/argprep/calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 723592a..4ef165a 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -18,7 +18,7 @@ def get_dataset_standard(source_dir): raise FileNotFoundError(f"Dataset description file missing from {source_dir.resolve()}") with (source_dir / 'dataset_description.json').open('r') as fname: standard_info = json.load(fname)['standard'] - range_type = standard_info['range'] if 'range' in standard_info.keys().lower() else 'default' + range_type = standard_info['range'] if 'range' in standard_info.keys() else 'default' return (standard_info['isotope'], range_type) From 51202ed834d37c6b5e6ff89495f10f6d1a5ad961 Mon Sep 17 00:00:00 2001 From: eugenegkim <53093555+eugenegkim@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:36:50 +0100 Subject: [PATCH 31/38] fix: boolean logic in auto standard detection --- nmriprep/argprep/calibration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 4ef165a..2d9e5a0 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -57,7 +57,7 @@ def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=N for peak1, peak2 in zip(peak_loc[:-1], peak_loc[1:]): troughs = trough_loc[np.logical_and(trough_loc > peak1, trough_loc < peak2)] thresholds.append( - min(troughs) if troughs else statistics.mean([peak1, peak2]) + min(troughs) if len(troughs) > 0 else statistics.mean([peak1, peak2]) ) regions = np.digitize(gray_medfilt, bins=thresholds) From dc5039223409df848d0ad2c3818edefb037492c8 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Fri, 9 May 2025 11:29:21 +0100 Subject: [PATCH 32/38] fix: subj typo --- nmriprep/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nmriprep/utils.py b/nmriprep/utils.py index d85af53..0218b52 100644 --- a/nmriprep/utils.py +++ b/nmriprep/utils.py @@ -70,12 +70,12 @@ def inverse_rodbard(y, min_, slope, ed50, max_): def normalise_by_region(df, region): region_df = df.query((f'region == "{region}"')) - if region_df.duplicated(['subj', 'slide', 'section']).any(): + if region_df.duplicated(['sub', 'slide', 'section']).any(): raise ValueError('region_df has duplicate keys!') region_df[f'median_{region}_values'] = df['values'].apply(np.median) out = df.merge( region_df, - on=['subj', 'slide', 'section'], + on=['sub', 'slide', 'section'], how='left', suffixes=('', '_r'), validate='m:1', From 49a85305072a946f1e63b5538241bd92eaab0537 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Fri, 9 May 2025 11:36:52 +0100 Subject: [PATCH 33/38] enh: flexibility to change normalisation summary statistic at command line --- nmriprep/measure.py | 2 +- nmriprep/parser.py | 7 +++++++ nmriprep/utils.py | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/nmriprep/measure.py b/nmriprep/measure.py index 19cd2b5..7f1421e 100644 --- a/nmriprep/measure.py +++ b/nmriprep/measure.py @@ -62,7 +62,7 @@ def roi_extract(): merge_keys = [col for col in summary_df.columns if 'values' not in col] for region in args.norm_regions: array_col = f'{region}_values' - main_df[array_col] = normalise_by_region(main_df, region) + main_df[array_col] = normalise_by_region(main_df, region, measure=args.norm_measure) summary_df = summary_df.merge( summarise_vals( main_df[merge_keys + [array_col]], diff --git a/nmriprep/parser.py b/nmriprep/parser.py index ea00851..3ced723 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -91,6 +91,13 @@ def get_roiextract_parser(): nargs='*', type=str, ) + parser.add_argument( + '--norm-measure', + help='summary statistic for normalisation', + choices=['mean', 'median', 'mode'], + default='median', + type=str, + ) parser.add_argument( '--output', help='name of output .json file', default='roi_values', type=str ) diff --git a/nmriprep/utils.py b/nmriprep/utils.py index 0218b52..393a71a 100644 --- a/nmriprep/utils.py +++ b/nmriprep/utils.py @@ -68,11 +68,11 @@ def inverse_rodbard(y, min_, slope, ed50, max_): return ed50 * (((min_ - max_) / (y - max_)) - 1.0) ** (1.0 / slope) -def normalise_by_region(df, region): +def normalise_by_region(df, region, measure='median'): region_df = df.query((f'region == "{region}"')) if region_df.duplicated(['sub', 'slide', 'section']).any(): raise ValueError('region_df has duplicate keys!') - region_df[f'median_{region}_values'] = df['values'].apply(np.median) + region_df[f'{measure}_{region}_values'] = df['values'].apply(np.median) out = df.merge( region_df, on=['sub', 'slide', 'section'], @@ -80,4 +80,4 @@ def normalise_by_region(df, region): suffixes=('', '_r'), validate='m:1', ) - return out['values'] / out[f'median_{region}_values'] + return out['values'] / out[f'{measure}_{region}_values'] From b1244b285fe35a85eb863b4f59a4ecdceb00d680 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 10 Jun 2025 11:42:04 +0100 Subject: [PATCH 34/38] enh: include normalisation modality --- nmriprep/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nmriprep/utils.py b/nmriprep/utils.py index 393a71a..0f89974 100644 --- a/nmriprep/utils.py +++ b/nmriprep/utils.py @@ -69,10 +69,11 @@ def inverse_rodbard(y, min_, slope, ed50, max_): def normalise_by_region(df, region, measure='median'): + summary_measure = {'mean': np.mean, 'median': np.median}[measure] region_df = df.query((f'region == "{region}"')) if region_df.duplicated(['sub', 'slide', 'section']).any(): raise ValueError('region_df has duplicate keys!') - region_df[f'{measure}_{region}_values'] = df['values'].apply(np.median) + region_df[f'{measure}_{region}_values'] = df['values'].apply(summary_measure) out = df.merge( region_df, on=['sub', 'slide', 'section'], From 80ca06f2c15d431125c8601c36b31e6cd4dbeaaf Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 10 Jun 2025 11:42:27 +0100 Subject: [PATCH 35/38] bug: drop mode from parser --- nmriprep/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmriprep/parser.py b/nmriprep/parser.py index 3ced723..4a32f81 100644 --- a/nmriprep/parser.py +++ b/nmriprep/parser.py @@ -94,7 +94,7 @@ def get_roiextract_parser(): parser.add_argument( '--norm-measure', help='summary statistic for normalisation', - choices=['mean', 'median', 'mode'], + choices=['mean', 'median'], default='median', type=str, ) From f550cf84689db71e0a5f99c10dca19e4cf8f8f16 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Tue, 10 Jun 2025 13:49:40 +0100 Subject: [PATCH 36/38] enh: add logic to separate subject images by film --- nmriprep/argprep/argprep.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index ff30830..943f7e2 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -5,7 +5,7 @@ from ..image import convert_nef_to_grey, save_slice from ..parser import get_argprep_parser from ..plotting import plot_curve, plot_mosaic, plot_single_slice -from ..utils import find_files, inverse_rodbard, rodbard +from ..utils import find_files, inverse_rodbard, rodbard, parse_kv from .calibration import calibrate_standard from .fieldprep import find_fields @@ -158,7 +158,13 @@ def main(): for sub_id in subjects_to_process: sub_files = subdirs[sub_id] - subject_workflow(sub_files, src_dir, out_dir, args, verbose) + if "film" in sub_files[0].stem: + subfilms = defaultdict(list) + [ subfilms[parse_kv(fname.stem)['film']].append(fname) for fname in sub_files ] + for film_idx in subfilms.keys(): + subject_workflow(subfilms[film_idx], src_dir, out_dir, args, verbose) + else: + subject_workflow(sub_files, src_dir, out_dir, args, verbose) if __name__ == '__main__': From 6d4f863f9f37df42fea335af67d12a76b7832c5f Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Wed, 23 Jul 2025 15:56:56 +0100 Subject: [PATCH 37/38] fix: allow lateralised regions for normalisation --- nmriprep/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nmriprep/utils.py b/nmriprep/utils.py index 0f89974..17fcb3f 100644 --- a/nmriprep/utils.py +++ b/nmriprep/utils.py @@ -72,7 +72,15 @@ def normalise_by_region(df, region, measure='median'): summary_measure = {'mean': np.mean, 'median': np.median}[measure] region_df = df.query((f'region == "{region}"')) if region_df.duplicated(['sub', 'slide', 'section']).any(): - raise ValueError('region_df has duplicate keys!') + # hemisphere duplicates are allowed, but those are the only exception + if not region_df.duplicated(['sub', 'slide', 'section', 'hemi']).any(): + region_df = ( + region_df + .groupby(['sub', 'slide', 'section'], as_index=False) + .agg({'values': lambda arrs: np.concatenate(arrs.values)}) + ) + else: + raise ValueError('region_df has duplicate keys!') region_df[f'{measure}_{region}_values'] = df['values'].apply(summary_measure) out = df.merge( region_df, From f6335df2bc4cec35ae1bfc626c4e12cf4dcceb83 Mon Sep 17 00:00:00 2001 From: eilidhmacnicol Date: Wed, 23 Jul 2025 15:59:22 +0100 Subject: [PATCH 38/38] sty: ruff --- nmriprep/argprep/argprep.py | 18 ++++++++++++------ nmriprep/argprep/calibration.py | 32 +++++++++++++++++++++----------- nmriprep/measure.py | 4 +++- nmriprep/utils.py | 8 +++----- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/nmriprep/argprep/argprep.py b/nmriprep/argprep/argprep.py index 943f7e2..5cf57a9 100644 --- a/nmriprep/argprep/argprep.py +++ b/nmriprep/argprep/argprep.py @@ -1,14 +1,16 @@ from collections import defaultdict + import nibabel as nb import numpy as np from ..image import convert_nef_to_grey, save_slice from ..parser import get_argprep_parser from ..plotting import plot_curve, plot_mosaic, plot_single_slice -from ..utils import find_files, inverse_rodbard, rodbard, parse_kv +from ..utils import find_files, inverse_rodbard, parse_kv, rodbard from .calibration import calibrate_standard from .fieldprep import find_fields + def subject_workflow(sub_files, src_dir, out_dir, args, verbose): sub_id = sub_files[0].parent.stem sub_dir = out_dir / sub_id @@ -42,7 +44,7 @@ def subject_workflow(sub_files, src_dir, out_dir, args, verbose): print(f'success! fitting data with parameters: {popt}') # find subject files - slide_files = [fname for fname in sub_files if "slide" in fname.stem] + slide_files = [fname for fname in sub_files if 'slide' in fname.stem] if len(slide_files) < 1: raise FileNotFoundError(f'No slide files found for {sub_id}') @@ -149,8 +151,9 @@ def main(): if args.subject_id: print(f'Processing subjects: {args.subject_id}') subjects_to_process = [ - subj for subj in all_subject_directories - if subj.stem.replace("sub-","") in args.subject_id + subj + for subj in all_subject_directories + if subj.stem.replace('sub-', '') in args.subject_id ] else: print('Processing all subjects') @@ -158,9 +161,12 @@ def main(): for sub_id in subjects_to_process: sub_files = subdirs[sub_id] - if "film" in sub_files[0].stem: + if 'film' in sub_files[0].stem: subfilms = defaultdict(list) - [ subfilms[parse_kv(fname.stem)['film']].append(fname) for fname in sub_files ] + [ + subfilms[parse_kv(fname.stem)['film']].append(fname) + for fname in sub_files + ] for film_idx in subfilms.keys(): subject_workflow(subfilms[film_idx], src_dir, out_dir, args, verbose) else: diff --git a/nmriprep/argprep/calibration.py b/nmriprep/argprep/calibration.py index 2d9e5a0..b4cf042 100644 --- a/nmriprep/argprep/calibration.py +++ b/nmriprep/argprep/calibration.py @@ -15,10 +15,14 @@ def get_image_patch(center_coord, square_apothem: int = 100): def get_dataset_standard(source_dir): if not (source_dir / 'dataset_description.json').exists(): - raise FileNotFoundError(f"Dataset description file missing from {source_dir.resolve()}") + raise FileNotFoundError( + f'Dataset description file missing from {source_dir.resolve()}' + ) with (source_dir / 'dataset_description.json').open('r') as fname: standard_info = json.load(fname)['standard'] - range_type = standard_info['range'] if 'range' in standard_info.keys() else 'default' + range_type = ( + standard_info['range'] if 'range' in standard_info.keys() else 'default' + ) return (standard_info['isotope'], range_type) @@ -108,9 +112,7 @@ def get_standard_value(array, medfilt_radius=40, square_size=900, roi_fig_name=N return np.median(gray[roi]) -def calibrate_standard( - sub_files, src_dir, flatfield_correction=None, out_dir=None -): +def calibrate_standard(sub_files, src_dir, flatfield_correction=None, out_dir=None): from scipy.optimize import curve_fit from .. import data @@ -120,17 +122,25 @@ def calibrate_standard( with importlib.resources.open_text(data, 'standards.json') as f: standard_vals = pd.Series(json.load(f)[isotope][range_type]) - standard_files = [fname for fname in sub_files if f"standard-{isotope}" in fname.stem] + standard_files = [ + fname for fname in sub_files if f'standard-{isotope}' in fname.stem + ] if len(standard_files) < 1: - raise FileNotFoundError(f'No {isotope} standard files found in {sub_files[0].parent}') + raise FileNotFoundError( + f'No {isotope} standard files found in {sub_files[0].parent}' + ) if len(standard_files) != len(standard_vals): # try filtering standard files by range type in case there are two sets of standard images - standard_files = [fname for fname in standard_files if f"range-{range_type}" in fname.stem] + standard_files = [ + fname for fname in standard_files if f'range-{range_type}' in fname.stem + ] if len(standard_files) != len(standard_vals): - raise RuntimeError(f"Number of standard files {len(standard_files)} does not match standard values") + raise RuntimeError( + f'Number of standard files {len(standard_files)} does not match standard values' + ) out_stem = '_'.join( - f'{k}-{v}' for k, v - in parse_kv(standard_files[0].stem).items() + f'{k}-{v}' + for k, v in parse_kv(standard_files[0].stem).items() if 'standard' not in k ) diff --git a/nmriprep/measure.py b/nmriprep/measure.py index 7f1421e..2b034ad 100644 --- a/nmriprep/measure.py +++ b/nmriprep/measure.py @@ -62,7 +62,9 @@ def roi_extract(): merge_keys = [col for col in summary_df.columns if 'values' not in col] for region in args.norm_regions: array_col = f'{region}_values' - main_df[array_col] = normalise_by_region(main_df, region, measure=args.norm_measure) + main_df[array_col] = normalise_by_region( + main_df, region, measure=args.norm_measure + ) summary_df = summary_df.merge( summarise_vals( main_df[merge_keys + [array_col]], diff --git a/nmriprep/utils.py b/nmriprep/utils.py index 17fcb3f..4d5308b 100644 --- a/nmriprep/utils.py +++ b/nmriprep/utils.py @@ -74,11 +74,9 @@ def normalise_by_region(df, region, measure='median'): if region_df.duplicated(['sub', 'slide', 'section']).any(): # hemisphere duplicates are allowed, but those are the only exception if not region_df.duplicated(['sub', 'slide', 'section', 'hemi']).any(): - region_df = ( - region_df - .groupby(['sub', 'slide', 'section'], as_index=False) - .agg({'values': lambda arrs: np.concatenate(arrs.values)}) - ) + region_df = region_df.groupby( + ['sub', 'slide', 'section'], as_index=False + ).agg({'values': lambda arrs: np.concatenate(arrs.values)}) else: raise ValueError('region_df has duplicate keys!') region_df[f'{measure}_{region}_values'] = df['values'].apply(summary_measure)