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

1import warnings 

2 

3import numpy as np 

4import pandas as pd 

5 

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 

12 

13 

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. 

18 

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 

27 

28 For some atlases some 'clean up' of the LUT is done 

29 (for example make sure that the LUT contains the background 'ROI'). 

30 

31 This can also generate a look up table 

32 for an arbitrary niimg-like or surface image. 

33 

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. 

39 

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. 

45 

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. 

51 

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.") 

58 

59 fname = "unknown" if function is None else function 

60 

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] 

66 

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)) 

79 

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 ) 

87 

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)) 

95 

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)] 

103 

104 # convert to dataframe and do some cleaning where required 

105 lut = pd.DataFrame({"index": index, "name": name}) 

106 

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 ) 

114 

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 ) 

120 

121 return lut 

122 

123 

124def check_look_up_table(lut, atlas, strict=False, verbose=1): 

125 """Validate atlas look up table (LUT). 

126 

127 Make sure it complies with BIDS requirements. 

128 

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. 

133 

134 Parameters 

135 ---------- 

136 lut : :obj:`pandas.DataFrame` 

137 Must be a pandas dataframe with at least "name" and "index" columns. 

138 

139 atlas : Niimg like object or SurfaceImage or numpy array 

140 

141 strict : bool, default = False 

142 Errors are raised instead of warnings if strict == True. 

143 

144 verbose: int 

145 No warning thrown if set to 0. 

146 

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. 

154 

155 ValueError 

156 If regions in the image do not exist in the atlas lookup table 

157 and `strict=True`. 

158 

159 Warns 

160 ----- 

161 UserWarning 

162 If regions in the image do not exist in the atlas lookup table 

163 and `strict=False`. 

164 

165 """ 

166 assert isinstance(lut, pd.DataFrame) 

167 assert "name" in lut.columns 

168 assert "index" in lut.columns 

169 

170 roi_id = _get_indices_from_image(atlas) 

171 

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()) 

187 

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()) 

199 

200 

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 

207 

208 

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)