def save_subject_template(sub, template='benson14', directory=None, create_directory=True): ''' save_subject_template(sub) writes out a set of mgz files in the given subject's analyses directory; the files are the angle, eccen, and varea label files for the subject's left and right hemispheres, as predicted by the Benson et al. (2014) template of retinotopy. The option template may be given to specify 'benson14' or 'benson17' models. ''' template = template.lower() fssub = ny.freesurfer_subject(sub) (lhdat, rhdat) = ny.vision.predict_retinotopy(fssub, template=template) for (hdat, hnm) in [(lhdat, 'lh'), (rhdat, 'rh')]: for (datkey, datname) in [('polar_angle', 'angle'), ('eccentricity', 'eccen'), ('visual_area', 'varea')]: dat = hdat[datkey] flnm = os.path.join(analyses_path(), sub, hnm + '.' + datname + '_' + template + '.mgz') img = nibabel.freesurfer.mghformat.MGHImage( np.asarray( [[dat]], dtype=(np.int32 if datname == 'varea' else np.float32)), np.eye(4)) img.to_filename(flnm) return None
def import_benson14_surface_from_freesurfer(subject): if isinstance(subject, basestring): subject = neuro.freesurfer_subject(subject) if not isinstance(subject, neurofs.Subject): raise ValueError('Subject given to FreeSurferAnatomyModule object is not a neuropythy' + \ ' FreeSurfer subject or string') # make sure there are template volume files that match this subject lang = os.path.join(subject.directory, 'surf', 'lh.angle_benson14.mgz') lecc = os.path.join(subject.directory, 'surf', 'lh.eccen_benson14.mgz') llab = os.path.join(subject.directory, 'surf', 'lh.v123roi_benson14.mgz') rang = os.path.join(subject.directory, 'surf', 'rh.angle_benson14.mgz') recc = os.path.join(subject.directory, 'surf', 'rh.eccen_benson14.mgz') rlab = os.path.join(subject.directory, 'surf', 'rh.v123roi_benson14.mgz') if not os.path.exists(lang) or not os.path.exists(lecc) or not os.path.exists(llab) \ or not os.path.exists(rang) or not os.path.exists(recc) or not os.path.exists(rlab): # Apply the template first... benson14_retinotopy_command(subject.directory) if not os.path.exists(lang) or not os.path.exists(lecc) or not os.path.exists(llab) \ or not os.path.exists(rang) or not os.path.exists(recc) or not os.path.exists(rlab): raise ValueError('No areas template found/created for subject: ' + lab) l_angle_mgz = fs.mghformat.load(lang) l_eccen_mgz = fs.mghformat.load(lecc) l_label_mgz = fs.mghformat.load(llab) r_angle_mgz = fs.mghformat.load(rang) r_eccen_mgz = fs.mghformat.load(recc) r_label_mgz = fs.mghformat.load(rlab) return { 'subject': subject, 'lh_polar_angle_mgh': l_angle_mgz, 'lh_eccentricity_mgh': l_eccen_mgz, 'lh_v123_labels_mgh': l_label_mgz, 'rh_polar_angle_mgh': r_angle_mgz, 'rh_eccentricity_mgh': r_eccen_mgz, 'rh_v123_labels_mgh': r_label_mgz }
def import_benson14_volumes_from_freesurfer(subject): if isinstance(subject, basestring): subject = neuro.freesurfer_subject(subject) if not isinstance(subject, neurofs.Subject): raise ValueError('Subject given to FreeSurferAnatomyModule object is not a neuropythy' + \ ' FreeSurfer subject or string') # make sure there are template volume files that match this subject ang = os.path.join(subject.directory, 'mri', 'angle_benson14.mgz') ecc = os.path.join(subject.directory, 'mri', 'eccen_benson14.mgz') lab = os.path.join(subject.directory, 'mri', 'v123roi_benson14.mgz') if not os.path.exists(ang) or not os.path.exists( ecc) or not os.path.exists(lab): # Apply the template first... benson14_retinotopy_command(subject.directory) if not os.path.exists(ang) or not os.path.exists( ecc) or not os.path.exists(lab): raise ValueError('No areas template found/created for subject: ' + lab) angle_mgz = fs.mghformat.load(ang) eccen_mgz = fs.mghformat.load(ecc) label_mgz = fs.mghformat.load(lab) ribbon_mgzs = (subject.LH.ribbon, subject.RH.ribbon) return { 'subject': subject, 'polar_angle_mgh': angle_mgz, 'eccentricity_mgh': eccen_mgz, 'v123_labels_mgh': label_mgz, 'ribbon_mghs': ribbon_mgzs }
def test_interpolation(self): ''' test_interpolation() performs a variety of high-level tests involving interpolation using neuropythy that should catch major errors to important components. ''' logging.info('neuropythy: Testing interpolation...') def choose(coll, k): return np.random.choice(coll, k, False) # to do these tests, we use the builtin dataset from Benson and Winawer (2018); see also # help(ny.data['benson_winawer_2018']) for more information on this dataset. dset = ny.data['benson_winawer_2018'] self.assertTrue(os.path.isdir(dset.cache_directory)) # pick 1 of the subjects at random allsubs = [dset.subjects['S12%02d' % (s+1)] for s in range(8)] subs = choose(allsubs, 1) fsa = ny.freesurfer_subject('fsaverage') def check_dtypes(a,b): for tt in [np.integer, np.floating, np.bool_, np.complexfloating]: self.assertEqual(np.issubdtype(a.dtype, tt), np.issubdtype(b.dtype, tt)) def calc_interp(hem, interhem, ps): for p in ps: self.assertEqual(np.sum(~np.isfinite(hem.prop(p))), 0) us = hem.interpolate(interhem, ps) for u in us: self.assertEqual(np.sum(~np.isfinite(u)), 0) vs = interhem.interpolate(hem, us) for v in vs: self.assertEqual(np.sum(~np.isfinite(v)), 0) return vs def check_interp(hem, ps, vs): for (p,v) in zip(ps,vs): logging.info('neuropythy: * %s', p) p = hem.prop(p) self.assertEqual(len(p), len(v)) self.assertLessEqual(np.min(p), np.min(v)) self.assertGreaterEqual(np.max(p), np.max(v)) check_dtypes(p, v) self.assertGreater(np.corrcoef(p, v)[0,0], 0.6) for sub in subs: logging.info('neuropythy: - Testing subject %s', sub.name) # left hemisphere should have a negative mean x-value, right a positive mean x-value self.assertTrue(np.mean(sub.lh.white_surface.coordinates, axis=1)[0] < 0) self.assertTrue(np.mean(sub.rh.pial_surface.coordinates, axis=1)[0] > 0) # some simple ideas: if we interpolate the properties from one subject to another and # then interpolate back, we should get approximately, if not exactly, the same thing # for this pick a couple random properties: ps = ['prf_variance_explained', 'inf-prf10_visual_area'] intersub = choose(allsubs, 1)[0] logging.info('neuropythy: - Testing properties %s via subject %s', ps, intersub.name) logging.info('neuropythy: - Testing LH interpolation') vs = calc_interp(sub.lh, intersub.lh, ps) check_interp(sub.lh, ps, vs) logging.info('neuropythy: - Testing RH interpolation') vs = calc_interp(sub.rh, intersub.rh, ps) check_interp(sub.rh, ps, vs)
def test_path(self): ''' test_path() ensures that the neuropythy.geometry.path and .path_trace data structures are working correctly. ''' logging.info('neuropythy: Testing Path and PathTrace') # simple box: should have an area of ~1600 in a flatmap and something close in a sphere pts = [(-20,-20), (20,-20), (20,20), (-20,20)] # use a simple map projection mpj = ny.map_projection('occipital_pole', 'lh', radius=np.pi/3) ctx = ny.freesurfer_subject('fsaverage').lh trc = ny.geometry.path_trace(mpj, pts, closed=True) fmp = mpj(ctx) pth = trc.to_path(fmp) self.assertTrue(np.isclose(1600, pth.surface_area))
def import_freesurfer_subject(subject): ''' import_freesurfer_subject is a calculator that requires a subject id (subject) and yields a Neuropythy FreeSurfer subject object, freesurfer_subject. @ subject Must be one of (a) the name of a FreeSurfer subject found on the subject path, (b) a path to a FreeSurfer subject directory, or (c) a neuropythy FreeSurfer subject object. ''' if isinstance(subject, basestring): subject = neuro.freesurfer_subject(subject) if not isinstance(subject, neuro.Subject): raise ValueError( 'Value given for subject is neither a string nor a neuropythy subject' ) return subject
def import_freesurfer_subject(subject=None): ''' import_freesurfer_subject is a calculator that requires a subject id (subject) and yields a Neuropythy FreeSurfer subject object, freesurfer_subject. @ subject Must be one of (a) the name of a FreeSurfer subject found on the subject path, (b) a path to a FreeSurfer subject directory, (c) a neuropythy FreeSurfer subject object, or (d) None, indicating that the pRF data does not come from a subject. ''' if subject is None: return None if pimms.is_str(subject): subject = ny.freesurfer_subject(subject) if not ny.is_subject(subject): raise ValueError( 'Value given for subject is neither a string nor a neuropythy subject' ) return subject
def aggregate_hemi(hem): global _subject_hemi_cache hem = hem.lower() if 'agg' not in _subject_hemi_cache: _subject_hemi_cache['agg'] = {} aggcch = _subject_hemi_cache['agg'] if hem in aggcch: return aggcch[hem] aggdat = aggregate_data(hem) agghem = getattr(ny.freesurfer_subject('fsaverage'), hem.upper()) agghem = agghem.using( properties=reduce(lambda p, x: p.without(x) if x in p else p, [ 'PRF_polar_angle', 'PRF_eccentricity', 'PRF_size', 'PRF_variance_explained', 'predicted_polar_angle', 'predicted_visual_area', 'predicted_eccentricity', 'polar_angle', 'eccentricity', 'visual_area' ], agghem.properties)) agghem = agghem.using(properties=agghem.properties.using( polar_angle=aggdat['polar_angle'], eccentricity=aggdat['eccentricity'], variance_explained=aggdat['variance_explained'], prf_size=aggdat['prf_size'])) aggcch[hem] = agghem return agghem
def aggregate(hemi, model='benson17', steps=5000, scale=1.0): ''' aggregate(hemi) yields a hemisphere mesh object for the fsaverage_sym subject with data from both the group average retinotopy and the 'Benson14' registered predictions of retinotopy stored in the properties. The mesh's coordinates have been registered to the V123 retinotopy model. The following options are accepted: * model (default: 'benson17') specifies the retinotopy model to use. * steps (default: None) specifies the number of steps to run in the minimization; if None, then uses 10000 for benson17 model and 20000 for the schira model. * scale (default: 1.0) specifies the scale of the retinotopy potential field term relative to the scale of the mesh-based terms. ''' global _agg_cache model = model.lower() hemi = hemi.lower() tpl = (hemi, model, steps, scale) if tpl in _agg_cache: return _agg_cache[tpl] dat = aggregate_register(hemi, model=model, steps=steps, scale=scale, exclusion_threshold=exclusion_threshold) pre = dat['prediction'] varea = pre['visual_area'] if 'visual_area' in pre else pre['V123_label'] mesh = ny.freesurfer_subject('fsaverage_sym').LH.sphere_surface mesh = mesh.using(coordinates=dat['registered_coordinates'], properties=mesh.properties.using( PRF_polar_angle=dat['sub_polar_angle'], PRF_eccentricity=dat['sub_eccentricity'], weight=dat['sub_weight'], predicted_polar_angle=pre['polar_angle'], predicted_eccentricity=pre['eccentricity'], predicted_visual_area=varea)) _agg_cache[tpl] = mesh return mesh
def subject_hemi(sub, hem, ds=None): global _subject_hemi_cache, _subject_hemi_cache_ds if ds is None: if sub not in _subject_hemi_cache: _subject_hemi_cache[sub] = {} scache = _subject_hemi_cache[sub] hem = hem.lower() if hem not in scache: gsdat = subject_data(sub, hem, 0) hemi = getattr(ny.freesurfer_subject(sub), hem.upper()) hemi = hemi.using( properties=reduce(lambda p, x: p.without(x) if x in p else p, [ 'PRF_polar_angle', 'PRF_eccentricity', 'PRF_size', 'PRF_variance_explained', 'predicted_polar_angle', 'predicted_visual_area', 'predicted_eccentricity', 'polar_angle', 'eccentricity', 'visual_area' ], hemi.properties)) hemi = hemi.using(properties=hemi.properties.using( gold_polar_angle=gsdat['polar_angle'], gold_eccentricity=gsdat['eccentricity'], gold_variance_explained=gsdat['variance_explained'], gold_prf_size=gsdat['prf_size'])) scache[hem] = hemi return scache[hem] elif (sub, hem, ds) in _subject_hemi_cache_ds: return _subject_hemi_cache_ds[(sub, hem, ds)] else: shemi = subject_hemi(sub, hem) sdat = subject_data(sub, hem, ds) shemi = shemi.using(properties=shemi.properties.using( polar_angle=sdat['polar_angle'], eccentricity=sdat['eccentricity'], variance_explained=sdat['variance_explained'], prf_size=sdat['prf_size'])) _subject_hemi_cache_ds[(sub, hem, ds)] = shemi return shemi
def main(args): # Parse the arguments... parser = argparse.ArgumentParser() parser.add_argument( 'reg', metavar='registration_file', nargs=1, help=('The distort2anat_tkreg.dat or similar file: the registration' ' file, in FreeSurfer\'s tkreg format, to apply to the EPIs.')) parser.add_argument( 'epis', metavar='EPI', type=str, nargs='+', help='The EPI files to be converted to anatomical orientation') parser.add_argument( '-t', '--tag', required=False, default='-', dest='tag', nargs=1, help=('A tag to append to the output filenames; if given as - or' ' omitted, overwrites original files.')) parser.add_argument( '-s', '--surf', required=False, default=False, dest='surface', action='store_true', help=('If provided, instructs the script to also produce files of the ' 'time-series resampled on the cortical surface.')) parser.add_argument( '-o', '--out', required=False, default='.', dest='outdir', help=( 'The output directory to which the files should be written; by' ' default this is the current directory (.); note that if this' ' directory also contains the EPI files and there is no tag given,' ' then the EPIs will be overwritten.')) parser.add_argument( '-m', '--method', required=False, default='linear', dest='method', help=('The method to use for volume-to-surface interpolation; this may' ' be nearest or linear; the default is linear.')) parser.add_argument( '-l', '--layer', required=False, default='midgray', dest='layer', help=( 'Specifies the cortical layer to user in interpolation from volume' ' to surface. By default, uses midgray. May be set to a value' ' between 0 (white) and 1 (pial) to specify an intermediate surface' ' or may be simply white, pial, or midgray.')) parser.add_argument( '-d', '--subjects-dir', required=False, default=None, dest='sdir', help=('Specifies the subjects directory to use; by default uses the' ' environment variable SUBJECTS_DIR.')) parser.add_argument('-v', '--verbose', required=False, default=False, action='store_true', dest='verbose', help='Print verbose output') if args[0].startswith('python'): args = args[2:] else: args = args[1:] args = parser.parse_args(args) # Check some of the arguments... epis = args.epis if len(epis) < 1: raise RuntimeError('No EPIs given') tag = args.tag[0] if tag == '-': tag = '' dosurf = args.surface outdir = args.outdir if not os.path.isdir(outdir): raise RuntimeError('Directory %s does not exist' % outdir) if args.verbose: def note(*args): six.print_(*args, flush=True) return True else: def note(*args): return False try: args.layer = float(args.layer) except: pass # Read in the registration file args.reg = args.reg[0] if not os.path.isfile(args.reg): raise RuntimeError('Given registration file not found: %s' % args.reg) with open(args.reg, 'r') as f: lines = [] while True: s = f.readline() if s is None or s == '': break lines.append(s) # This tells us some info... sub = lines[0].strip() if args.sdir is not None: ny.add_subject_path(args.sdir) try: sub = ny.freesurfer_subject(sub) except: raise ValueError( 'No subject %s; you may need to set your SUBJECTS_DIR' % sub) affine = np.asarray([[float(ss) for ss in s.split()] for s in lines[4:8]]) affinv = np.linalg.inv(affine) displm = sub.lh.affine # loop over the given EPIs for epi in epis: note('Processing EPI %s...' % epi) # import the epi file.. img = ny.load(epi, to='image') # edit the header... note(' - Correcting volume orientation...') new_affine = np.dot(displm, np.dot(affinv, ny.freesurfer.tkr_vox2ras(img))) newimg = nib.Nifti1Image(img.dataobj, new_affine, img.header) (epi_dir, epi_flnm) = os.path.split(epi) if epi_flnm[:-4] in ['.mgz', '.mgh', '.nii']: pre = epi_flnm[:-4] suf = epi_flnm[-4:] else: pre = epi_flnm[:-7] suf = epi_flnm[-7:] srf_flnm = pre + tag + '.mgz' epi_flnm = pre + tag + suf newimg.to_filename(os.path.join(args.outdir, epi_flnm)) # okay, now project to the surface if args.surface: note(' - Projecting to surface...') (ldat, rdat) = sub.image_to_cortex(newimg, surface=args.layer, method=args.method, dtype=np.float32) # we need to fix the dimensions... for (d, h) in zip([ldat, rdat], ['lh', 'rh']): if d.shape[-1] == 1: # then this should properly be a 3d MGH image, not a 4d one. im = nib.freesurfer.mghformat.MGHImage( np.transpose(reduce(np.expand_dims, [-1], d), (0, 2, 1)), np.eye(4)) else: im = nib.freesurfer.mghformat.MGHImage( np.transpose(reduce(np.expand_dims, [-1, -1], d), (0, 2, 3, 1)), np.eye(4)) im.to_filename(os.path.join(args.outdir, h + '.' + srf_flnm)) # That's it! return 0
elif f.startswith('sub-'): sub = f elif f == 'derivatives': derpth = os.path.join(pp, f) if derpth is None: die('Could not find derivatives path') if sub is None: die('Could not deduce subject from BIDS path') if os.path.isdir(os.path.join(derpth, 'freesurfer')): sub = os.path.join(derpth, 'freesurfer', 'sub-' + sub) elif 'SUBJECTS_DIR' in os.environ: tmp = os.path.join(os.environ['SUBJECTS_DIR'], sub) if os.path.isdir(tmp): sub = tmp else: die('No derivatives/freesurfer or $SUBJECTS_DIR/%s directory found', sub) print('FreeSurfer Path: %s' % sub) print('Image Path: %s' % pth) try: sub = ny.freesurfer_subject(sub) lh = sub.lh rh = sub.rh except: raise die('Failed to load freesurfer subject') sys.stdout.write('\nPreparing flat-maps...') sys.stdout.flush() try: mps_post = { h: ny.map_projection('occipital_pole', h, radius=np.pi / 2) for h in ['lh', 'rh'] } mps_ante = { h: mp.copy(center=-mp.center, center_right=-mp.center_right) for (h, mp) in six.iteritems(mps_post)
def main(args): # Parse the arguments... parser = argparse.ArgumentParser() parser.add_argument('sub', metavar='subject', nargs=1, help=('The FreeSurfer subject ID or directory.')) parser.add_argument('outdir', metavar='directory', type=str, nargs=1, help='The directory containing the output nifti files.') parser.add_argument('-x', '--no-surf', required=False, default=True, dest='surface', action='store_false', help=('If provided, instructs the script not to produce files of the ' 'pRF parameters resampled onto the cortical surface.')) parser.add_argument('-m', '--method', required=False, default='linear', dest='method', help=('The method to use for volume-to-surface interpolation; this may' ' be nearest or linear; the default is linear.')) parser.add_argument('-l', '--layer', required=False, default='midgray', dest='layer', help=('Specifies the cortical layer to user in interpolation from volume' ' to surface. By default, uses midgray. May be set to a value' ' between 0 (white) and 1 (pial) to specify an intermediate surface' ' or may be simply white, pial, or midgray.')) parser.add_argument('-d', '--subjects-dir', required=False, default=None, dest='sdir', help=('Specifies the subjects directory to use; by default uses the' ' environment variable SUBJECTS_DIR.')) parser.add_argument('-y', '--no-invert-y', required=False, default=True, dest='invert_y', action='store_false', help=('If provided, does not invert the y-coordinate exported from' ' VistaSoft; by default this inversion is performed.')) parser.add_argument('-v', '--verbose', required=False, default=False, action='store_true', dest='verbose', help='Print verbose output') if args[0].startswith('python'): args = args[2:] else: args = args[1:] args = parser.parse_args(args) # Check some of the arguments... sub = ny.freesurfer_subject(args.sub[0]) dosurf = args.surface outdir = args.outdir[0] if not os.path.isdir(outdir): raise RuntimeError('Directory %s does not exist' % outdir) else: os.chdir(outdir) if args.verbose: def note(*args): six.print_(*args, flush=True) return True else: def note(*args): return False try: args.layer = float(args.layer) except: pass # figure out what datasets are here... dsets = [fl[:-13] for fl in os.listdir('.') if fl.endswith('-xcrds.nii.gz')] # loop over the given EPIs for ds in dsets: note('Processing Dataset %s...' % ds) # import the files... note(' - Importing parameters...') (x,y,s,v) = [ny.load('%s-%s.nii.gz' % (ds, suff), to='image') for suff in ['xcrds','ycrds','sigma','vexpl']] # fix polar angle/eccen note(' - Creating polar angle/eccentricity images...') ang = np.arctan2(-y.get_data() if args.invert_y else y.get_data(), x.get_data()) ang = np.mod((90.0 - 180.0/np.pi * ang) + 180, 360) - 180 a = nib.Nifti1Image(ang, x.affine, x.header) e = nib.Nifti1Image(np.sqrt(y.get_data()**2 + x.get_data()**2), x.affine, x.header) a.to_filename('%s-angle.nii.gz' % ds) e.to_filename('%s-eccen.nii.gz' % ds) # surfaces... if not dosurf: continue note(' - Projecting to surface...') to_output = {'xcrds':x, 'ycrds':y, 'sigma': s, 'vexpl': v} xy = ([0,0],[0,0]) for (dname,im) in six.iteritems(to_output): (ldat, rdat) = sub.image_to_cortex(im, args.layer, method=args.method, dtype=np.float32, weights=v) if dname == 'xcrds': xy[0][0] = ldat xy[1][0] = rdat elif dname == 'ycrds': xy[0][1] = ldat xy[1][1] = rdat # we need to fix the dimensions... for (d,h) in zip([ldat,rdat], ['lh','rh']): ny.save('%s.%s-%s.mgz' % (h, ds, dname), d) # Special handling for the angle and eccen (avoids need for circular averaging) xy = (np.asarray(xy[0]), np.asarray(xy[1])) (lecc,recc) = [np.sqrt(np.sum(u**2, axis=0)) for u in xy] (lang,rang) = [np.arctan2(-u[1] if args.invert_y else u[1], u[0]) for u in (xy[0] * ny.util.zinv(lecc), xy[1] * ny.util.zinv(recc))] (lang,rang) = [np.mod((90 - 180/np.pi * aa) + 180, 360) - 180 for aa in (lang,rang)] # export these... for (h,nm,dat) in [('lh','eccen',lecc), ('lh','angle',lang), ('rh','eccen',recc), ('rh','angle',rang)]: ny.save('%s.%s-%s.mgz' % (h, ds, nm), dat) # That's it! return 0
def die(s, *argv): if len(argv) > 0: s = s % tuple(argv) sys.stderr.write(s + '\n') sys.stderr.flush() sys.exit(1) if sys.argv != 4: dir('SYNTAX: fsnative_to_fsaverage.py <freesurfer_subject_id_or_path> <coordinates_file> <out>' ) # load the freesurfer subject sid = sys.argv[1] try: sub = ny.freesurfer_subject(sid) except Exception: die('Could not load freesurfer subject: %s', sid) # load the coords file coords = pandas.read_csv(sys.argv[2], sep=' ', header=None) coords = coords[[1, 2, 3]].values # find nearest for each of these points on the subject's pial surface lpial = sub.lh.pial_surface rpial = sub.rh.pial_surface (nl, nr) = (lpial.vertex_count, rpial.vertex_count) n = nl + nr pial = np.vstack([lpial.coordinates.T, rpial.coordinates.T]) try: sh = space.cKDTree(pial)