Repository: nilearn/nilearn

Issue:
Add label maps as output to `get_clusters_table`
<!--Provide a brief description what you would like changes and why.-->
I would like to add the label maps as outputs to `get_clusters_table` because it would be nice to be able to visualize where the clusters are on the brain as opposed to relying on the coordinate information from the table.

<!--Please fill in the following information, to the best of your ability.-->
### Benefits to the change
Able to visualize outputs, and also one could threshold the label map to get isolated clusters which could be helpful for other analyses.

### Pseudocode for the new behavior, if applicable

See linked PR #3477



Discussion:

Resolving patch:
diff --git a/examples/04_glm_first_level/plot_bids_features.py b/examples/04_glm_first_level/plot_bids_features.py
--- a/examples/04_glm_first_level/plot_bids_features.py
+++ b/examples/04_glm_first_level/plot_bids_features.py
@@ -140,7 +140,8 @@
 # We can get a latex table from a Pandas Dataframe for display and publication
 # purposes
 from nilearn.reporting import get_clusters_table
-print(get_clusters_table(z_map, norm.isf(0.001), 10).to_latex())
+table = get_clusters_table(z_map, norm.isf(0.001), 10)
+print(table.to_latex())
 
 #########################################################################
 # Generating a report
diff --git a/examples/04_glm_first_level/plot_predictions_residuals.py b/examples/04_glm_first_level/plot_predictions_residuals.py
--- a/examples/04_glm_first_level/plot_predictions_residuals.py
+++ b/examples/04_glm_first_level/plot_predictions_residuals.py
@@ -73,7 +73,8 @@
 from nilearn.maskers import NiftiSpheresMasker
 
 table = get_clusters_table(z_map, stat_threshold=3.1,
-                           cluster_threshold=20).set_index('Cluster ID', drop=True)
+                           cluster_threshold=20)
+table.set_index('Cluster ID', drop=True)
 table.head()
 
 # get the 6 largest clusters' max x, y, and z coordinates
diff --git a/nilearn/reporting/_get_clusters_table.py b/nilearn/reporting/_get_clusters_table.py
--- a/nilearn/reporting/_get_clusters_table.py
+++ b/nilearn/reporting/_get_clusters_table.py
@@ -17,7 +17,7 @@
     center_of_mass,
     generate_binary_structure,
 )
-from nilearn.image import threshold_img
+from nilearn.image import threshold_img, new_img_like
 from nilearn.image.resampling import coord_transform
 from nilearn._utils import check_niimg_3d
 from nilearn._utils.niimg import _safe_get_data
@@ -206,7 +206,8 @@ def _pare_subpeaks(xyz, ijk, vals, min_distance):
 
 
 def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
-                       two_sided=False, min_distance=8.):
+                       two_sided=False, min_distance=8.,
+                       return_label_maps=False):
     """Creates pandas dataframe with img cluster statistics.
 
     This function should work on any statistical maps where more extreme values
@@ -250,6 +251,12 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
             If two different clusters are closer than ``min_distance``, it can
             result in peaks closer than ``min_distance``.
 
+    return_label_maps : :obj:`bool`, optional
+        Whether or not to additionally output cluster label map images.
+        Default=False.
+    
+        .. versionadded:: 0.10.1.dev
+
     Returns
     -------
     df : :obj:`pandas.DataFrame`
@@ -267,6 +274,15 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
                            Rows corresponding to subpeaks will not have a value
                            in this column.
         ================== ====================================================
+        
+    label_maps : :obj:`list`
+        Returned if return_label_maps=True
+        List of Niimg-like objects of cluster label maps.
+        If two_sided==True, first and second maps correspond
+        to positive and negative tails.
+
+        .. versionadded:: 0.10.1.dev
+
     """
     cols = ['Cluster ID', 'X', 'Y', 'Z', 'Peak Stat', 'Cluster Size (mm3)']
     # Replace None with 0
@@ -274,7 +290,8 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
 
     # check that stat_img is niimg-like object and 3D
     stat_img = check_niimg_3d(stat_img)
-
+    affine = stat_img.affine
+    
     # Apply threshold(s) to image
     stat_img = threshold_img(
         img=stat_img,
@@ -298,6 +315,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
     signs = [1, -1] if two_sided else [1]
     no_clusters_found = True
     rows = []
+    label_maps = []
     for sign in signs:
         # Flip map if necessary
         temp_stat_map = stat_map * sign
@@ -318,6 +336,8 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
 
         # Now re-label and create table
         label_map = label(binarized, bin_struct)[0]
+        # Save label maps as nifti objects
+        label_maps.append(new_img_like(stat_img, label_map, affine=affine))
         clust_ids = sorted(list(np.unique(label_map)[1:]))
         peak_vals = np.array(
             [np.max(temp_stat_map * (label_map == c)) for c in clust_ids])
@@ -384,4 +404,7 @@ def get_clusters_table(stat_img, stat_threshold, cluster_threshold=None,
     else:
         df = pd.DataFrame(columns=cols, data=rows)
 
-    return df
+    if return_label_maps:
+        return df, label_maps
+    else:
+        return df


Test patch:
diff --git a/nilearn/reporting/tests/test_reporting.py b/nilearn/reporting/tests/test_reporting.py
--- a/nilearn/reporting/tests/test_reporting.py
+++ b/nilearn/reporting/tests/test_reporting.py
@@ -128,6 +128,15 @@ def test_get_clusters_table(tmp_path):
     stat_img.to_filename(fname)
     cluster_table = get_clusters_table(fname, 4, 0, two_sided=True)
     assert len(cluster_table) == 2
+    
+    # test with returning label maps
+    cluster_table, label_maps = get_clusters_table(
+        stat_img, 4, 0, two_sided=True, return_label_maps=True)
+    label_map_positive_data = label_maps[0].get_fdata()
+    label_map_negative_data = label_maps[1].get_fdata()
+    # make sure positive and negative clusters are returned in the label maps
+    assert np.sum(label_map_positive_data[2:4, 5:7, 6:8] != 0) == 8
+    assert np.sum(label_map_negative_data[4:6, 7:9, 8:10] != 0) == 8
 
     # test with extra dimension
     data_extra_dim = data[..., np.newaxis]