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

RF/FIX: Scale all phase maps to [0, 2pi] range #88

Merged
merged 2 commits into from
Feb 15, 2020

Conversation

effigies
Copy link
Member

@effigies effigies commented Feb 13, 2020

Final decision: Instead of making [-pi, pi] a special case, all data will be naively scaled to [0, 2pi]. This ensures that the source data intervals are respected, but may result in a central mode slightly off of pi.

We judge that the simplicity and consistency outweighs any small adjustments and potential for bugs resulting from a mode-based transform with separate treatment of the left and right tail.


I have some Philips Achieva 2-echo phase/magnitude fieldmaps that come out of dcm2niix with a range of approximately (-pi, pi).

This adjusts au2rads to detect that case and simply wraps the negative values to (pi, 2pi).

This may resolve #83.

cc @ins0mniac2

data -= mode(data, axis=None)[0][0]
if np.allclose((data.min(), data.max()), (-np.pi, np.pi), atol=0.01):
# Already in rads, but wrap to 0 - 2pi
data[data < 0] += 2 * np.pi
Copy link
Member

@oesteban oesteban Feb 13, 2020

Choose a reason for hiding this comment

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

Why only negatives?

Suggested change
data[data < 0] += 2 * np.pi
data -= data.min()

Copy link
Member Author

Choose a reason for hiding this comment

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

Because this function sets the range to (0, 2pi), not (pi, 3pi). And it seems unwise to go precisely counter-phase by adding np.pi to everything.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I had the suggestion above sign-flipped. I've fixed it.

This is going to wrap and scale only negatives - you don't want either (and even less only for the lower half of the histogram).

If I understand correctly, the initial range goes from -pi to pi. Adding pi will move the range to 0-2pi

Copy link
Member Author

Choose a reason for hiding this comment

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

So does fieldmap correction treat (θ±π) the same? Shifting by half phase feels problematic.

Copy link
Member Author

Choose a reason for hiding this comment

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

To be clear, this is equivalent to:

data = (data + 2 * np.pi) % (2 * np.pi)

But without bothering add, divide and round things in the (0, pi) range. It doesn't feel controversial to me. data += np.pi does.

Copy link
Member

@oesteban oesteban Feb 13, 2020

Choose a reason for hiding this comment

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

So does fieldmap correction treat (θ±π) the same? Shifting by half phase feels problematic.

FSL FUGUE theoretically only unwraps phase maps in the (0, 2π) range. It seems to work with (-π, π), though.

I don't know why data -= np.pi is controversial - it just moves all values from (-π, π) to (0, 2π).

The transformation you propose will wrap negative phase values (which you definitely don't want to do).

Normally, a phase maps' histogram looks like a centered pyramid. Doing this, we are selecting the left side of the pyramid, shifting it to overlap the right side of the pyramid and finally checking no values have gone over 2pi. That will introduce another wrapping that FUGUE might or might not pick up.

EDIT: The shifted intensities will not overlap, now that I look into this more carefully, but the final distribution will be like a V, with the notch at pi.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's talk in the meeting.

# Scale upper tail
data[data > 0] = np.pi * data[data > 0] / data[data > 0].max()
# Scale upper tail
data[data > 0] = np.pi * data[data > 0] / data[data > 0].max()
Copy link
Member

@oesteban oesteban Feb 13, 2020

Choose a reason for hiding this comment

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

This (the old code) looks problematic - we shift the left side of the pyramid first, hence making it overlap with the right hand side. Operations should be done the other way around.

Okay, sorry for the noise. There is no shifting yet.

@effigies
Copy link
Member Author

Here's my histogram: Figure_1

@effigies
Copy link
Member Author

Results of live discussion:

Replace mode-centering and separately rescaling positive and negative to simply scaling min/max to 0/2pi. This would affect fieldmaps from all scanners.

@mattcieslak @Aksoo Your input here would be appreciated.

Copy link
Member Author

@effigies effigies left a comment

Choose a reason for hiding this comment

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

Just a couple explainers before you get to asking. :-)

@@ -663,28 +663,19 @@ def au2rads(in_file, newpath=None):
"""Convert the input phase difference map in arbitrary units (a.u.) to rads."""
from scipy.stats import mode
im = nb.load(in_file)
data = im.get_fdata(dtype='float32')
data = im.get_fdata(caching='unchanged') # Read as float64 for safety
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm extremely comfortable using double memory for one volume to keep things deterministic.


hdr.set_data_dtype(np.float32)
hdr.set_xyzt_units('mm')
out_file = fname_presuffix(in_file, suffix='_rads', newpath=newpath)
nb.Nifti1Image(data, im.affine, hdr).to_filename(out_file)
nb.Nifti1Image(data, None, hdr).to_filename(out_file)
Copy link
Member Author

Choose a reason for hiding this comment

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

If you're keeping the affines in the header, there's no need to pass an affine in to override.

sdcflows/interfaces/fmap.py Show resolved Hide resolved
@kimsin98
Copy link
Contributor

Replace mode-centering and separately rescaling positive and negative to simply scaling min/max to 0/2pi. This would affect fieldmaps from all scanners.

I also had doubts about mode centering. This looks simpler and cleaner.

@effigies
Copy link
Member Author

@mgxd I see the CI has gotten clogged. Naturally.

If it makes life easier, I can cancel this and master and just merge this to only have one job in queue.

@mgxd
Copy link
Contributor

mgxd commented Feb 14, 2020

Doesn't the current master CI needs to pass to upload the new version to PyPI?

I'm fine with cutting corners, but would like to see a green check here prior to merging just for my peace of mind

@effigies
Copy link
Member Author

Okay. I'll leave this one up. Still cancelled master since I rebased this on master.

We'll push to PyPI with the tag: https://circleci.com/workflow-run/52c4f144-141b-41ed-acad-e218e1b65f2a

@effigies effigies changed the title FIX: Adjust au2rads to handle input data with 2pi range RF/FIX: Scale all phase maps to [0, 2pi] range Feb 15, 2020
@pull-assistant
Copy link

Score: 0.29

Best reviewed: with all changes


Optimal code review plan (2 commits squashed)

FIX: Adjust au2rads ... ... RF: Always simply re...

Squashed 2 commits:

Powered by Pull Assistant. Last update ee0fe99 ... 410365a. Read the comment docs.

@effigies effigies merged commit 28b084f into nipreps:master Feb 15, 2020
@effigies effigies deleted the fix/rads2rads branch February 15, 2020 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Invalid transformations for some Phase1/Phase2 fieldmaps
4 participants