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

Fixes and improvements (by mtnhuck) #13

Merged
merged 16 commits into from
Jun 26, 2019
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
22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
language: python

cache:
directories:
- $HOME/.cache/pip

python:
- 2.7
- 3.5
- 3.6
- 3.7

install:
- travis_retry pip install -e .
- travis_retry pip install pytest pytest-cov coverage

script:
- python -m pytest --cov=bids2nda -s -v bids2nda

after_success:
- travis_retry pip install codecov
- codecov
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Extract NIHM Data Archive compatible metadata from Brain Imaging Data Structure

optional arguments:
-h, --help show this help message and exit


## GUID_MAPPING file format
The is the file format produced by the GUID Tool: one line per subject in the format
Expand All @@ -32,3 +32,7 @@ The is the file format produced by the GUID Tool: one line per subject in the fo

## Example outputs
See [/examples](/examples)

## Notes:
Column experiment_id must be manually filled in for now.
This is based on experiment ID's received from NDA after setting the study up through the NDA website [here](https://ndar.nih.gov/user/dashboard/collections.html).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am I reading it right @mtnhuck that it is pretty much yet another "mapping" but now for the task/runs? unfortunately that url requires a login so can't assess. Could you please give an example?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a number that comes from the NDA archive, ie they provide you with a number for your experiment. I'm still waiting to hear back from them if it is at the session level or the scan level (ie would resting-state need a different experiment number than a face-perception task?). An example number which I generated on their website is 867.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's wait for them to reply and then make it an option or mapping

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the .gov ppl, experiment_id is per "experiment within the study" so resting state would get one and face-perception would get another.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As agreed later on, this is the id for user to provide. We have a preliminary patch ready to submit (will be done after this PR is merged) to provide the mapping from BIDS task id to experiment_id via a simple .tsv file.

65 changes: 64 additions & 1 deletion bids2nda/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import nibabel as nb
import json
import pandas as pd
import numpy as np


# Gather our code in a main() function
Expand Down Expand Up @@ -74,6 +75,55 @@ def dict_append(d, key, value):
d[key] = [value, ]


def cosine_to_orientation(iop):
"""Deduce slicing from cosines

From http://nipy.org/nibabel/dicom/dicom_orientation.html#dicom-voxel-to
-patient-coordinate-system-mapping

From Section C.7.6.1.1.1 we see that the "positive row axis" is left to
right, and is the direction of the rows, given by the direction of last
pixel in the first row from the first pixel in that row. Similarly the
"positive column axis" is top to bottom and is the direction of the columns,
given by the direction of the last pixel in the first column from the first
pixel in that column.

Let's rephrase: the first three values of "Image Orientation Patient" are
the direction cosine for the "positive row axis". That is, they express the
direction change in (x, y, z), in the DICOM patient coordinate system
(DPCS), as you move along the row. That is, as you move from one column to
the next. That is, as the column array index changes. Similarly, the second
triplet of values of "Image Orientation Patient" (img_ornt_pat[3:] in
Python), are the direction cosine for the "positive column axis", and
express the direction you move, in the DPCS, as you move from row to row,
and therefore as the row index changes.

Parameters
----------
iop: list of float
Values of the ImageOrientationPatient field

Returns
-------
{'Axial', 'Coronal', 'Sagittal'}
"""
# Solution based on https://stackoverflow.com/a/45469577
iop_round = np.round(iop)
plane = np.cross(iop_round[0:3], iop_round[3:6])
plane = np.abs(plane)
if plane[0] == 1:
return "Sagittal"
elif plane[1] == 1:
return "Coronal"
elif plane[2] == 1:
return "Axial"
else:
raise RuntimeError(
"Could not deduce the image orientation of %r. 'plane' value is %r"
% (iop, plane)
)


def run(args):

guid_mapping = dict([line.split(" - ") for line in open(args.guid_mapping).read().split("\n") if line != ''])
Expand Down Expand Up @@ -155,18 +205,29 @@ def run(args):
else:
description = suffix
dict_append(image03_dict, 'experiment_id', '')
# Shortcut for the global.const section -- apparently might not be flattened fully
metadata_const = metadata.get('global', {}).get('const', {})
dict_append(image03_dict, 'image_description', description)
dict_append(image03_dict, 'scan_type', suffix_to_scan_type[suffix])
dict_append(image03_dict, 'scan_object', "Live")
dict_append(image03_dict, 'image_file_format', "NIFTI")
dict_append(image03_dict, 'image_modality', "MRI")
dict_append(image03_dict, 'scanner_manufacturer_pd', metadata.get("Manufacturer", ""))
dict_append(image03_dict, 'scanner_type_pd', metadata.get("ManufacturersModelName", ""))
dict_append(image03_dict, 'scanner_software_versions_pd', metadata.get("HardcopyDeviceSoftwareVersion", ""))
dict_append(image03_dict, 'scanner_software_versions_pd', metadata.get("SoftwareVersions", ""))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 -- I do not see the HardcopyDeviceSoftwareVersion present anywhere.
If they were interested in all "versions", then we could also extend it with ConversionSoftwareVersion but since they do not care -- "better" for us, worse for reproducibility ;)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one is settled

dict_append(image03_dict, 'magnetic_field_strength', metadata.get("MagneticFieldStrength", ""))
dict_append(image03_dict, 'mri_echo_time_pd', metadata.get("EchoTime", ""))
dict_append(image03_dict, 'flip_angle', metadata.get("FlipAngle", ""))
dict_append(image03_dict, 'receive_coil', metadata.get("ReceiveCoilName", ""))
# ImageOrientationPatientDICOM is populated by recent dcm2niix,
# and ImageOrientationPatient might be provided by exhastive metadata
# record done by heudiconv
iop = metadata.get(
'ImageOrientationPatientDICOM',
metadata_const.get("ImageOrientationPatient", None)
)
dict_append(image03_dict, 'image_orientation', cosine_to_orientation(iop) if iop else '')

dict_append(image03_dict, 'transformation_performed', 'Yes')
dict_append(image03_dict, 'transformation_type', 'BIDS2NDA')

Expand Down Expand Up @@ -194,6 +255,8 @@ def run(args):
dict_append(image03_dict, 'image_resolution1', nii.header.get_zooms()[0])
dict_append(image03_dict, 'image_resolution2', nii.header.get_zooms()[1])
dict_append(image03_dict, 'image_resolution3', nii.header.get_zooms()[2])
dict_append(image03_dict, 'image_slice_thickness', metadata_const.get("SliceThickness", nii.header.get_zooms()[2]))
dict_append(image03_dict, 'photomet_interpret', metadata.get("global",{}).get("const",{}).get("PhotometricInterpretation",""))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This beast I am not sure even where to get from. In our use-case we just adjusted the code to provide 'NA' as a default value.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pinged the NIH help desk for some guidance on the interpretation of this field. Will report back.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

if len(nii.shape) > 3:
image_resolution4 = nii.header.get_zooms()[3]
else:
Expand Down
Empty file added bids2nda/tests/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions bids2nda/tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from ..main import cosine_to_orientation


def test_cosine_to_orientation():
assert cosine_to_orientation([0.9, -0.03, -0.1, 0.03, 0.9, 0.1]) == 'Axial'
assert cosine_to_orientation([0, 0.9, 0.1, 0.03, 0.1, -0.9]) == 'Sagittal'


4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@

# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages.
packages=["bids2nda"],
packages=["bids2nda",
"bids2nda.tests",
],

# List run-time dependencies here. These will be installed by pip when your
# project is installed.
Expand Down