def gaussian_2d(sz, sig): ''' gaussian_2d(size, sigma) yields a matrix with the given size representing a 2D gaussian image with the given sigma as its standard deviation. ''' sz = int(np.round(sz)) mid = (sz - 1) / 2 if not pimms.is_number(mid): mid = np.mean(mid) x = np.arange(-mid, sz - mid, 1) r2 = np.sum([u**2 for u in np.meshgrid(x, x)], axis=0) res = np.exp(-0.5 * r2 / sig**2) return res / np.sum(res)
def make_disk(imsz, radius, mid=None, edge_width=0): ''' make_disk(imsize, radius, centerpx, edge_width) yields a disk matrix whose internal values are 1 and whose external values are 0; the image size, radius of the disk, and the disk's center are given in pixels as the first three arguments. The final argument, edge_width is the number of pixels over which to smoothly blend the edge of the disk. If the parameter centerpx is omitted, the center of the image is used automatically. ''' if pimms.is_number(imsz): imsz = (int(np.round(imsz)), int(np.round(imsz))) if mid is None: mid = ((imsz[0] - 1) / 2, (imsz[1] - 1) / 2) elif pimms.is_number(mid): mid = (mid, mid) if edge_width is None: edge_width = 0 (x, y) = [np.arange(-u, sz - u, 1) for (u, sz) in zip(mid, imsz)] r = np.sqrt(np.sum([u**2 for u in np.meshgrid(x, y)], axis=0)) im = (r < radius).astype('float') if not np.isclose(edge_width, 0) and edge_width > 0: (r0, r1) = (radius - edge_width / 2, radius + edge_width / 2) ii = (r > r0) & (r < r1) im[ii] = 0.5 * (1 - np.sin((r[ii] - radius) / edge_width * np.pi)) return im
def parameters(params): ''' mdl.parameters is a persistent map of the parameters for the given SchiraModel object mdl. ''' if not pimms.is_pmap(params): params = pyr.pmap(params) # do the translations that we need... scale = params['scale'] if pimms.is_number(scale): params = params.set('scale', (scale, scale)) elif not is_tuple(scale): params = params.set('scale', tuple(scale)) shear = params['shear'] if pimms.is_number(shear) and np.isclose(shear, 0): params = params.set('shear', ((1, 0), (0, 1))) elif shear[0][0] != 1 or shear[1][1] != 1: raise RuntimeError('shear matrix diagonal elements must be 1!') elif not is_tuple(shear) or not all(is_tuple(s) for s in shear): params.set('shear', tuple([tuple(s) for s in shear])) center = params['center'] if pimms.is_number(center) and np.isclose(center, 0): params = params.set('center', (0.0, 0.0)) return pimms.persist(params, depth=None)
def to_name(nm): ''' Dataset.to_name(name) yields a valid dataset name equivalent to the given name or raises an error if name is not valid. In order to be valid, a name must be either strings or a tuple of number and strings that start with a string. ''' if pimms.is_str(nm): return nm if not pimms.is_vector(nm): raise ValueError('name must be a string or tuple') if len(nm) < 1: raise ValueError( 'names that are tuples must have at least one element') if not pimms.is_str(nm): raise ValueError('names that are tuples must begin with a string') if not all(pimms.is_str(x) or pimms.is_number(x) for x in nm): raise ValueError( 'dataset names that are tuples must contain only strings and numbers' ) return tuple(nm)
def _parse_field_function_argument(argdat, args, faces, edges, coords): # first, see if this is an easy one... if argdat == 'F': return faces elif argdat == 'X': return coords elif argdat == 'E': return edges elif pimms.is_int(argdat): return to_java_array(args[argdat]) # okay, none of those; must be a list with a default arg argname = argdat[0] argdflt = argdat[1] # see if we can find such an arg... for i in range(len(args)): if pimms.is_str(args[i]) and args[i].lower() == argname.lower(): return (args[i + 1] if pimms.is_number(args[i + 1]) else to_java_array(args[i + 1])) # did not find the arg; use the default: return argdflt
def mesh_register(mesh, field, max_steps=2000, max_step_size=0.05, max_pe_change=1, method='random', return_report=False, initial_coordinates=None): ''' mesh_register(mesh, field) yields the mesh that results from registering the given mesh by minimizing the given potential field description over the position of the vertices in the mesh. The mesh argument must be a Mesh object (see neuropythy.geometry) such as can be read from FreeSurfer using the neuropythy.freesurfer_subject function. The field argument must be a list of field names and arguments; with the exception of 'mesh' (or 'standard'), the arguments must be a list, the first element of which is the field type name, the second element of which is the field shape name, and the final element of which is a dictionary of arguments accepted by the field shape. The following are valid field type names: * 'mesh' : the standard mesh potential, which includes an edge potential, an angle potential, and a perimeter potential. Accepts no arguments, and must be passed as a single string instead of a list. * 'edge': an edge potential field in which the potential is a function of the change in the edge length, summed over each edge in the mesh. * 'angle': an angle potential field in which the potential is a function of the change in the angle measure, summed over all angles in the mesh. * 'perimeter': a potential that depends on the vertices on the perimeter of a 2D mesh remaining in place; the potential changes as a function of the distance of each perimeter vertex from its reference position. * 'anchor': a potential that depends on the distance of a set of vertices from fixed points in space. After the shape name second argument, an anchor must be followed by a list of vertex ids then a list of fixed points to which the vertex ids are anchored: ['anchor', shape_name, vertex_ids, fixed_points, args...]. The following are valid shape names: * 'harmonic': a harmonic function with the form (c/q) * abs(x - x0)^q. Parameters: * 'scale', the scale parameter c; default: 1. * 'order', the order parameter q; default: 2. * 'Lennard-Jones': a Lennard-Jones function with the form c (1 + (r0/r)^q - 2(r0/r)^(q/2)); Parameters: * 'scale': the scale parameter c; default: 1. * 'order': the order parameter q; default: 2. * 'Gaussian': A Gaussian function with the form c (1 - exp(-0.5 abs((x - x0)/s)^q)) Parameters: * 'scale': the scale parameter c; default: 1. * 'order': the order parameter q; default: 2. * 'sigma': the standard deviation parameter s; default: 1. * 'infinite-well': an infinite well function with the form c ( (((x0 - m)/(x - m))^q - 1)^2 + (((M - x0)/(M - x))^q - 1)^2 ) Parameters: * 'scale': the scale parameter c; default: 1. * 'order': the order parameter q; default: 0.5. * 'min': the minimum value m; default: 0. * 'max': the maximum value M; default: pi. Options: The following optional arguments are accepted. * max_steps (default: 2000) the maximum number of steps to minimize for. * max_step_size (default: 0.1) the maximum distance to allow a vertex to move in a single minimization step. * max_pe_change: the maximum fraction of the initial potential value that the minimizer should minimize away before returning; i.e., 0 indicates that no minimization should be allowed while 0.9 would indicate that the minimizer should minimize until the potential is 10% or less of the initial potential. * return_report (default: False) indicates that instead of returning the registered data, mesh_register should instead return the Java Minimizer.Report object (for debugging). * method (default: 'random') specifies the search algorithm used; available options are 'random', 'nimble', and 'pure'. Generally all options will converge on a similar solution, but usually 'random' is fastest. The 'pure' option uses the nben library's step function, which performs straight-forward gradient descent. The 'nimble' option performs a gradient descent in which subsets of vertices in the mesh that have the highest gradients during the registration are updated more often than those vertices with small gradients; this can sometimes but not always increase the speed of the minimization. Note that instead of 'nimble', one may alternately provide ('nimble', k) where k is the number of partitions that the vertices should be sorted into (by partition). 'nimble' by itself is equivalent to ('nimble', 4). Note also that a single step of nimble minimization is equivalent to 2**k steps of 'pure' minimization. Finally, the 'random' option uses the nben library's randomStep function, which is a gradient descent algorithm that moves each vertex in the direction of its negative gradient during each step but which randomizes the length of the gradient at each individual vertex by drawing from an exponential distribution centered at the vertex's actual gradient length. In effect, this can prevent vertices with very large gradients from dominating the minimization and often results in the best results. * initial_coordinates (default: None) specifies the start coordinates of the registration; if None, uses those in the given mesh, which is generally desired. Examples: registered_mesh = mesh_register( mesh, [['edge', 'harmonic', 'scale', 0.5], # slightly weak edge potential ['angle', 'infinite-well'], # default arguments for an infinite-well angle potential ['anchor', 'Gaussian', [1, 10, 50], [[0.0, 0.0], [1.1, 1.1], [2.2, 2.2]]]], max_step_size=0.05, max_steps=10000) ''' # Sanity checking: # First, make sure that the arguments are all okay: if not isinstance(mesh, geo.Mesh): raise RuntimeError( 'mesh argument must be an instance of neuropythy.geometry.Mesh') if not pimms.is_vector(max_steps): max_steps = [max_steps] for ms in max_steps: if not pimms.is_int(ms) or ms < 0: raise RuntimeError('max_steps argument must be a positive integer') if not pimms.is_vector(max_step_size): max_step_size = [max_step_size] for mss in max_step_size: if not pimms.is_number(mss) or mss <= 0: raise RuntimeError('max_step_size must be a positive number') if not pimms.is_number( max_pe_change) or max_pe_change <= 0 or max_pe_change > 1: raise RuntimeError( 'max_pe_change must be a number x such that 0 < x <= 1') if pimms.is_vector(method): if method[0].lower( ) == 'nimble' and len(method) > 1 and not pimms.is_str(method[1]): method = [method] else: method = [method] if initial_coordinates is None: init_coords = mesh.coordinates else: init_coords = np.asarray(initial_coordinates) if init_coords.shape[0] != mesh.coordinates.shape[0]: init_coords = init_coords.T # If steps is 0, we can skip most of this... if np.sum(max_steps) == 0: if return_report: return None else: return init_coords # Otherwise, we run at least some minimization max_pe_change = float(max_pe_change) nrounds = len(max_steps) if nrounds > 1: if len(max_step_size) == 1: max_step_size = [max_step_size[0] for _ in max_steps] if len(method) == 1: method = [method[0] for _ in max_steps] # Parse the field argument. faces = to_java_ints(mesh.tess.indexed_faces) edges = to_java_ints(mesh.tess.indexed_edges) coords = to_java_doubles(mesh.coordinates) init_coords = coords if init_coords is mesh.coordinates else to_java_doubles( init_coords) potential = _parse_field_arguments(field, faces, edges, coords) # Okay, that's basically all we need to do the minimization... rep = [] for (method, max_step_size, max_steps) in zip(method, max_step_size, max_steps): minimizer = java_link().jvm.nben.mesh.registration.Minimizer( potential, init_coords) max_step_size = float(max_step_size) max_steps = int(max_steps) if pimms.is_str(method): method = method.lower() if method == 'nimble': k = 4 else: k = 0 else: k = method[1] method = method[0].lower() if method == 'pure': r = minimizer.step(max_pe_change, max_steps, max_step_size) elif method == 'random': # if k is -1, we do the inverse version where we draw from the 1/mean distribution r = minimizer.randomStep(max_pe_change, max_steps, max_step_size, k == -1) elif method == 'nimble': r = minimizer.nimbleStep(max_pe_change, max_steps, max_step_size, int(k)) else: raise ValueError('Unrecognized method: %s' % method) rep.append(r) init_coords = minimizer.getX() # Return the report if requested if return_report: return rep else: result = init_coords return np.asarray([[x for x in row] for row in result])
def _generate_subject_DROI_boundary_data(sub, h, paradigm, angle_delta, min_variance_explained=0, method=None, eccentricity_range=(0, 7), surface_area='midgray'): import neuropythy as ny, numpy as np, copy, six CLS = VisualPerformanceFieldsDataset paradigm = paradigm.lower() erng = eccentricity_range minvexpl = min_variance_explained if paradigm == 'vertical': # This is the weird case: we handle it separately: just run the function # for both ventral and dorsal and concatenate the V1 parts (vnt, drs) = [ CLS._generate_subject_DROI_boundary_data( sub, h, para, angle_delta, min_variance_explained=minvexpl, eccentricity_range=erng, surface_area=surface_area) for para in ['ventral', 'dorsal'] ] f = CLS._vertical_DROI_from_ventral_dorsal return f(vnt, drs) # Get the hemisphere: hem = sub.hemis[h] # Setup masks and handle eccentricity_range if erng in [None, Ellipsis]: erng = (0, 7) if pimms.is_number(erng): erng = (0, erng) masks = [('inf_eccentricity', erng[0], erng[1])] dmasks = [('prf_variance_explained', min_variance_explained, 1)] # Get the inferred angle (we'll need this) angle = np.abs(hem.prop('inf_polar_angle')) # We setup two different ROIs for V1/V2 or for D/V then we join them; this # is because we aren;t 100% confident that the boundaries are drawn in the # right place, but this should let the ROI grow appropriately on either side masks = (copy.copy(masks), masks) dmasks = (copy.copy(dmasks), dmasks) if paradigm == 'ventral': dprop = 'ventral_distance' xprop = 'dorsal_distance' ref_angle = 0 for m in masks: m.append(('inf_polar_angle', -0.1, 90)) masks[0].append(('inf_visual_area', 1)) masks[1].append(('inf_visual_area', 2)) elif paradigm == 'dorsal': dprop = 'dorsal_distance' xprop = 'ventral_distance' ref_angle = 180 for m in masks: m.append(('inf_polar_angle', 90, 180.1)) masks[0].append(('inf_visual_area', 1)) masks[1].append(('inf_visual_area', 2)) elif paradigm == 'horizontal': dprop = 'horizontal_distance' xprop = None ref_angle = 90 for m in masks: m.append(('inf_visual_area', 1)) masks[0].append(('inf_polar_angle', 90, 180.1)) masks[1].append(('inf_polar_angle', -0.1, 90)) elif paradigm == 'hventral': dprop = 'horizontal_distance' xprop = None ref_angle = 90 masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 1)) masks[0].append(('inf_polar_angle', -0.1, 90)) elif paradigm == 'hdorsal': dprop = 'horizontal_distance' xprop = None ref_angle = 90 masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 1)) masks[0].append(('inf_polar_angle', 90, 180.1)) elif paradigm == 'ventral_v1': dprop = 'ventral_distance' xprop = 'dorsal_distance' ref_angle = 0 for m in masks: m.append(('inf_polar_angle', -0.1, 90)) masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 1)) elif paradigm == 'dorsal_v1': dprop = 'dorsal_distance' xprop = 'ventral_distance' ref_angle = 180 for m in masks: m.append(('inf_polar_angle', 90, 180.1)) masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 1)) elif paradigm == 'ventral_v2': dprop = 'ventral_distance' xprop = 'dorsal_distance' ref_angle = 0 for m in masks: m.append(('inf_polar_angle', -0.1, 90)) masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 2)) elif paradigm == 'dorsal_v2': dprop = 'dorsal_distance' xprop = 'ventral_distance' ref_angle = 180 for m in masks: m.append(('inf_polar_angle', 90, 180.1)) masks = (masks[0], ) dmasks = (dmasks[0], ) masks = (masks[0], ) dmasks = (dmasks[0], ) masks[0].append(('inf_visual_area', 2)) else: raise ValueError('unrecognized paradigm: %s' % (paradigm, )) # Get the indices ii = [] for (m, dm) in zip(masks, dmasks): kk = CLS._generate_DROIs(sub, h, dprop, 'prf_polar_angle', ref_angle, angle_delta, masks=m, distance_masks=dm, inv_prop=xprop, method=method) ii = np.union1d(ii, kk) ii = np.array(ii, dtype='int') # Grab the other data: if pimms.is_str( surface_area) and not surface_area.endswith('_surface_area'): surface_area = surface_area + '_surface_area' surface_area = hem.property(surface_area) sa = surface_area[ii] th = hem.prop('thickness')[ii] vl = sa * th return { 'surface_area_mm2': sa, 'mean_thickness_mm': th, 'volume_mm3': vl, 'indices': ii, 'visual_area': hem.prop('inf_visual_area')[ii] }
def calc_oriented_contrast_images(image_array, pixels_per_degree, background, gabor_spatial_frequencies, gabor_orientations=_default_gabor_orientations, max_image_size=250, min_pixels_per_degree=0.5, max_pixels_per_filter=27, ideal_filter_size=17, use_spatial_gabors=True, multiprocess=True): ''' calc_oriented_contrast_images is a calculator that takes as input an image array along with its resolution (pixels_per_degree) and a set of gabor orientations and spatial frequencies, and computes the result of filtering the images in the image array with the various filters. The results of this calculation are stored in oriented_contrast_images, which is an ordered tuple of contrast image arrays; the elements of the tuple correspond to spatial frequencies, which are given by the afferent value gabor_spatial_frequencies, and the image arrays are o x n x n where o is the number of orientations, given by the afferent value gabor_orientations, and n is the size of the height/width of the image at that particular spatial frequency. Note that the scaled_pixels_per_degree gives the pixels per degree of all of the elements of the efferent value oriented_contrast_energy_images. Required afferent values: * image_array * pixels_per_degree * background @ use_spatial_gabors Must be either True (use spatial gabor filters instead of the steerable pyramid) or False (use the steerable pyramid); by default this is True. @ gabor_orientations Must be a list of orientation angles for the Gabor filters or an integer number of evenly-spaced gabor filters to use. By default this is equivalent to 8. @ gabor_spatial_frequencies Must be a list of spatial frequencies (in cycles per degree) to use when filtering the images. @ min_pixels_per_degree Must be the minimum number of pixels per degree that will be used to represent downsampled images during contrast energy filtering. If this number is too low, the resulting filters may lose precision, while if this number is too high, the filtering may require a long time and a lot of memory. The default value is 0.5. @ max_pixels_per_filter Must be the maximum size of a filter before down-sampling the image during contrast energy filtering. If this value is None, then no rescaling is done during filtering (this may require large amounts of time and memory). By default this is 27. Note that min_pixels_per_degree has a higher precedence than this value in determining if an image is to be resized. @ ideal_filter_size May specify the ideal size of a filter when rescaling. By default this is 17. @ max_image_size Specifies that if the image array has a dimension with more pixels thatn the given value, the images should be down-sampled. Efferent output values: @ oriented_contrast_images Will be a tuple of n image arrays, each of which is of size q x m x m, where n is the number of spatial frequencies, q is the number of orientations, and m is the size of the scaled image in pixels. The scaled image pixels-per-degree values are given by scaled_pixels_per_degree. The values in the arrays represent the complex-valued results of filtering the image_array with the specified filters. @ scaled_image_arrays Will be a tuple of scaled image arrays; these are scaled to different sizes for efficiency in filtering and are stored here for debugging purposes. @ scaled_pixels_per_degree Will be a tuple of values in pixels per degree that specify the resolutions of the oriented_contrast_energy_images. @ gabor_filters Will be a tuple of gabor filters used on each of the scaled_image_arrays. ''' # process some arguments if pimms.is_number(gabor_orientations): gabor_orientations = np.pi * np.arange(0, gabor_orientations) / gabor_orientations gabor_orientations = np.asarray([pimms.mag(th, 'rad') for th in gabor_orientations]) nth = len(gabor_orientations) if min_pixels_per_degree is None: min_pixels_per_degree = 0 min_pixels_per_degree = pimms.mag(min_pixels_per_degree, 'pixels / degree') if max_pixels_per_filter is None: max_pixels_per_filter = image_array.shape[-1] max_pixels_per_filter = pimms.mag(max_pixels_per_filter, 'pixels') # if the ideal filter size is given as None, make one up if ideal_filter_size is None: ideal_filter_size = (max_pixels_per_filter + 1) / 2 + 1 ideal_filter_size = pimms.mag(ideal_filter_size, 'pixels') if ideal_filter_size > max_pixels_per_filter: ideal_filter_size = max_pixels_per_filter pool = None if multiprocess is True or pimms.is_int(multiprocess): try: import multiprocessing as mp pool = mp.Pool(mp.cpu_count() if multiprocess is True else multiprocess) except: pool = None # These will be updated as we go through the spatial frequencies: d2p0 = pimms.mag(pixels_per_degree, 'pixels/degree') imar = image_array d2p = d2p0 # how wide are the images in degrees imsz_deg = float(image_array.shape[-1]) / d2p0 # If we're over the max image size, we need to downsample now if image_array.shape[1] > max_image_size: ideal_zoom = image_array.shape[-1] / max_image_size imar = sktr.pyramid_reduce(imar.T, ideal_zoom, mode='constant', cval=background, multichannel=True).T d2p = float(imar.shape[1]) / imsz_deg # We build up these solution oces = {} # oriented contrast energy images d2ps = {} # scaled pixels per degree sims = {} # scaled image arrays flts = {} # gabor filters # walk through spatial frequencies first for cpd in reversed(sorted(gabor_spatial_frequencies)): cpd = pimms.mag(cpd, 'cycles/degree') cpp = cpd / d2p # start by making the filter... filt = gabor_kernel(cpp, theta=0) # okay, if the filt is smaller than max_pixels_per_filter, we're fine if filt.shape[0] > max_pixels_per_filter and d2p > min_pixels_per_degree: # we need to potentially resize the image, but only if it's still # higher resolution than min pixels per degree ideal_zoom = float(filt.shape[0]) / float(ideal_filter_size) # resize an image and check it out... im = sktr.pyramid_reduce(imar[0], ideal_zoom, mode='constant', cval=background, multichannel=False) new_d2p = float(im.shape[0]) / imsz_deg if new_d2p < min_pixels_per_degree: # this zoom is too much; we will try to zoom to the min d2p instead ideal_zoom = d2p / min_pixels_per_degree im = sktr.pyramid_reduce(imar[0], ideal_zoom, mode='constant', cval=background, multichannel=False) new_d2p = float(im.shape[0]) / imsz_deg # if this still didn't work, we aren't resizing, just using what we have if new_d2p < min_pixels_per_degree: new_d2p = d2p ideal_zoom = 1 # okay, at this point, we've only failed to find a d2p if ideal_zoom is 1 if ideal_zoom != 1 and new_d2p != d2p: # resize and update values imar = sktr.pyramid_reduce(imar.T, ideal_zoom, mode='constant', cval=background, multichannel=True).T d2p = new_d2p cpp = cpd / d2p # Okay! We have resized if needed, now we do all the filters for this spatial frequency if use_spatial_gabors: # Using convolution with Gabors filters = np.asarray([gabor_kernel(cpp, theta=th) for th in gabor_orientations]) if pool is None: freal = np.asarray( [[ndi.convolve(im, k.real, mode='constant', cval=background) for im in imar] for k in filters]) fimag = np.asarray( [[ndi.convolve(im, k.imag, mode='constant', cval=background) for im in imar] for k in filters]) filt_ims = freal + 1j*fimag else: iis = np.asarray(np.round(np.linspace(0, imar.shape[0], len(pool._pool))), dtype=np.int) i0s = iis[:-1] iis = iis[1:] filt_ims = np.asarray( [np.concatenate( [x for x in pool.map( _convolve_from_arg, [(imar[i0:ii],k,background) for (i0,ii) in zip(i0s,iis)]) if len(x) > 0], axis=0) for k in filters]) else: # Using the steerable pyramid filters = None if pool is None: filt_ims = np.asarray([spyr_filter(imar, th, cpp, 1, len(gabor_orientations)) for th in gabor_orientations]) else: iis = np.asarray(np.round(np.linspace(0, imar.shape[0], len(pool._pool))), dtype=np.int) i0s = iis[:-1] iis = iis[1:] filt_ims = np.asarray( [np.concatenate( [x for x in pool.map( _spyr_from_arg, [(imar[i0:ii],th,cpp,nth) for (i0,ii) in zip(i0s,iis)]) if len(x) > 0], axis=0) for th in gabor_orientations]) # add the results to the lists of results filt_ims.setflags(write=False) imar.setflags(write=False) if filters is not None: filters.setflags(write=False) oces[cpd] = filt_ims d2ps[cpd] = d2p sims[cpd] = imar flts[cpd] = filters if pool is not None: pool.close() # okay, we've finished; just mark things as read-only and make lists into tuples... cpds = [pimms.mag(cpd, 'cycles/degree') for cpd in gabor_spatial_frequencies] (oces, d2ps, sims, flts) = [tuple([m[cpd] for cpd in cpds]) for m in (oces, d2ps, sims, flts)] # and return! return {'oriented_contrast_images': oces, 'scaled_pixels_per_degree': d2ps, 'scaled_image_arrays': sims, 'gabor_filters': flts}