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

[WIP] NF: Population specific bundle atlas creation #2425

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

drombas
Copy link
Contributor

@drombas drombas commented Jul 26, 2021

Hi! This is part of GSoC 2021.

Goal

Given the segmented bundles of several subjects as input, compute a bundle atlas that is representative of that population.

Implementation

The input bundles are combined in pairs following a tree structure until a single bundle is obtained:

The main logic is implemented in atlasing.bundles.compute_bundle_atlas() and consists of these steps:

  1. Check input arguments and retrieve the list of bundles and subjects to be processed
  2. Preprocess the bundles (set the number of points per streamline, remove bundles with few streamlines...)
  3. Create the tree structure with atlasing.bundles.get_pairwise_tree()
  4. Build the atlas by following the tree. At each pair, bundles are aligned and then combined by atlasing.bundles.combine_bundles(), which implements several methods to match the streamlines and combine them into a single bundle.

Comments
This is WIP as we are still testing different approaches and need to reach a final decision on:

  • Which input file formats to support (currently it only supports trk)
  • Which bundle combination methods to support

List of tasks:

  • Create atlasing module and first tests
  • Extend the coverage of atlasing module tests
  • Add tests for the workflow
  • Polish docstrings and comments
  • Create an example/tutorial

@pep8speaks
Copy link

pep8speaks commented Jul 26, 2021

Hello @drombas, Thank you for updating !

Cheers ! There are no PEP8 issues in this Pull Request. 🍻

Comment last updated at 2021-08-17 10:47:06 UTC

@codecov
Copy link

codecov bot commented Jul 26, 2021

Codecov Report

Merging #2425 (3e35297) into master (cf5f4f9) will increase coverage by 0.23%.
The diff coverage is 96.84%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2425      +/-   ##
==========================================
+ Coverage   84.49%   84.73%   +0.23%     
==========================================
  Files         125      127       +2     
  Lines       16624    16909     +285     
  Branches     2702     2769      +67     
==========================================
+ Hits        14046    14327     +281     
- Misses       1911     1913       +2     
- Partials      667      669       +2     
Impacted Files Coverage Δ
dipy/io/utils.py 87.66% <78.78%> (+1.06%) ⬆️
dipy/workflows/atlasing.py 90.90% <90.90%> (ø)
dipy/atlasing/bundles.py 99.58% <99.58%> (ø)
dipy/data/fetcher.py 42.37% <100.00%> (+0.24%) ⬆️

@skoudoro
Copy link
Member

skoudoro commented Aug 2, 2021

Thank you @drombas for this PR. Let us know when it is ready for a first review. Thank you

@drombas
Copy link
Contributor Author

drombas commented Aug 2, 2021

Hi @skoudoro ! Tests finally look mostly ok so yes, please, have a look at it!

Copy link
Contributor

@arokem arokem left a comment

Choose a reason for hiding this comment

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

This looks really interesting. I have a couple of small comments.

dipy/atlasing/bundles.py Outdated Show resolved Hide resolved
dipy/atlasing/bundles.py Outdated Show resolved Hide resolved
dipy/atlasing/bundles.py Outdated Show resolved Hide resolved
dipy/atlasing/bundles.py Outdated Show resolved Hide resolved
dipy/atlasing/bundles.py Outdated Show resolved Hide resolved
@@ -420,3 +425,71 @@ def read_img_arr_or_path(data, affine=None):
affine = data.affine
data = data.get_fdata()
return data, affine


def show_bundles(bundles, fname, colors=None, opacity=None, linewidth=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder whether this shouldn't go into dipy.viz

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 putted there cause I use it to save the output as png files but makes sense to move it to dipy.viz. @skoudoro what do you think?

dipy/io/utils.py Outdated Show resolved Hide resolved
raise ValueError("Subjects cannot be duplicated")

print(str(len(subjects)) + " subjects to be processed:")
print(subjects)
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be pretty long, 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.

Yes but I found it useful as a safety check. Using logger.debug() might be a good option?

Copy link
Contributor

@BramshQamar BramshQamar left a comment

Choose a reason for hiding this comment

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

Hi @drombas,

Great work! Thank you for creating the PR. I have added my first review.

Bramsh

@@ -0,0 +1,506 @@
"""Atlasing module: utilities to compute population specific bundle atlases.
Copy link
Contributor

Choose a reason for hiding this comment

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

population-specific streamline-based bundle atlases.

"""Atlasing module: utilities to compute population specific bundle atlases.

Available functions:
get_pairwise_tree: computes the matching pairs for a given number of items.
Copy link
Contributor

Choose a reason for hiding this comment

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

items or bundles?

combine_bundle: combines two bundles into a single one using different
streamline combination methods.

compute_bundle_atlas: given a list of input bundles computes the population
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this create a whole-brain atlas from given bundles? eg: merging all bundles to create one whole brain atlas. Or does it create one atlas bundle per bundle type? I think it's the second one. Maybe just rephrase it.

"""Pairwise tree structure calculation.

Constructs a pairwise tree by randomly matching the indexes of a given
number of items. The computed index structure is intended to be used for
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just use the word 'bundle' since it is a bundle-specific atlas or do you think this is more generic and could be used for other purposes? If it's the second, then maybe we need to move it to utils?

# If n_item is odd duplicate one of the others
if np.mod(n_item, 2) == 1:
# avoid removing again an item twice (index == 0)
lonely = np.random.randint(1, n_item)
Copy link
Contributor

Choose a reason for hiding this comment

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

How about the variable name single?

from dipy.atlasing.bundles import compute_atlas_bundle


class DiscreteBundleAtlasFlow(Workflow):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should drop the word 'Discrete'.


Given several segmented bundles as input, compute the atlas by
combining the bundles pairwise.

Copy link
Contributor

Choose a reason for hiding this comment

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

Add some more details

distance : str, optional
Distance metric to be used to combine bundles. Default is 'mdf'.
The 'mdf_se' distance uses only start/end points of streamlines.
comb_method : str, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as in the other functions.

space=Space.RASMM)
save_trk(new_tractogram, file, bbox_valid_check=False)
return atlas, atlas_merged
return atlas
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just return atlas, atlas_merged
atlas_merged can be None if there isn't one. This way the function output will be consistent (returns fixed number of objects).

Copy link
Contributor

Choose a reason for hiding this comment

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

Will solve the issue in the workflow as well where you have written the same function call twice with if and else conditions.

bundle_names=bundle_names,
model_bundle_dir=model_bundle_dir,
out_dir=out_dir,
merge_out=merge_out,
Copy link
Contributor

Choose a reason for hiding this comment

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

You can call it one time and just always have it return two outputs. See my comment above in compute_atlas_bundle function.

rm_small_clusters=1,
qbx_thr=[5])

points, offsets = unlist_streamlines(static)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why unlist and relist here?

@iPsych
Copy link

iPsych commented Nov 16, 2022

When I run it and generate average tract using TRACULA training data (33 subjects), I found interesting error only happens in bilateral cab_PP (cingulum-angular bundle).

I guess it's due to the irregularity of each subject's tract shape, but cannot clearly figure out what happened.
The tracts also attached.

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In [20], line 4
      2 for x in tracts:
      3   print(x)
----> 4   atlasing.bundles.compute_atlas_bundle('atlasing_test_flt/'+x+'/',model_bundle_dir='/Volumes/Jotunn/Suicide_Study/TRACULA_DIPY_Vis/atlasing_test_flt/rh.cab_PP/trc001',n_point=100,n_stream_min=5)
[rh.cab_PP.zip](https://github.com/dipy/dipy/files/10022300/rh.cab_PP.zip)

File ~/Library/jupyterlab-desktop/jlab_server/lib/python3.8/site-packages/dipy/atlasing/bundles.py:402, in compute_atlas_bundle(in_dir, subjects, group, mid_path, bundle_names, model_bundle_dir, out_dir, merge_out, save_temp, n_stream_min, n_stream_max, n_point, distance, comb_method, skip_pairs)
    399 streamlines = set_number_of_points(streamlines, n_point)
    401 if model_bundles is not None:
--> 402     streamlines, _, _, _ = slr_with_qbx(static=model_bundle,
    403                                         moving=streamlines,
    404                                         x0='affine',
    405                                         rm_small_clusters=1,
    406                                         qbx_thr=[5])
    407     header = header_model
    409 file = f'{step_dir}/bundle_{i}_prev_{sub}'

File ~/Library/jupyterlab-desktop/jlab_server/lib/python3.8/site-packages/dipy/align/streamlinear.py:980, in slr_with_qbx(static, moving, x0, rm_small_clusters, maxiter, select_random, verbose, greater_than, less_than, qbx_thr, nb_pts, progressive, rng, num_threads)
    977 rstreamlines2 = set_number_of_points(rstreamlines2, nb_pts)
    978 rstreamlines2._data.astype('f4')
--> 980 cluster_map2 = qbx_and_merge(rstreamlines2, thresholds=qbx_thr, rng=rng)
    982 qb_centroids2 = remove_clusters_by_size(cluster_map2, rm_small_clusters)
    984 if verbose:

File ~/Library/jupyterlab-desktop/jlab_server/lib/python3.8/site-packages/dipy/segment/clustering.py:746, in qbx_and_merge(streamlines, thresholds, nb_pts, select_randomly, rng, verbose)
    742 qbx_merge = QuickBundlesX([thresholds[-1]],
    743                           metric=AveragePointwiseEuclideanMetric())
    745 final_level = len(thresholds)
--> 746 len_qbx_fl = len(qbx_clusters.get_clusters(final_level))
    747 qbx_ordering_final = rng.choice(len_qbx_fl, len_qbx_fl, replace=False)
    749 qbx_merged_cluster_map = qbx_merge.cluster(
    750     qbx_clusters.get_clusters(final_level).centroids,
    751     ordering=qbx_ordering_final).get_clusters(1)

AttributeError: 'ClusterMapCentroid' object has no attribute 'get_clusters'

@drombas
Copy link
Contributor Author

drombas commented Nov 16, 2022

Hi @iPsych,
I suspect it is related with a bundle having very few streamlines (trc30 maybe).
Anyways, let me check it with my last version of this method (this PR is quite old)

@skoudoro skoudoro force-pushed the master branch 7 times, most recently from 1419292 to ca6268a Compare December 8, 2023 22:25
@skoudoro skoudoro force-pushed the master branch 3 times, most recently from 5935e1e to 765963e Compare January 24, 2024 19:24
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.

None yet

6 participants