-
Notifications
You must be signed in to change notification settings - Fork 261
NF: Conformation function and CLI tool to apply shape, orientation and zooms #853
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
Conversation
Codecov Report
@@ Coverage Diff @@
## master #853 +/- ##
==========================================
+ Coverage 91.70% 91.73% +0.03%
==========================================
Files 96 97 +1
Lines 12311 12359 +48
Branches 2173 2177 +4
==========================================
+ Hits 11290 11338 +48
Misses 684 684
Partials 337 337
Continue to review full report at Codecov.
|
ready for review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor comments.
Is actual (command line) "command" interface coming in this PR or later?
nibabel/processing.py
Outdated
""" | ||
x = np.asarray(x) | ||
x_min, x_max = x.min(), x.max() | ||
return (x - x_min) * (new_max - new_min) / (x_max - x_min) + new_min |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should it become resilient to x_max == x_min
when it would either set it to new_min
or blow some better than ZeroDivisonError exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you @yarikoptic - i will account for x_max == x_min
and i will add a test for this case.
nibabel/tests/test_processing.py
Outdated
assert c.dataobj.dtype == np.dtype(np.uint8) | ||
assert aff2axcodes(c.affine) == ('R', 'A', 'S') | ||
|
||
# Error on non-3D images. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that what freesurfer does? I thought it might be nice to have this one applicable to 4D (or 5D - AFNI etc) series, to "conform" the first 3 dimensions to desired resolution/orientation etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think it is what freesurfer does, but for simplicity and due to my lack of knowledge, i made it this way. how should the output shape and zooms be changed to support 4- and 5-D images?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output shape and zooms would not be affected by rolling the first three axes.
In any event, I'm okay with getting this in and adding 4/5D image support later, but if we're going to have a test, then we should mark it as one that we intend to relax. Otherwise it may look like a desired constraint to future developers.
@yarikoptic - it could come in this pr. is there a preferred entrypoint? |
So you're already not doing exactly what FreeSurfer does, so I don't think you need to stick too close to what it does. I think you obviously want to be able to specify dimensions, zooms and orientation from the CLI. I would suggest that the dtype and rescaling might also be useful parameters. For rescaling, I would make it a boolean flag to indicate that the data should be rescaled to the full range of the dtype (True) or preserve values and adjusting scale parameters as needed (False). Anyway, I haven't reviewed the code yet, but figured I might chime in about capabilities. |
this makes sense for integer types, but i don't think it should be done for float types. or perhaps we could make things much easier and not cast the data. |
I agree that rescaling into the range for floats doesn't make sense. |
Hi @kaczmarj just checking in on this. Do you want to aim for a 3.1 release (soon-ish...) or 3.2? |
hi @effigies - thanks for the ping. i will work on this tonight and will update. i aim for 3.1. |
Hello @kaczmarj, Thank you for updating! Cheers! There are no style issues detected in this Pull Request. 🍻 To test for issues locally, Comment last updated at 2020-04-15 20:35:57 UTC |
@effigies - i added the command-line program |
RF: Update conformation to reorient, rescale and resample
Thanks @effigies. As far as I'm concerned, this PR should be good for final review. When merging, can you squash and merge? There are some commits in here are irrelevant and include incorrect code. |
Will review tomorrow or Friday. Happy to squash, if that's what you prefer. |
LGTM. @yarikoptic What do you think? |
Bump @yarikoptic, in case this notification escaped, too. |
@kaczmarj Sorry, forgot to squash. I think we'll survive a little mess. |
@kaczmarj just to flag a gotcha for the freesurfer conform code... they seem to do image histogram normalization (shift values to center white matter somewhere around 100) and I believe they truncate to uint8 unless told not to do so. We got tripped up by that in attempting an identical but all python implementation. @patrick-mcclure may have some input on details of how to resolve it (though we may have just stuck with the freesurfer output?). It might be worth taking into account if this is for neural nets. |
Ok, thanks, good to know. And was the diagnosis that it was primarily the histogram normalization or were there other hypotheses? |
Thanks @leej3 and @patrick-mcclure. You are right that this does not perform histogram normalization or truncating to uint8. I think not truncating to uint8 should be fine, because that truncation will lose precision anyway. I'd be curious to hear if you think histogram normalization had a beneficial effect on the model performance. |
Well you certainly can't mix and match:train on mri_convert output and predict on output that was conformed without normalization. My memory was that the normalization performed by freesurfer was important for heterogeneous datasets, i.e. it was essential for any broadly usable tool. Correct me if I'm wrong @patrick-mcclure. |
@leej3 - is this done during |
From what I remember, training using normalized volumes and then testing on
non-normalized volumes lead to a significant decrease in test performance
compared to testing using the same volumes with normalization.
…On Mon, Apr 20, 2020 at 9:51 AM john lee ***@***.***> wrote:
Well you certainly can't mix and match:train on mri_convert output and
predict on output that was conformed without normalization. My memory was
that the normalization performed by freesurfer was important for
heterogeneous datasets, i.e. it was essential for any broadly usable tool.
Correct me if I'm wrong @patrick-mcclure
<https://github.com/patrick-mcclure>.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#853 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACPK4HSNKQ7MZK7D54LAUR3RNRHPDANCNFSM4J2AI2LA>
.
|
@satra @leej3 - it looks like the histogram normalization happens during the conversion to uint8: https://github.com/freesurfer/freesurfer/blob/dev/mri_convert/mri_make_uchar.cpp from the comments at the top of that file:
|
@patrick-mcclure and @leej3 - i think there are two things getting mixed up here.
let's keep 2 out of this discussion, since that relates to preprocessing for models that nibabel could not care less about. |
@kaczmarj - i would still like to know how mri_convert can determine where this ball is without doing any registration. i can conform a blank mri scan if i want to. |
The mri_make_uchar tool compiled from the code @kaczmarj referenced is in fact run later, I believe here in recon-all. It's coming back to me now. I think I had gotten confused by the output naming and assumed orig.mgz was the output of the first step of the pipeline with just a basic conform performed but it is in fact following the uint8 conversion (which does that normalization). So summary is that mri_convert is indeed just performing a format conversion and with the -conform option it additionally performs operations equivalent to the ones here. Agreed, 2. is another problem |
to add onto that @leej3 ,
|
Thanks for detailing that. |
@kaczmarj - perhaps why it needs to create a histogram is relevant to implementing 3. since the basic scaling can be implemented just with nibabel's changing datatype function, which does the scaling on the range of the data. |
@satra - i am still trying to figure that out. i might need to translate the c++ code to python/numpy to give a good answer. The linear transformation is:
where other values are as follows:
the |
i implemented relevant parts of the histogram is used to modify the denominator of the scale function. more specifically, the histogram is used to update import numpy as np
def change_type(x, f_low=0.0, f_high=0.999):
# The defaults come from `mri_convert.c`
x = np.asarray(x)
N_HIST_BINS = 1000
hist_bins = [0] * N_HIST_BINS
src_min, src_max = x.min(), x.max()
dest_min, dest_max = 0, 255 # for uint8
bin_size = (src_max - src_min) / float(N_HIST_BINS)
bin_threshold = N_HIST_BINS / 5.0
# for in vivo
bin_threshold = 10
DZERO = lambda x: abs(float(x)) < 1e-15
nonzero = 0
for val in x.ravel():
if not DZERO(val):
nonzero += 1
bin_ = int((val - src_min) / bin_size)
if bin_ < 0:
bin_ = 0
if bin_ > N_HIST_BINS:
bin_ = N_HIST_BINS - 1
hist_bins[bin_] += 1
# Modify `src_min`
nth = int(f_low * x.size)
n_passed = 0
bin_ = 0
while (n_passed < nth) and (bin_ < N_HIST_BINS):
n_passed += hist_bins[bin_]
bin_ += 1
src_min = float(bin_ * bin_size + src_min)
# Modify `src_max`
nth = int((1.0 - f_high) * nonzero)
n_passed = 0
bin_ = N_HIST_BINS - 1
while (n_passed < nth) and bin_ > 0:
n_passed += hist_bins[bin_]
bin_ -= 1
src_max = float(bin_ * bin_size + src_min)
assert src_max > src_min
scale = (dest_max - dest_min) / (src_max - src_min)
xnew = scale * (x - src_min) + dest_min
# This is not part of this function, but somewhere, there must be a
# clipping operation, such as
# xnew.clip(dest_min, dest_max)
return xnew |
@kaczmarj - this looks like a straight up and it seems so should be easily reimplementable with |
1 similar comment
@kaczmarj - this looks like a straight up and it seems so should be easily reimplementable with |
closes #851
related to neuronets/kwyk#5
This PR adds a function to resample a volume to arbitrary shape and voxel sizes. This is meant to be a close replica of freesurfer's
mri_convert --conform
command. In freesurfer, the image is reoriented to LIA, but in this command, the image is reoriented to RAS.So this command:
The heavy lifting is done by
nibabel.processing.resample_from_to()
.I have not added tests yet.