Coverage for nilearn/_utils/bids.py: 11%
76 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1import warnings
3import numpy as np
4import pandas as pd
6from nilearn._utils import check_niimg
7from nilearn._utils.logger import find_stack_level
8from nilearn._utils.niimg import safe_get_data
9from nilearn.surface.surface import SurfaceImage
10from nilearn.surface.surface import get_data as get_surface_data
11from nilearn.typing import NiimgLike
14def generate_atlas_look_up_table(
15 function=None, name=None, index=None, strict=False
16):
17 """Generate a BIDS compatible look up table for an atlas.
19 For a given deterministic atlas supported by Nilearn,
20 this returns a pandas dataframe to use as look up table (LUT)
21 between the name of a ROI and its index in the associated image.
22 This LUT is compatible with the dseg.tsv BIDS format
23 describing brain segmentations and parcellations,
24 with an 'index' and 'name' column
25 ('color' may be an example of an optional column).
26 https://bids-specification.readthedocs.io/en/latest/derivatives/imaging.html#common-image-derived-labels
28 For some atlases some 'clean up' of the LUT is done
29 (for example make sure that the LUT contains the background 'ROI').
31 This can also generate a look up table
32 for an arbitrary niimg-like or surface image.
34 Parameters
35 ----------
36 function : obj:`str` or None, default=None
37 Atlas fetching function name as a string.
38 Defaults to "unknown" in case None is passed.
40 name : iterable of bytes or string, or int or None, default=None
41 If an integer is passed,
42 this corresponds to the number of ROIs in the atlas.
43 If an iterable is passed, then it contains the ROI names.
44 If None is passed, then it is inferred from index.
46 index : iterable of integers, niimg like, surface image or None, \
47 default=None
48 If None, then the index of each ROI is derived from name.
49 If a Niimg like or SurfaceImage is passed,
50 then a LUT is generated for this image.
52 strict: bool, default=False
53 If True, an error will be thrown
54 if ``name`` and ``index``have different length.
55 """
56 if name is None and index is None:
57 raise ValueError("'index' and 'name' cannot both be None.")
59 fname = "unknown" if function is None else function
61 # deal with names
62 if name is None:
63 if fname == "unknown":
64 index = _get_indices_from_image(index)
65 name = [str(x) for x in index]
67 # deal with indices
68 if index is None:
69 index = list(range(len(name)))
70 else:
71 index = _get_indices_from_image(index)
72 if fname in ["fetch_atlas_basc_multiscale_2015"]:
73 index = []
74 for x in name:
75 tmp = 0.0 if x in ["background", "Background"] else float(x)
76 index.append(tmp)
77 elif fname in ["fetch_atlas_schaefer_2018", "fetch_atlas_pauli_2017"]:
78 index = list(range(1, len(name) + 1))
80 if len(name) != len(index):
81 if strict:
82 raise ValueError(
83 f"'name' ({len(name)}) and 'index' ({len(index)}) "
84 "have different lengths. "
85 "Cannot generate a look up table."
86 )
88 if len(name) < len(index):
89 warnings.warn(
90 "Too many indices for the names. "
91 "Padding 'names' with 'unknown'.",
92 stacklevel=find_stack_level(),
93 )
94 name += ["unknown"] * (len(index) - len(name))
96 if len(name) > len(index):
97 warnings.warn(
98 "Too many names for the indices. "
99 "Dropping excess names values.",
100 stacklevel=find_stack_level(),
101 )
102 name = name[: len(index)]
104 # convert to dataframe and do some cleaning where required
105 lut = pd.DataFrame({"index": index, "name": name})
107 if fname in [
108 "fetch_atlas_pauli_2017",
109 ]:
110 lut = pd.concat(
111 [pd.DataFrame([[0, "Background"]], columns=lut.columns), lut],
112 ignore_index=True,
113 )
115 # enforce little endian of index column
116 if lut["index"].dtype.byteorder == ">":
117 lut["index"] = lut["index"].astype(
118 lut["index"].dtype.newbyteorder("=")
119 )
121 return lut
124def check_look_up_table(lut, atlas, strict=False, verbose=1):
125 """Validate atlas look up table (LUT).
127 Make sure it complies with BIDS requirements.
129 Throws warning / errors:
130 - lut is not a dataframe with the required columns
131 - if there are mismatches between the number of ROIs
132 in the LUT and the number of unique ROIs in the associated image.
134 Parameters
135 ----------
136 lut : :obj:`pandas.DataFrame`
137 Must be a pandas dataframe with at least "name" and "index" columns.
139 atlas : Niimg like object or SurfaceImage or numpy array
141 strict : bool, default = False
142 Errors are raised instead of warnings if strict == True.
144 verbose: int
145 No warning thrown if set to 0.
147 Raises
148 ------
149 AssertionError
150 If:
151 - lut is not a dataframe with the required columns
152 - if there are mismatches between the number of ROIs
153 in the LUT and the number of unique ROIs in the associated image.
155 ValueError
156 If regions in the image do not exist in the atlas lookup table
157 and `strict=True`.
159 Warns
160 -----
161 UserWarning
162 If regions in the image do not exist in the atlas lookup table
163 and `strict=False`.
165 """
166 assert isinstance(lut, pd.DataFrame)
167 assert "name" in lut.columns
168 assert "index" in lut.columns
170 roi_id = _get_indices_from_image(atlas)
172 if len(lut) != len(roi_id):
173 if missing_from_image := set(lut["index"].to_list()) - set(roi_id):
174 missing_rows = lut[
175 lut["index"].isin(list(missing_from_image))
176 ].to_string(index=False)
177 msg = (
178 "\nThe following regions are present "
179 "in the atlas look-up table,\n"
180 "but missing from the atlas image:\n\n"
181 f"{missing_rows}\n"
182 )
183 if strict:
184 raise ValueError(msg)
185 if verbose:
186 warnings.warn(msg, stacklevel=find_stack_level())
188 if missing_from_lut := set(roi_id) - set(lut["index"].to_list()):
189 msg = (
190 "\nThe following regions are present "
191 "in the atlas image, \n"
192 "but missing from the atlas look-up table: \n\n"
193 f"{missing_from_lut}"
194 )
195 if strict:
196 raise ValueError(msg)
197 if verbose:
198 warnings.warn(msg, stacklevel=find_stack_level())
201def sanitize_look_up_table(lut, atlas) -> pd.DataFrame:
202 """Remove entries in lut that are missing from image."""
203 check_look_up_table(lut, atlas, strict=False, verbose=0)
204 indices = _get_indices_from_image(atlas)
205 lut = lut[lut["index"].isin(indices)]
206 return lut
209def _get_indices_from_image(image):
210 if isinstance(image, NiimgLike):
211 img = check_niimg(image)
212 data = safe_get_data(img)
213 elif isinstance(image, SurfaceImage):
214 data = get_surface_data(image)
215 elif isinstance(image, np.ndarray):
216 data = image
217 else:
218 raise TypeError(
219 "Image to extract indices from must be one of: "
220 "Niimg-Like, SurfaceIamge, numpy array. "
221 f"Got {type(image)}"
222 )
223 return np.unique(data)