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

NF - Particle Filtering Tractography (merge) #1384

Merged
merged 49 commits into from Feb 6, 2018

Conversation

gabknight
Copy link
Contributor

@gabknight gabknight commented Nov 28, 2017

This PR replaces #1340, and follows the changes of PR #1342.

This PR add a new tissue classifier (CMC) and a new tractography algorithm (PFT). Both methods are described in [Girard et al., NImg, 2014].

CMC - new tissue Classifier. CmcTissueClassifier used probabilities and the tissue partial volume fraction maps to determine when the tracking stops. As it is very similar to ActTissueClassifier, both classes now extend an a new abstract class ConstrainedTissueClassifier.

PFT - new tractography method. ParticleFilteringTracking is inherited from the LocalTracking class. The particle filter is used to find a valid streamline path when the streamline being traced reach the state INVALIDPOINT.

An example is giving in doc/examples/particle_filtering_fiber_tracking.py

References

Girard, G., Whittingstall, K., Deriche, R., & Descoteaux, M. Towards quantitative connectivity analysis: reducing tractography biases. NeuroImage, 98, 266-278, 2014.

streamline[streamline_i + s, j] = particle_paths[0, p, s, j]
directions[streamline_i + s, j] = particle_dirs[0, p, s, j]

###TODO fix this
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nilgoyette, do you know why cython doesn't let me do tissue_class[0] = TissueClass(particle_states[0, p, 0])

NameError: name 'TissueClass' is not defined

I do have

from .tissue_classifier cimport TissueClass

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that a cython enum doesn't "exists". It's simply defining the constants in the enum. You can cast it to the right type though:

tissue_class[0] = <TissueClass>particle_states[0, p, 0]

I tested and it compiles and all the tests pass. There's no way to check if the particle_states returns a valid TissueClass but hey, it's cython!

@codecov-io
Copy link

codecov-io commented Nov 28, 2017

Codecov Report

Merging #1384 into master will increase coverage by 0.11%.
The diff coverage is 96.5%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1384      +/-   ##
==========================================
+ Coverage   87.07%   87.19%   +0.11%     
==========================================
  Files         227      230       +3     
  Lines       29063    29497     +434     
  Branches     3125     3171      +46     
==========================================
+ Hits        25307    25719     +412     
- Misses       3046     3061      +15     
- Partials      710      717       +7
Impacted Files Coverage Δ
dipy/tracking/local/tests/test_tracking.py 97.79% <100%> (ø)
dipy/tracking/local/__init__.py 100% <100%> (ø) ⬆️
dipy/segment/tests/test_mrf.py 92.5% <100%> (ø) ⬆️
dipy/direction/tests/test_pmf.py 90.47% <90.47%> (ø)
dipy/tracking/local/localtracking.py 97.5% <94.44%> (-2.5%) ⬇️
...ipy/tracking/local/tests/test_tissue_classifier.py 98.33% <96.29%> (+2.58%) ⬆️
dipy/reconst/mapmri.py 89.3% <0%> (-0.09%) ⬇️
dipy/reconst/tests/test_mapmri.py 98.56% <0%> (ø) ⬆️
dipy/reconst/forecast.py 90.15% <0%> (ø)
... and 3 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e0db0d7...a63ae09. Read the comment docs.

@gabknight gabknight added this to Needs a review in Dipy 0.14.0 Nov 29, 2017
@gabknight
Copy link
Contributor Author

Thanks for the quick feedback @nilgoyette. That's was the line I needed :)

@StongeEtienne it would be awesome if you could test this PFT implementation with SET. You will notice the speedup!

@skoudoro
Copy link
Member

No @gabknight and It is weird that it appears only on one build. I will restart this build to be sure that it is reproducible before investigating it

@gabknight
Copy link
Contributor Author

Thank again @nilgoyette for noticing this bug. I much happier to find it now :p

Here is the whole brain tracking (stanford_hardi) with and without PFT, same seeding, same tracking parameters.

pft

@codecov-io
Copy link

codecov-io commented Jan 24, 2018

Codecov Report

Merging #1384 into master will decrease coverage by 0.32%.
The diff coverage is 96.87%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1384      +/-   ##
==========================================
- Coverage   87.07%   86.75%   -0.33%     
==========================================
  Files         227      237      +10     
  Lines       29063    30008     +945     
  Branches     3125     3232     +107     
==========================================
+ Hits        25307    26033     +726     
- Misses       3046     3239     +193     
- Partials      710      736      +26
Impacted Files Coverage Δ
dipy/tracking/local/__init__.py 100% <100%> (ø) ⬆️
dipy/tracking/local/tests/test_tracking.py 97.9% <100%> (ø)
dipy/segment/tests/test_mrf.py 92.5% <100%> (ø) ⬆️
dipy/direction/tests/test_pmf.py 90.47% <90.47%> (ø)
dipy/tracking/local/localtracking.py 97.59% <94.87%> (-2.41%) ⬇️
...ipy/tracking/local/tests/test_tissue_classifier.py 98.33% <96.29%> (+2.58%) ⬆️
dipy/tracking/tests/test_fbc.py 91.66% <0%> (-0.93%) ⬇️
dipy/reconst/mapmri.py 89.3% <0%> (-0.09%) ⬇️
dipy/reconst/tests/test_mapmri.py 98.56% <0%> (ø) ⬆️
dipy/segment/tests/test_metric.py 97.5% <0%> (ø) ⬆️
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e0db0d7...14f4c76. Read the comment docs.

/ step_size))
self.pft_max_nbr_front_steps = int(np.ceil(pft_front_tracking_dist
/ step_size))
self.pft_max_steps = (self.pft_max_nbr_back_steps
Copy link
Contributor

Choose a reason for hiding this comment

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

pft_max_steps is never used outsite of this constructor. Should not be in self
Or use it. You have a back_steps + front_steps in your call to _pft.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

back_steps + front_steps is variable (depending on the current tracking point). I removed it from self. thx.

double point[3]
double dir[3]
double voxdir[3]
double eps = np.finfo(np.float).eps
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to make sure, do you want float32.eps or float64.eps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

float64 should be fine as we always use double in trilinear_interpolate4d. It might be safer to put eps slightly higher as we do multiple additions in trilinear_interpolate4d.


sum_weights = 0
for p in range(particle_count):
sum_weights += particle_weights[p]
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be sure, are you aware that sometimes your weights all equal NaN?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't. This happens when all the weighs of the particle are equal to 0. In this case, all particle paths are equally bad and we return the path of the first particle, with the TissueClass INVALIDPOINT. i.e. PFT failed to find an alternative valid path. Having weights = 0 or nan produces the same output. I now removed the division that made the nans. thx.

sum_squared += particle_weights[p] * particle_weights[p]

# Resample the particles if the weights are too uneven.
# Particles with negligable weights are replaced by duplicates of
Copy link
Contributor

Choose a reason for hiding this comment

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

negligible

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thx.

# Particles with negligable weights are replaced by duplicates of
# those with high weigths through resampling
N_effective = 1. / sum_squared
if N_effective < particle_count / 10.:
Copy link
Contributor

Choose a reason for hiding this comment

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

Using your test (without the fixed random), I couldn't get this to happen. In fact, everytime I tested, all elements in the arrays (paths, dirs, states, weights) are perfectly equal in the p dimension. Looking at the code, I don't see how they could be different. If they are actually always equal, then this algo is much too complicated!

I probably don't understand well enough. Can you please explain how it can happen? Is it only because I'm using test data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On my side, the resampling happens and the weights, dirs, states, path are different.
try running nosetests --nocapture -v in dipy/tracking/local/
and adding a print at line localtrack.pyx:463 print("Resampling")
add adding a print at line localtracking.py:239 print(self.particle_particle_weights)

You can also try running the example: python doc/example/particle_filtering_tractography.py to check if pft_streamlines and 'local_prob_streamlines are different.


In _pft(.), we do probabilistic tractography for P particles and N steps. at each tracking step, the P particle weights are updates following the information provided by the TissueClassifier (localtrack.pyx:line 441). After the N steps, we sample 1 particle from the P particles using their weight (higher weight, more likely to be sampled). The path of this particle is the one used in the final streamline.

The thing here is that P particles might be very few particles to properly explore a given configuration. To overcome that, we focus the exploration in regions more promising (i.e. particles with higher weights). To do that, we discard particles with low weight and duplicate particles with high weights. This is done when the weight distribution is unbalanced (e.g. when 1. / sum_squared < particle_count / 10).

fixed_step(point, voxdir, step_size)
copypoint(point, &particle_paths[0, p, s + 1, 0])
copypoint(dir, &particle_dirs[0, p, s + 1, 0])
particle_states[0, p, 0] = tc.check_point_c(point)
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason why you shared the particle tissue_class with the particle length? IMO, it's harder to understand for no real gain. Shouldn't they be 2 arrays?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought that would be more clear to group them to reduce the number of arrays... Let me try with 2 arrays.

if (self.pft_max_nbr_front_steps < 0
or self.pft_max_nbr_back_steps < 0
or pft_max_steps < 1):
raise ValueError("The number of PFT steps must be greater than 0.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't <= 0.0 be better for pft_max_nbr_front_steps and pft_max_nbr_back_steps? It seems to me that pft_back_tracking_dist and pft_front_tracking_dist should be > 0.0. No?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At least one of the 2 must be >0, otherwise, there is no PFT. Putting pft_max_nbr_front_steps = 0 is conceivable, with a pft_back_tracking_dist > 0, or the other way around.

With CMC (probabilistic stopping criteria), pft_back_tracking_dist can be 0 without issue, that wouldn't work for ACT (deterministic stopping criteria), i.e. with ACT and pft_back_tracking_dist = 0, the output would be the same as without PFT.

Copy link
Contributor

@nilgoyette nilgoyette left a comment

Choose a reason for hiding this comment

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

We could discuss much more about everything in this PR, but I think we are at a point where we should merge it. Good job.

@skoudoro
Copy link
Member

skoudoro commented Jan 31, 2018

Any other review or request?

If not, I will merge it this Friday.

@jchoude
Copy link
Contributor

jchoude commented Feb 1, 2018

Retested integration in our normal work, works well. It is a +1 from me.

Good job @gabknight and everyone!

@Garyfallidis
Copy link
Contributor

Garyfallidis commented Feb 2, 2018

Give me time to review this until this Sunday. Sorry busy week.

be used in conjunction with PFT. In this example, we used CMC.
"""

import matplotlib.pyplot as plt
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused import

step_size=step_size,
maxlen=1000,
return_all=False)
probabilistic_streamlines = [s for s in probabilistic_streamlines]
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 the old standard for generating streamlines which has memory issues what you should use is

from dipy.tracking.streamline import Streamlines
streamlines = Streamlines(probabilistic_streamlines)

See example tracking_quick_start.py for more.

maxlen=1000,
return_all=False)
probabilistic_streamlines = [s for s in probabilistic_streamlines]
save_trk("probabilistic_streamlines.trk", probabilistic_streamlines, affine,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should also be updated by a new function. But @skoudoro will take care of that in his Streamlines API update PR.
#1379

pft_front_tracking_dist=1,
particle_count=15,
return_all=False)
streamlines = [s for s in pft_streamlines]
Copy link
Contributor

Choose a reason for hiding this comment

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

See below.

@Garyfallidis
Copy link
Contributor

Apart from a few comments. I think this is ready.

@gabknight
Copy link
Contributor Author

Thank you for the feedback @Garyfallidis, I made the changes. Thank you all for your help and feedback on this PR!

@Garyfallidis
Copy link
Contributor

Merging 🌟 ! Many years on the making for this PR. Good luck! And @gabknight lets make a workflow :)

@Garyfallidis Garyfallidis merged commit e4c8b9b into dipy:master Feb 6, 2018
Dipy 0.14.0 automation moved this from Needs a review to Done Feb 6, 2018
@jchoude
Copy link
Contributor

jchoude commented Feb 6, 2018

🍺 thanks @gabknight for the hard work and everyone (@nilgoyette ) for the reviews!

ShreyasFadnavis pushed a commit to ShreyasFadnavis/dipy that referenced this pull request Sep 20, 2018
NF - Particle Filtering Tractography (merge)
@gabknight gabknight deleted the NF_cmc_pft_merged branch April 4, 2024 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Dipy 0.14.0
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

7 participants