Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enh: Anonymisation at runtime #113

Merged
merged 2 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
This document contains the Spec2nii release history in reverse chronological order.

0.7.1 (WIP)
--------------------------------
- The --anon flag can be passed with any call to anonymise after writing files.

0.7.0 (Saturday 5th August 2023)
--------------------------------
- Fixed a bug in Philips Classic DICOM orientations (supplementing the fixes to Enhanced DICOM in `0.6.11`)
Expand Down
44 changes: 30 additions & 14 deletions spec2nii/anonymise.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
from nifti_mrs.hdr_ext import Hdr_Ext


def anon_nifti_mrs(args):
"""Function for anonymising input NIfTI-MRS files.
def anon_nifti_mrs(nifti_mrs_img, extra_keys=None, verbose=False):
"""Function for anonymising an existing nifti-mrs object

:param args: Command line arguments parsed in spec2nii.py
:return: List of anonymised images
:rtype: [nib.nifti2.Nifti2Image,]
:return: List of output names
:rtype: [str,]
:param nifti_mrs_img: NIfTI-MRS object to anonymise
:type nifti_mrs_img: nifti_mrs.nifti_mrs.NIFTI_MRS
:param extra_keys: List of keys to explicitly remove, defaults to None
:type extra_keys: List, optional
:param verbose: Print verbose output, defaults to False
:type verbose: bool, optional
:return: Anonymised NIfTI-MRS object
:rtype: nifti_mrs.nifti_mrs.NIFTI_MRS
"""
# Load data
nifti_mrs_img = NIFTI_MRS(args.file)

# Extract header extension
hdr_ext = nifti_mrs_img.hdr_ext.to_dict()
Expand All @@ -38,8 +39,8 @@ def iter_json(in_dict):
removed = {}
for key, value in in_dict.items():
# Explicitly set on command line
if args.remove\
and key in args.remove:
if extra_keys\
and key in extra_keys:
removed.update({key: value})
# Standard defined
elif key in standard_defined:
Expand All @@ -65,24 +66,39 @@ def iter_json(in_dict):

anon_hdr, removed_dict = iter_json(hdr_ext)

if args.verbose:
if verbose:
pp = pprint.PrettyPrinter(indent=4)
print('\nThe following keys were removed:')
pp.pprint(removed_dict)
print('\nThe following keys were kept:')
pp.pprint(anon_hdr)

# Make new NIfTI-MRS image
anon_out = gen_nifti_mrs_hdr_ext(
return gen_nifti_mrs_hdr_ext(
nifti_mrs_img[:],
nifti_mrs_img.dwelltime,
Hdr_Ext.from_header_ext(anon_hdr),
affine=nifti_mrs_img.getAffine('voxel', 'world'))


def anon_file(args):
"""Function for anonymising NIfTI-MRS files stored on drive.

:param args: Command line arguments parsed in spec2nii.py
:return: List of anonymised images
:rtype: [nib.nifti2.Nifti2Image,]
:return: List of output names
:rtype: [str,]
"""

# Process output name.
if args.fileout:
fname_out = [args.fileout, ]
else:
fname_out = [args.file.with_suffix('').with_suffix('').name, ]

return [anon_out, ], fname_out
return [
anon_nifti_mrs(
NIFTI_MRS(args.file),
verbose=args.verbose,
extra_keys=args.remove), ], fname_out
21 changes: 17 additions & 4 deletions spec2nii/spec2nii.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ def add_common_parameters(subparser):
help="Override SpectrometerFrequency field with input(s). Input in MHz.")
subparser.add_argument("--override_dwelltime", type=float,
help="Override dwell time field with input. Input in seconds.")
subparser.add_argument(
'--anon',
action='store_true',
help="Create file without sensitive metadata. For greater control use spec2nii anon.")
subparser.add_argument('--verbose', action='store_true')

return subparser

# Auto subcommand - heuristic ID of file type
Expand Down Expand Up @@ -234,7 +239,8 @@ def add_common_parameters(subparser):
nifti1=False,
override_nucleus=None,
override_frequency=None,
override_dwelltime=None)
override_dwelltime=None,
anon=False)

parser_dump = subparsers.add_parser('dump', help='Dump contents of headers from existing NIfTI-MRS file.')
parser_dump.add_argument('file', help='NIfTI-MRS file', type=Path)
Expand Down Expand Up @@ -264,7 +270,8 @@ def add_common_parameters(subparser):
override_nucleus=None,
override_frequency=None,
override_dwelltime=None,
verbose=False)
verbose=False,
anon=False)

if len(sys.argv) == 1:
parser.print_usage(sys.stderr)
Expand All @@ -284,6 +291,12 @@ def add_common_parameters(subparser):

if self.imageOut:
self.implement_overrides(args)

if args.anon:
from spec2nii.anonymise import anon_nifti_mrs
for idx, nifti_mrs_img in enumerate(self.imageOut):
self.imageOut[idx] = anon_nifti_mrs(nifti_mrs_img, verbose=args.verbose)

self.validate_output()
self.write_output(args.json, args.nifti1)
self.validate_write(args.verbose)
Expand Down Expand Up @@ -641,8 +654,8 @@ def varian(self, args):

# Anonymise function
def anon(self, args):
from spec2nii.anonymise import anon_nifti_mrs
self.imageOut, self.fileoutNames = anon_nifti_mrs(args)
from spec2nii.anonymise import anon_file
self.imageOut, self.fileoutNames = anon_file(args)

# Dump function
@staticmethod
Expand Down
23 changes: 23 additions & 0 deletions tests/test_anon.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,26 @@ def test_anon(tmp_path):
assert 'PatientName' not in hdr_ext_a
assert 'PatientSex' not in hdr_ext_a
assert 'RepetitionTime' not in hdr_ext_a


def test_inline_anon(tmp_path):
# Convert twix
subprocess.check_call(['spec2nii', 'twix',
'-e', 'image',
'-f', 'original',
'-o', tmp_path,
'-j', str(data_path),
'--anon',
'--verbose'])

img_o = read_nifti_mrs(tmp_path / 'original.nii.gz')

hdr_ext_codes = img_o.header.extensions.get_codes()
hdr_ext_o = json.loads(img_o.header.extensions[hdr_ext_codes.index(44)].get_content())

assert 'SpectrometerFrequency' in hdr_ext_o
assert 'ResonantNucleus' in hdr_ext_o
assert 'dim_5' in hdr_ext_o
assert 'OriginalFile' not in hdr_ext_o
assert 'PatientName' not in hdr_ext_o
assert 'PatientDoB' not in hdr_ext_o
Loading