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
IVIM #1058
IVIM #1058
Conversation
@RafaelNH @arokem This is a very basic fitting using scipy's nonlinear curve_fit(). I haven't tried two stage fitting (higher and lower bvalues) and tried to keep this PR short. This notebook gives the result for different sets of simulated data and also for one of dipy's dataset. The generated data fits well but dipy's dataset gives poor fitting (perhaps due to very hight bvalues). I will look into Eric's dataset and try again. Till then, comments and suggestions are welcome. :-) |
mevals = np.array(([D_star, D_star, D_star], [D, D, D])) | ||
# This gives an isotropic signal | ||
|
||
signal = multi_tensor(gtab, mevals, snr=None, fractions=[f, 100 - f]) |
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 think you want f to be between 0 and 1, at least that's how you defined it in ivim_function. That may be why you're getting overflow errors in the fitting. Not that 0 to 100 is wrong, it just needs to be consistent.
Also, f (perfusion fraction) should be around 10%, not 90%. It should fit it fine but it's just not the typical brain composition to have 90% of the signal flowing and 10% stationary.
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.
Ah, yes. I am changing it to stay consistent. In multi_tensor it is taken as 0 - 100. Should I follow that or use 0 - 1 for "f" and while passing to multi_tensor do a fractions = [100*f, 100*(1-f)]
@arokem. What is dipy's take on this ?
@etpeterson Thanks for pointing it out. But even after changing it, the overflow errors persist. Can it be because the data and bvalues not what it is supposed to be for this model. (I am using 'small_101D', I 'll take a look at the data Eric posted)
I think we might need tests to determine the range of f, D_star and D such that results are valid while using this model. Incorrect parameters give a very bad fit.
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.
Looks like dipy may be a little conflicted also. Single_tensor uses S0=1 and multi_tensor uses S0=100, though others seem to prefer the 100 area.
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.
Fixes to make things more consistent would be most welcome!
On Thu, May 26, 2016 at 9:18 AM, Eric Peterson notifications@github.com
wrote:
In dipy/reconst/tests/test_ivim.py
#1058 (comment):
+def test_nlls_fit():
- """
- Test the implementation of NLLS
- """
- fimg, fbvals, fbvecs = get_data('small_101D')
- bvals, bvecs = read_bvals_bvecs(fbvals, fbvecs)
- gtab = gradient_table(bvals, bvecs)
- f, D_star, D = 90, 0.0015, 0.0008
- mevals = np.array(([D_star, D_star, D_star], [D, D, D]))
This gives an isotropic signal
- signal = multi_tensor(gtab, mevals, snr=None, fractions=[f, 100 - f])
Looks like dipy may be a little conflicted also. Single_tensor uses S0=1
and multi_tensor uses S0=100, though others seem to prefer the 100 area.—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/nipy/dipy/pull/1058/files/951c19fe208c5d54250cf7088c16b962fad29d51#r64775504
Looks like a good start. Just a few comments beyond the notes above. The single stage fitting should be fine for noiseless data (I believe) so don't worry about not having the 2-stage yet. The noiseless tests should fit exactly (or very close) so especially in the 2nd noiseless test the fit isn't right. On a side note you may want to print out or add to the plots the true and fitted values so you can see just how close all the numbers are. And finally, try using the b-value and b-vector set the Federau, LeBihan, or I used with more b-values below 300 and maxing out at around 1000, it's more suited to these D and D_star values you're using. |
flat_data = data.reshape((-1, data.shape[-1])) | ||
# Flatten for the iteration over voxels: | ||
bvals = gtab.bvals | ||
ivim_params = np.empty((flat_data.shape[0], 4)) |
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.
It looks like you're letting curve_fit use ones for the initial guess. The initial guess should be reasonable values, like approximate values from the a paper for example. It also would be nice for the user to send these in if they choose. This could also be why you're getting overflows.
@etpeterson Updated Ipython notebook with the plots and various values mentioned. Plots and values look much better and give very less error. Funny thing happening though. Trying to figure that out (perhaps something with curve fit or my function) but otherwise, this looks okay and is working as expected. I guess the next step would be to use the data you gave and test some more or try a two-stage fitting. |
Those noiseless fits look good. That error is on the scale of the accuracy of the nonlinear fit so I'd say it's working. The swapping is probably just because the initial guess is [1,1,1,1] so the algorithm has no way of knowing which should be D and which should be D* so it just picks one. A final check could be if D*>D and if not swap them and recalculate f just to make sure you return physically reasonable values. Also, the 2 stage fitting would probably fix this as well because in a way it finds close to accurate guesses. |
@@ -147,7 +146,8 @@ def nlls_fit(data, gtab, jac=False): | |||
bvals = gtab.bvals | |||
ivim_params = np.empty((flat_data.shape[0], 4)) | |||
for vox in range(flat_data.shape[0]): | |||
popt, pcov = curve_fit(ivim_function, bvals, flat_data[vox]) | |||
popt, pcov = curve_fit(ivim_function, bvals, flat_data[vox], |
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.
Oh I see you added a guess but your D and D* guesses are still the same here which may be why they're getting swapped.
Looks like you are setting the initial value, sorry. See my inline comment regarding that. One other thing, I'd report some kind of norm (L1 or L2 probably) as the error (numpy.linalg.norm), just easier and currently you could get 0 error from positives and negatives cancelling. |
ivim_params = np.empty((flat_data.shape[0], 4)) | ||
for vox in range(flat_data.shape[0]): | ||
popt, pcov = curve_fit(ivim_function, bvals, flat_data[vox], | ||
p0=[1.0, 0.10, 0.001, 0.001]) |
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.
Can we please pass p0
as an input to this function as a key-word argument (and allow users to initialize this through the IvimModel __init__
, again as a kwarg with these values as defaults)
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.
@arokem The dimension of the initial guess vector [S, f, D_star, D] for the parameters should be the same as the number of voxels ?
But, I guess most of the times the user will just give one set of guesses for all the voxels. The code can check for this and pass the p0 array as p0[voxel] if a set of initial guesses are provided. Does it make sense to keep that provision ? @etpeterson
For now, I am having a keyword argument in the fit method and using the same guess for all voxels.
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 think allowing for a single voxel and a whole array (image) would be good here. You're right that for the one stage fit a single set of guesses is what you'd expect but when you implement the two part fitting you'll probably be sending in an array of initial guesses so it would be good to have that capability.
I just gave a look to the code! Did you try to use a procedure similar to the one suggested by Bihan et al., (1988)? -i.e., fitting first the data with b-value > 200 using the monoexponential fit, then estimate the f-value. This will be a nice initial estimates for your biexponential fit, and might improve the robustness of the estimates with noise. Federau et al., 2012 followed a similar approach. |
@arokem @rafael @etpeterson I have updated and implemented all the changes we discussed. The test coverage is coming out to be 99%. |
Updated blog with the example |
I ran the IVIM code and it's looking like it's failing a lot. The pic is from the f parameter and is scaled with black at 0 and white at 0.5. In my experience f should be fairly clean. I'm running it again now with a mask (which I forgot to use last time). The rough code is below. Am I missing something here? ivimmodel = IVIMModel(gtab)
ivimfit = ivimmdel.fit(img,mask)
ivimparams = ivimfit.model_params |
Just to be sure that I understand the failure mode: those values of 'f' On Wed, Aug 10, 2016 at 1:57 AM, Eric Peterson notifications@github.com
|
Yup! I was getting the same f profiles with the example data! @sahmed95 can you show us the f maps before the non-linear convergence? |
Mostly just much smoother. Sorry I didn't give much info there. Here's a side by side of what my code is generating (right) vs this PR (left). Not that mine is the gold standard by any means but just that the CSF regions have higher f values relative to the tissue and the values seem to vary more naturally. But on a side note, this PR is about 5x faster than mine to run, this whole brain fitting was 87 minutes, so that's good. I can probably re-run it and save out the linear fitting as well, that's a good diagnosis. Also perhaps the nonlinear fitting status. @sahmed95 what voxel coordinates were you using for testing? I want to see if I'm getting the same numbers there that you were. |
This is what I am using for the tests. Can you tell me the slice for which you have posted the images ? (z value). Masking should not have affected the results anyway. |
If Scipy 17 is not used, many times the value for D is becoming -ve and that is driving the f values to be incorrect I guess. Also, check these two plots for which f value is almost same but the D* value is 10 times more. Is it supposed to mean anything ?
@etpeterson In your code you find 'f' by taking 1 - S0/S0_intercept in I am not able to think of why the current PR should give incorrect results since it seems to fit data points well. |
Notebook which shows the CSF section and the fitting. https://gist.github.com/sahmed95/1af7d4550ee3862c25849946427e05d6 |
@etpeterson I am trying out the semilog plots. Meanwhile consider these two voxels : In one of them D = 0, and the other has D* = 0. But both have high f. You can use this notebook to check some more plots. With noise, f shoots up to 50% : |
@arokem
Also, @etpeterson in your code : https://github.com/etpeterson/IVIM_fitting/blob/master/IVIM_fit.py#L261 you fit three times :
Did I miss something ? |
Regarding the plots, if D or D* are 0 then it's just a single exponential so the f is no longer true. If D is 0 then it's 100% D* so the f would be 1, and vice versa. I think this may be the problem though, that it's reducing one of the D values to 0 which means that f becomes extraneous (the intercept then becomes S0*f essentially) and that is why the images look bad but the fits look good. So to me it's looking like the guesses are good but the final fit is still moving very far from the initial guesses. Federau didn't do a full fit at the end, like you mention, so his preserves D in that way. I do a full fit in my approach but I regularize to the initial parameters so it doesn't depart too far from them. You can think about it as ways to enforce a biexponential fit in data that is very slightly biexponential. It's nothing that you did wrong or even a bug in the code, if I'm right :) I didn't have this experience even before regularization so perhaps leastsq is more aggressive in reducing parameters to 0 than minimize is. For me the test to see if this theory is right would be to display the D and f map from estimate_x0 and the fit side by side. In my experience the estimates should be fairly clean looking, and that would be the gold standard D so if the fit and the estimate_x0 results differ then your problem is with the fit changing the initial guesses too much. |
Actually I just checked my processing and it finished so here are the f estimates and fits (first image left and right). The estimates look smooth and reasonable, but in about half the cases the fitting diverges from that significantly. And in those regions the D maps of the fit went to 0 (second image left and right). It's looking to me like either fixing D (not fitting it) or regularizing D and maybe f is the way to go. |
I implemented @etpeterson 's method as a quick fix and the results seem to be better. Please have a look at this notebook : https://github.com/sahmed95/notebooks/blob/ivim_profiling/ivim_testing.ipynb And the corresponding code is here : https://github.com/sahmed95/dipy/blob/ivim_linear/dipy/reconst/ivim.py#L270 I am following the exact method followed by @etpeterson in his original code. You may pull that branch and try a full brain fitting. |
So is it just a matter of regions in the data where leastsq jumps to D=0 or D*=0 ? |
Wow that was fast! You just added a D* fitting right? I'm kind of surprised that was the fix to be honest. Maybe the initial guesses we had weren't that good. The results you're showing do look good though. I think either D or D* going to 0 was just a sign that it was failing. |
Ok. That actually makes sense and I think that's what Federau's paper mentions. Right now, I can think of three ways to do this :
|
@etpeterson I didn't just add a D* fit. I followed this :
|
OK. I usually do a final fitting for the whole thing at the end, but it's regularized. Mostly because I don't trust the f or S0 estimate completely which affects D* also. I thought this approach ended up being too through and there's no need to fit bvals < 200 and fit only D* separately was just taking extra time and not really helping. But perhaps that kind of attention is necessary? |
This doesn't work and least_squares jumps into the D=0 or D*=0 solution nevertheless. I just tried it and the results are same as before. This leaves us with either option 1 (only fit D once and estimate f from the intercepts) or option 3 (filter out the unfeasible params) |
Update : Using minimize with bounds too doesnot help and the solutions quickly jump to high values of f or D/D*=0. This happens even if I make the tolerance 1e-10. @arokem @etpeterson The last images you have posted, which one of them are the results of this PR ? If the left ones are using this PR then I think the fit is working fine. Only in case of unreasonable signal values f is erroneous. This can be simply tackled by clipping such values of f to 0. or selecting a good mask for the data. Using the linear fitting method mentioned before also leads to fairly low values of f which is making me think that leaving the fit as it is and filtering the anomalous parameters might be a good option.
This might be an issue as just doing a simple linear fitting for D and S0 and using it to get f does not pass the current test cases. One last option I thought was to set up better bounds like f = 0.3 or D/D* = 0.005. (mismatch 100.0%)
Here you can see both one stage and two stage implemented together. This passes all current test cases but again has the issue of jumping to erroneous paramters in two stage : quantshah@da39858#diff-399e30b2aa12d6577cd208ab46f99082R245 |
The images are from the older PR, the f and D values. I haven't tested any newer PRs yet. The left images are from the estimated values and the right are from the fitted. So basically the estimates are good but the fits aren't. This is what you're saying too. I think the current method doesn't have issues with strong signals but it seems to as there is more noise. So maybe using the normal 2 stage fitting with some SNR based regularization, or even simpler just keeping S0 and D the same as the estimate and only fitting f and D* (which would be faster). The first would be more like my approach and the 2nd more like Federau. I'm not sure which would be better or even if it's worth tweaking at this point or that is more of a post-merge PR. |
Initial simulations of IVIM
Notebook with examples
https://github.com/sahmed95/notebooks/blob/ivim/ivim_dev.ipynb