Repository: ME-ICA/tedana

Issue:
The make_adaptive_mask method results in significant loss of voxels
I used the following command to run tedana:

`tedana -e $TE -d e1_in.nii.gz e2_in.nii.gz e3_in.nii.gz e4_in.nii.gz --out-dir="${Bids_dir}/${sub_no}/${cond}/tedana_nomask" --tedpca kundu-stabilize
`

However, I noticed that the result in desc-adaptiveGoodSignal_mask.nii.gz contains a significant loss of signal. After tracing the issue, I found that the problem lies within the compute_epi_mask method, particularly with the following code:

```
lower_cutoff = int(np.floor(lower_cutoff * len(sorted_input)))
upper_cutoff = min(int(np.floor(upper_cutoff * len(sorted_input))), len(sorted_input) - 1)
delta = (sorted_input[lower_cutoff + 1 : upper_cutoff + 1]- sorted_input[lower_cutoff:upper_cutoff])
ia = delta.argmax()
threshold = 0.5 * (sorted_input[ia + lower_cutoff] + sorted_input[ia + lower_cutoff + 1])
```

My understanding is that this code is trying to find the boundary between the background and brain tissue regions. However, in my case, the threshold value reached as high as 6000, which suggests that the ideal boundary was not found. I believe the difference between high signal areas in my images is larger than the difference between background and brain signals.

As a result, I tried lowering the upper_cutoff from 0.85 to 0.65, and the threshold dropped to 300, significantly reducing the signal loss problem.

I'm not sure if my understanding is correct. Does anyone have any suggestions on this?

Below are the results for desc-adaptiveGoodSignal_mask.nii.gz:
Left: upper_cutoff = 0.85
Right: upper_cutoff = 0.65

<img width="1093" alt="Image" src="https://github.com/user-attachments/assets/97a3225a-d247-4dcf-85a0-486328ecacd8" />

Hints from issue comments:
@tsalo: We _strongly_ recommend providing a precalculated mask to the workflow instead of relying on the automasking. If changing the threshold works for your data, then I think it makes sense to calculate a brain mask with that threshold, save it to a file, and then feed the mask into tedana when you run it.@handwerkerd: Just to add more, we've included a simple automatic mask calculation just so that tedana can run without an external mask, but pretty much every established full pipeline for fMRI data (i.e. AFNI, fMRIPrep, FSL, & SPM) has a better mask generation tool. Since tedana isn't a full pipeline, we recommend taking the mask generated by whatever pipeline you're using. Given that mask, tedana calculates an adaptive mask that identifies voxels where the shortest echo might have signal, but later voxels do not. This is very conservative so it should remove too many additional voxels with typical multi-echo fMRI scans. If it is removing a lot of voxels, that's a sign that there might be issues with acquisition choices or the data. More info on the adaptive mask calculation is here: https://tedana.readthedocs.io/en/stable/approach.html#adaptive-mask-generation

That all said, this is a very frequent question. I'm wondering, if we should remove the default for tedana to calculate a general mask or add a warning when that option is used.

Solution patch:
diff --git a/tedana/workflows/tedana.py b/tedana/workflows/tedana.py
index 02494c0..367931c 100644
--- a/tedana/workflows/tedana.py
+++ b/tedana/workflows/tedana.py
@@ -106,7 +106,8 @@ def _get_parser():
             "space as `data`. If an explicit mask is not "
             "provided, then Nilearn's compute_epi_mask "
             "function will be used to derive a mask "
-            "from the first echo's data."
+            "from the first echo's data. "
+            "Providing a mask is recommended."
         ),
         default=None,
     )
@@ -441,6 +442,8 @@ def tedana_workflow(
         spatially aligned with `data`. If an explicit mask is not provided,
         then Nilearn's compute_epi_mask function will be used to derive a mask
         from the first echo's data.
+        Since most pipelines use better masking tools,
+        providing a mask, rather than using compute_epi_mask, is recommended.
     convention : {'bids', 'orig'}, optional
         Filenaming convention. bids uses the latest BIDS derivatives version (1.5.0).
         Default is 'bids'.
@@ -688,7 +691,7 @@ def tedana_workflow(
         RepLGR.info("A user-defined mask was applied to the data.")
         mask = utils.reshape_niimg(mask).astype(int)
     elif t2smap and not mask:
-        LGR.info("Using user-defined T2* map to generate mask")
+        LGR.info("Assuming user=defined T2* map is masked and using it to generate mask")
         t2s_limited_sec = utils.reshape_niimg(t2smap)
         t2s_limited = utils.sec2millisec(t2s_limited_sec)
         t2s_full = t2s_limited.copy()
@@ -701,7 +704,12 @@ def tedana_workflow(
         mask = utils.reshape_niimg(mask).astype(int)
         mask[t2s_limited == 0] = 0  # reduce mask based on T2* map
     else:
-        LGR.info("Computing EPI mask from first echo")
+        LGR.warning(
+            "Computing EPI mask from first echo using nilearn's compute_epi_mask function. "
+            "Most external pipelines include more reliable masking functions. "
+            "It is strongly recommended to provide an external mask, "
+            "and to visually confirm that mask accurately conforms to data boundaries."
+        )
         first_echo_img = io.new_nii_like(io_generator.reference_img, data_cat[:, 0, :])
         mask = compute_epi_mask(first_echo_img).get_fdata()
         mask = utils.reshape_niimg(mask).astype(int)


Test patch:
diff --git a/tedana/tests/test_integration.py b/tedana/tests/test_integration.py
index 1825091..17806ef 100644
--- a/tedana/tests/test_integration.py
+++ b/tedana/tests/test_integration.py
@@ -254,7 +254,7 @@ def test_integration_three_echo(skip_integration):
     check_integration_outputs(fn, out_dir)
 
 
-def test_integration_three_echo_external_regressors_single_model(skip_integration):
+def test_integration_three_echo_external_regressors_single_model(skip_integration, caplog):
     """Integration test of tedana workflow with extern regress and F stat."""
 
     if skip_integration:
@@ -299,6 +299,8 @@ def test_integration_three_echo_external_regressors_single_model(skip_integratio
     fn = resource_filename("tedana", "tests/data/cornell_three_echo_outputs.txt")
     check_integration_outputs(fn, out_dir)
 
+    assert "It is strongly recommended to provide an external mask," in caplog.text
+
 
 def test_integration_three_echo_external_regressors_motion_task_models(skip_integration):
     """Integration test of tedana workflow with extern regress and F stat."""