Skip to content

Commit bb2642e

Browse files
authored
Create unique series description (#275)
added unique SeriesDescription to outputs 4 ezBIDS * Made use of the SeriesDescription field produced by dcm2niix to allow ezBIDS to differentiate between mulitple pet scans for the same subject/session even. The SeriesDescription field previously housed (observed at least) the reconstruction method. Now it gets some PET BIDS entities along with an MD5 hash of the TimeZero, InjectedMass, TracerName, ReconMethodName etc to ensure that a unique and human readable value is recorded in SeriesDescription field; added the same functionality and field to the ecat methods * Sanitized date's from datetime fields injested by the spreadsheet reader when those fields should be Time objects. e.g. TimeZero, MolarActivityMeasureTime etc. * bump version
1 parent 63a7ab0 commit bb2642e

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

pypet2bids/pypet2bids/dcm2niix4pet.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,32 @@ def run_dcm2niix(self):
521521
sidecar_json.update(self.spreadsheet_metadata.get('nifti_json', {}))
522522
sidecar_json.update(self.additional_arguments)
523523

524+
# this is mostly for ezBIDS, but it helps us to make better use of the series description that
525+
# dcm2niix generates by default for PET imaging
526+
collect_these_fields = {
527+
'SeriesDescription': '',
528+
'TracerName': 'trc',
529+
'InjectedRadioactivity': '',
530+
'InjectedRadioactivityUnits': '',
531+
'ReconMethodName': 'rec',
532+
'TimeZero': '',
533+
}
534+
collection_of_fields = {}
535+
for field, entity_string in collect_these_fields.items():
536+
if sidecar_json.get(field):
537+
# if there's a shortened entity string for the field use that
538+
if entity_string != '':
539+
collection_of_fields[entity_string] = sidecar_json.get(field)
540+
else:
541+
collection_of_fields[field] = sidecar_json.get(field)
542+
543+
if self.session_id:
544+
collection_of_fields['ses'] = self.session_id
545+
546+
hash_string = helper_functions.hash_fields(**collection_of_fields)
547+
548+
sidecar_json.update({'SeriesDescription': hash_string})
549+
524550
# if there's a subject id rename the output file to use it
525551
if self.subject_id:
526552
if 'nii.gz' in created_path.name:

pypet2bids/pypet2bids/ecat.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,32 @@ def show_sidecar(self, output_path=None):
363363
"""
364364
self.prune_sidecar()
365365
self.sidecar_template = helper_functions.replace_nones(self.sidecar_template)
366+
367+
# this is mostly for ezBIDS, but it helps us to make better use of the series description that
368+
# dcm2niix generates by default for PET imaging, here we mirror the dcm2niix output for ecats
369+
collect_these_fields = {
370+
'SeriesDescription': '',
371+
'TracerName': 'trc',
372+
'InjectedRadioactivity': '',
373+
'InjectedRadioactivityUnits': '',
374+
'ReconMethodName': 'rec',
375+
'TimeZero': '',
376+
}
377+
collection_of_fields = {}
378+
for field, entity_string in collect_these_fields.items():
379+
if self.sidecar_template.get(field, None):
380+
# if there's a shortened entity string for the field use that
381+
if entity_string != '':
382+
collection_of_fields[entity_string] = self.sidecar_template.get(field)
383+
else:
384+
collection_of_fields[field] = self.sidecar_template.get(field)
385+
386+
if helper_functions.collect_bids_part('ses', self.output_path) != '':
387+
collection_of_fields['ses'] = helper_functions.collect_bids_part('ses', self.output_path)
388+
389+
hash_string = helper_functions.hash_fields(**collection_of_fields)
390+
self.sidecar_template['SeriesDescription'] = hash_string
391+
366392
if output_path:
367393
if not isinstance(output_path, pathlib.Path):
368394
output_path = pathlib.Path(output_path)

pypet2bids/pypet2bids/helper_functions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import shutil
2121
import typing
2222
import json
23+
import hashlib
2324
import warnings
2425
import logging
2526
import dotenv
@@ -979,3 +980,20 @@ def format(self, record):
979980
log_fmt = self.FORMATS.get(record.levelno)
980981
formatter = logging.Formatter(log_fmt)
981982
return formatter.format(record)
983+
984+
985+
def hash_fields(**fields):
986+
hash_return_string = ""
987+
hash_string = ""
988+
keys_we_want = ['ses', 'rec', 'trc']
989+
for key, value in fields.items():
990+
# sanitize values
991+
regex = r"[^a-zA-Z0-9]"
992+
value = re.sub(regex, "", str(value))
993+
hash_string += f"{key}-{value}_"
994+
if key in keys_we_want:
995+
hash_return_string += f"{key}-{value}_"
996+
997+
hash_hex = hashlib.md5(hash_string.encode('utf-8')).hexdigest()
998+
999+
return f"{hash_return_string}{hash_hex}"

pypet2bids/pypet2bids/update_json_pet_file.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dateutil import parser
88
import argparse
99
import pydicom
10+
import datetime
1011
from typing import Union
1112

1213
try:
@@ -584,6 +585,19 @@ def get_metadata_from_spreadsheet(metadata_path: Union[str, Path], image_folder,
584585
dicom_metadata=image_header_dict,
585586
**additional_arguments)
586587

588+
# remove any dates from the spreadsheet time values
589+
for key, value in spreadsheet_values.items():
590+
if 'time' in key.lower():
591+
if isinstance(value, str):
592+
# check to see if the value converts to a datetime object with a date
593+
try:
594+
time_value = parser.parse(value).time().strftime("%H:%M:%S")
595+
spreadsheet_values[key] = time_value
596+
except ValueError:
597+
pass
598+
if isinstance(value, datetime.datetime):
599+
spreadsheet_values[key] = value.time().strftime("%H:%M:%S")
600+
587601
if Path(metadata_path).is_dir() or metadata_path == "":
588602
# we accept folder input as well as no input, in the
589603
# event of no input we search for spreadsheets in the
@@ -621,11 +635,18 @@ def get_metadata_from_spreadsheet(metadata_path: Union[str, Path], image_folder,
621635
# even out the values in the blood tsv columns if they're different lengths by appending zeros to the end
622636
# of each column/list
623637
# determine the longest column
624-
longest_column = max([len(column) for column in spreadsheet_metadata['blood_tsv'].values()])
625-
# iterate over each column, determine how many zeros to append to the end of each column
626-
for column in spreadsheet_metadata['blood_tsv'].keys():
627-
zeros_to_append = longest_column - len(spreadsheet_metadata['blood_tsv'][column])
628-
spreadsheet_metadata['blood_tsv'][column] += [0] * zeros_to_append
638+
column_lengths = [len(column) for column in spreadsheet_metadata['blood_tsv'].values()]
639+
640+
try:
641+
longest_column = max(column_lengths)
642+
except ValueError:
643+
# columns are all the same length or there are no columns
644+
longest_column = None
645+
if longest_column:
646+
# iterate over each column, determine how many zeros to append to the end of each column
647+
for column in spreadsheet_metadata['blood_tsv'].keys():
648+
zeros_to_append = longest_column - len(spreadsheet_metadata['blood_tsv'][column])
649+
spreadsheet_metadata['blood_tsv'][column] += [0] * zeros_to_append
629650

630651
# check for existing blood json values
631652
for column in blood_json_columns:

pypet2bids/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pypet2bids"
3-
version = "1.3.5"
3+
version = "1.3.6"
44
description = "A python implementation of an ECAT to BIDS converter."
55
authors = ["anthony galassi <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)