|
19 | 19 |
|
20 | 20 | import nibabel as nb
|
21 | 21 | import numpy as np
|
| 22 | +from scipy import linalg |
22 | 23 |
|
23 | 24 | from .. import logging
|
24 | 25 | from ..external.due import due, Doi, BibTeX
|
25 | 26 | from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
|
26 | 27 | BaseInterfaceInputSpec, File, isdefined)
|
| 28 | +from .misc import regress_poly |
27 | 29 | IFLOG = logging.getLogger('interface')
|
28 | 30 |
|
29 |
| - |
30 | 31 | class ComputeDVARSInputSpec(BaseInterfaceInputSpec):
|
31 | 32 | in_file = File(exists=True, mandatory=True, desc='functional data, after HMC')
|
32 | 33 | in_mask = File(exists=True, mandatory=True, desc='a brain mask')
|
@@ -276,6 +277,152 @@ def _run_interface(self, runtime):
|
276 | 277 | def _list_outputs(self):
|
277 | 278 | return self._results
|
278 | 279 |
|
| 280 | +class CompCorInputSpec(BaseInterfaceInputSpec): |
| 281 | + realigned_file = File(exists=True, mandatory=True, |
| 282 | + desc='already realigned brain image (4D)') |
| 283 | + mask_file = File(exists=True, mandatory=False, |
| 284 | + desc='mask file that determines ROI (3D)') |
| 285 | + components_file = File('components_file.txt', exists=False, |
| 286 | + mandatory=False, usedefault=True, |
| 287 | + desc='filename to store physiological components') |
| 288 | + num_components = traits.Int(6, usedefault=True) # 6 for BOLD, 4 for ASL |
| 289 | + use_regress_poly = traits.Bool(True, usedefault=True, |
| 290 | + desc='use polynomial regression' |
| 291 | + 'pre-component extraction') |
| 292 | + regress_poly_degree = traits.Range(low=1, default=1, usedefault=True, |
| 293 | + desc='the degree polynomial to use') |
| 294 | + |
| 295 | +class CompCorOutputSpec(TraitedSpec): |
| 296 | + components_file = File(exists=True, |
| 297 | + desc='text file containing the noise components') |
| 298 | + |
| 299 | +class CompCor(BaseInterface): |
| 300 | + ''' |
| 301 | + Interface with core CompCor computation, used in aCompCor and tCompCor |
| 302 | +
|
| 303 | + Example |
| 304 | + ------- |
| 305 | +
|
| 306 | + >>> ccinterface = CompCor() |
| 307 | + >>> ccinterface.inputs.realigned_file = 'functional.nii' |
| 308 | + >>> ccinterface.inputs.mask_file = 'mask.nii' |
| 309 | + >>> ccinterface.inputs.num_components = 1 |
| 310 | + >>> ccinterface.inputs.use_regress_poly = True |
| 311 | + >>> ccinterface.inputs.regress_poly_degree = 2 |
| 312 | + ''' |
| 313 | + input_spec = CompCorInputSpec |
| 314 | + output_spec = CompCorOutputSpec |
| 315 | + |
| 316 | + def _run_interface(self, runtime): |
| 317 | + imgseries = nb.load(self.inputs.realigned_file).get_data() |
| 318 | + mask = nb.load(self.inputs.mask_file).get_data() |
| 319 | + voxel_timecourses = imgseries[mask > 0] |
| 320 | + # Zero-out any bad values |
| 321 | + voxel_timecourses[np.isnan(np.sum(voxel_timecourses, axis=1)), :] = 0 |
| 322 | + |
| 323 | + # from paper: |
| 324 | + # "The constant and linear trends of the columns in the matrix M were |
| 325 | + # removed [prior to ...]" |
| 326 | + if self.inputs.use_regress_poly: |
| 327 | + voxel_timecourses = regress_poly(self.inputs.regress_poly_degree, |
| 328 | + voxel_timecourses) |
| 329 | + voxel_timecourses = voxel_timecourses - np.mean(voxel_timecourses, |
| 330 | + axis=1)[:, np.newaxis] |
| 331 | + |
| 332 | + # "Voxel time series from the noise ROI (either anatomical or tSTD) were |
| 333 | + # placed in a matrix M of size Nxm, with time along the row dimension |
| 334 | + # and voxels along the column dimension." |
| 335 | + M = voxel_timecourses.T |
| 336 | + numvols = M.shape[0] |
| 337 | + numvoxels = M.shape[1] |
| 338 | + |
| 339 | + # "[... were removed] prior to column-wise variance normalization." |
| 340 | + M = M / self._compute_tSTD(M, 1.) |
| 341 | + |
| 342 | + # "The covariance matrix C = MMT was constructed and decomposed into its |
| 343 | + # principal components using a singular value decomposition." |
| 344 | + u, _, _ = linalg.svd(M, full_matrices=False) |
| 345 | + components = u[:, :self.inputs.num_components] |
| 346 | + components_file = os.path.join(os.getcwd(), self.inputs.components_file) |
| 347 | + np.savetxt(components_file, components, fmt="%.10f") |
| 348 | + return runtime |
| 349 | + |
| 350 | + def _list_outputs(self): |
| 351 | + outputs = self._outputs().get() |
| 352 | + outputs['components_file'] = os.path.abspath(self.inputs.components_file) |
| 353 | + return outputs |
| 354 | + |
| 355 | + def _compute_tSTD(self, M, x): |
| 356 | + stdM = np.std(M, axis=0) |
| 357 | + # set bad values to x |
| 358 | + stdM[stdM == 0] = x |
| 359 | + stdM[np.isnan(stdM)] = x |
| 360 | + return stdM |
| 361 | + |
| 362 | +class TCompCorInputSpec(CompCorInputSpec): |
| 363 | + # and all the fields in CompCorInputSpec |
| 364 | + percentile_threshold = traits.Range(low=0., high=1., value=.02, |
| 365 | + exclude_low=True, exclude_high=True, |
| 366 | + usedefault=True, desc='the percentile ' |
| 367 | + 'used to select highest-variance ' |
| 368 | + 'voxels, represented by a number ' |
| 369 | + 'between 0 and 1, exclusive. By ' |
| 370 | + 'default, this value is set to .02. ' |
| 371 | + 'That is, the 2% of voxels ' |
| 372 | + 'with the highest variance are used.') |
| 373 | + |
| 374 | +class TCompCor(CompCor): |
| 375 | + ''' |
| 376 | + Interface for tCompCor. Computes a ROI mask based on variance of voxels. |
| 377 | +
|
| 378 | + Example |
| 379 | + ------- |
| 380 | +
|
| 381 | + >>> ccinterface = TCompCor() |
| 382 | + >>> ccinterface.inputs.realigned_file = 'functional.nii' |
| 383 | + >>> ccinterface.inputs.mask_file = 'mask.nii' |
| 384 | + >>> ccinterface.inputs.num_components = 1 |
| 385 | + >>> ccinterface.inputs.use_regress_poly = True |
| 386 | + >>> ccinterface.inputs.regress_poly_degree = 2 |
| 387 | + >>> ccinterface.inputs.percentile_threshold = .03 |
| 388 | + ''' |
| 389 | + |
| 390 | + input_spec = TCompCorInputSpec |
| 391 | + output_spec = CompCorOutputSpec |
| 392 | + |
| 393 | + def _run_interface(self, runtime): |
| 394 | + imgseries = nb.load(self.inputs.realigned_file).get_data() |
| 395 | + |
| 396 | + # From the paper: |
| 397 | + # "For each voxel time series, the temporal standard deviation is |
| 398 | + # defined as the standard deviation of the time series after the removal |
| 399 | + # of low-frequency nuisance terms (e.g., linear and quadratic drift)." |
| 400 | + imgseries = regress_poly(2, imgseries) |
| 401 | + imgseries = imgseries - np.mean(imgseries, axis=1)[:, np.newaxis] |
| 402 | + |
| 403 | + time_voxels = imgseries.T |
| 404 | + num_voxels = np.prod(time_voxels.shape[1:]) |
| 405 | + |
| 406 | + # "To construct the tSTD noise ROI, we sorted the voxels by their |
| 407 | + # temporal standard deviation ..." |
| 408 | + tSTD = self._compute_tSTD(time_voxels, 0) |
| 409 | + sortSTD = np.sort(tSTD, axis=None) # flattened sorted matrix |
| 410 | + |
| 411 | + # use percentile_threshold to pick voxels |
| 412 | + threshold_index = int(num_voxels * (1. - self.inputs.percentile_threshold)) |
| 413 | + threshold_std = sortSTD[threshold_index] |
| 414 | + mask = tSTD >= threshold_std |
| 415 | + mask = mask.astype(int) |
| 416 | + |
| 417 | + # save mask |
| 418 | + mask_file = 'mask.nii' |
| 419 | + nb.nifti1.save(nb.Nifti1Image(mask, np.eye(4)), mask_file) |
| 420 | + self.inputs.mask_file = mask_file |
| 421 | + |
| 422 | + super(TCompCor, self)._run_interface(runtime) |
| 423 | + return runtime |
| 424 | + |
| 425 | +ACompCor = CompCor |
279 | 426 |
|
280 | 427 | def compute_dvars(in_file, in_mask, remove_zerovariance=False):
|
281 | 428 | """
|
|
0 commit comments