def __call__(self, cpd, bandwidth=1): ''' f(cpd) yields an image stack identical in shape to f.image_array which has been filtered at the given frequency cpd (in cycles per degree). The cpd argument may be in different units as long as the units are annotated using pint. Valid units are {cycles/radians/degrees} per {degrees/radians/cycles, pixels}. ''' cpp = self.to_cycles_per_pixel(cpd) # we need to calculate a new set of filtered images... # The filtered orientations res = {} bg = self.background for th in self.gabor_orientations: if self.spatial_gabors: kerns = scaled_gabor_kernel(cpp.m, theta=pimms.mag(th, 'rad')) kerns = (kerns.real, kerns.imag) rmtx = np.zeros(self.image_array.shape) for (i, im) in enumerate(self.image_array): rmtx[i, :, :] = np.sqrt( np.sum([ ndi.convolve( im, k, mode='constant', cval=self.background)** 2 for k in kerns ], axis=0)) else: rmtx = np.sqrt( spyr_filter(self.image_array, pimms.mag(th, 'rad'), cpd, bandwidth, len(self.gabor_orientations))) rmtx.setflags(write=False) res[th] = rmtx return pyr.pmap(res)
def calc_gabor_spatial_frequencies(image_array, pixels_per_degree, gabor_spatial_frequency_count=16, gabor_spatial_frequency_min=None, gabor_spatial_frequency_max=None): ''' calc_gabor_spatial_frequencies is a calculator that constructs a reasonable set of spatial frequencies (in cycles per degree) at which to filter an image given its pixels per degree. The reason for this calculator is that for a low-contrast image, a given frequency, even if visible to a human observer in theory, might be so high that we can't detect it at the given image resolution. This function picks a set of <gabor_spatial_frequency_count> frequencies that are evenly log-spaced such that no frequency is smaller than a minimum or larger than a maximum that are chosen based on image resolution. Afferent parameters: * image_array * pixels_per_degree @ gabor_spatial_frequency_count Must be the number of spatial frequencies at which to filter the images; by default this is 16. @ gabor_spatial_frequency_min Must be the minimum spatial frequency (in cycles per degree) at which to filter the images. If None, then the minimum is equivalent to one half of a cycle per image. By default, this is Ellipsis. @ gabor_spatial_frequency_max Must be the maximum spatial frequency (in cycles per degree) at which to filter the images. If None, then the maximum is equivalent to 0.5 cycles / pixel. Efferent values: @ gabor_spatial_frequencies Will be a numpy array of spatial frequency values to use in the filtering of the image arrays; values will be in cycles/degree. ''' # Process some arguments pixels_per_degree = pimms.mag(pixels_per_degree, 'pixels/degree') imsz = image_array.shape[1] imsz_deg = imsz / pixels_per_degree if gabor_spatial_frequency_min is None: gabor_spatial_frequency_min = 0.5 / imsz_deg gabor_spatial_frequency_min = pimms.mag(gabor_spatial_frequency_min, 'cycles/degree') if gabor_spatial_frequency_min == 0: raise ValueError('gabor_spatial_frequency_min must be positive') if gabor_spatial_frequency_max is None: gabor_spatial_frequency_max = 0.5 * pixels_per_degree gabor_spatial_frequency_max = pimms.mag(gabor_spatial_frequency_max, 'cycles/degree') if np.isinf(gabor_spatial_frequency_max): raise ValueError('gabor_spatial_frequency_min must be finite') # okay, lets transfer to log-space lmin = np.log(gabor_spatial_frequency_min) lmax = np.log(gabor_spatial_frequency_max) # make the spacing lfs = np.linspace(lmin, lmax, gabor_spatial_frequency_count) # return to exponential space fs = np.exp(lfs) # That's basically it! fs.setflags(write=False) return pimms.quant(fs, 'cycles/degree')
def calc_image_retinotopy(pixels_per_degree, max_eccentricity, output_pixels_per_degree=None, output_max_eccentricity=None): ''' calc_image_retinotopy calculates retinotopic coordinates (polar angle, eccentricity, label) for an output image the same size as the input images. I.e., each pixel in the input images get a single pRF center for V1, V2, and V3 (one each). ''' maxecc = pimms.mag(max_eccentricity, 'deg') if output_max_eccentricity is None else \ pimms.mag(output_max_eccentricity, 'deg') d2p = pimms.mag(pixels_per_degree, 'px / deg') if output_pixels_per_degree is None else \ pimms.mag(output_pixels_per_degree, 'px / deg') # we progressively downsample in order to make this more efficient... (ang, ecc, hem) = ([], [], []) esteps = np.concatenate([[0], 1.5 * 2**np.arange(0, np.ceil(np.log2(maxecc)), 1) ]) for (k0, k1) in zip(esteps[:-1], esteps[1:]): # make the image-data at resolution k1; we can ignore beyond k1 eccen for now dim = int(np.ceil((d2p / k1) * 2.0 * k1)) center = 0.5 * (dim - 1) # in pixels # x/y in degrees x = (np.arange(dim) - center) / (d2p / k1) # mesh grid... (x, y) = np.meshgrid(x, -x) # convert to eccen and theta/angle eccen = np.sqrt(x**2 + y**2) theta = np.arctan2(y, x) angle = np.mod(90 - 180.0 / np.pi * theta + 180, 360.0) - 180 # get hemispheres hemis = np.sign(angle) hemis[hemis == 0] = 1.0 # find the relevant pixels ii = (k0 <= eccen) & (eccen < k1) ang.append(angle[ii]) ecc.append(eccen[ii]) hem.append(hemis[ii]) # and turn these into lists with visual area labels (angle, eccen, hemis) = [np.concatenate(u) for u in (ang, ecc, hem)] label = np.concatenate([np.full(len(angle), k) for k in (1, 2, 3)]) (angle, eccen, hemis) = [np.concatenate((u, u, u)) for u in (angle, eccen, hemis)] for u in (angle, eccen, label, hemis): u.setflags(write=False) return { 'polar_angles': pimms.quant(angle, 'deg'), 'eccentricities': pimms.quant(eccen, 'deg'), 'labels': label, 'hemispheres': hemis }
def divisively_normalize_spatialfreq(data, cpds, background=0.5, divisive_exponent=2, saturation_constant=0.1): ''' Divisively normalizes data taking into account the previous and following spatial frequency level. Data is the 4D decomposition of an image into spatial frequencies and orientations, such as the result of the steerable pyramid transform. Author: Chrysa Papadaniil <*****@*****.**> ''' s = saturation_constant r = divisive_exponent ii = np.argsort([pimms.mag(cpd, 'cycles/degree') for cpd in cpds]) if not np.array_equal(ii, range(len(ii))): cpds = cpds[ii] data = data[ii] numlevels = len(data) normalizers = [np.sum(d, axis=0) for d in data] normalized = [] for level in range(numlevels): neis = [ii for ii in (level-1, level+1) if ii >=0 if i < numlevels] n0 = np.array(normalizers[level]) for ii in neis: ni = normalizers[ii] if ni.shape[0] != n0.shape[0]: ni = np.array(ni) zoom = float(n0.shape[0]) / float(ni.shape[0]) f = sktr.pyramid_expand if zoom > 1 else sktr.pyramid_reduce n0 += f(ni, zoom, mode='constant', cval=background, multichannel=False) n0 /= len(neis) # use the mean normalized.append(np.mean(data[level]**r, axis=0) / (s**r + n0**r)) for nrm in normalized: nrm.setflags(write=False) return tuple(normalized)
def spatial_frequency_sensitivity(prf, cpds): ''' sco.impl.kay13.spatial_frequency_sensitivity(prf, cpds) always yields a narrow band of sensitivity to spatial frequencies near 3 cycles/degree. ''' p0 = 3.0 lf0 = _np.log2(1/p0) # weights are log-gaussian distributed around the preferred period cpds = _np.log2(_pimms.mag(cpds, 'cycles/degree')) ws = _np.exp(-0.5 * ((cpds - lf0)/0.25)**2) return ws / _np.sum(ws)
def test_grating(max_eccentricity=10, pixels_per_degree=6.4, theta=0, mod_theta=np.pi / 2, spatial_frequency=3, mod_spatial_frequency=None, contrast=1, mod_contrast=0): ''' test_grating() yields a test-image; this is typically an image with a single luminance grating that is modulated by a contrast grating. By default the contrast grating is flat, so there is effectively no contrast grating, but this any many other features of the image may be manipulated via the various optional parameters. Optional arguments: * max_eccentricity (default: 10 degrees) specifies the max eccentricity to include in the image; this is used in combination with pixels_per_degree. * pixels_per_dgree (default: 6.4 px/deg) specifies the number of pixels per degree. * theta (default: 0) specifies the angle of the luminance gradient. * mod_theta (default: pi/2) specifies the angle of the modulating contrast gradient. * spatial_frequency (default: 3 cycles/deg) specifies the spatial frequency of the luminance grating. * mod_spatial_frequency (default: 0.5 cycles/deg) specifies the spatial frequency of the modulating contrast grating. * contrast (default: 1) specifies the contrast of the luminance grating. * mod_contrast (default: 0) specifies the amount of contrast in the modulated grating. Note that the default value of 0 will result in an image without a modulated contrast grating. ''' import warnings # Process/organize arguments maxecc = pimms.mag(max_eccentricity, 'deg') d2p = pimms.mag(pixels_per_degree, 'px/deg') theta = pimms.mag(theta, 'rad') modth = pimms.mag(mod_theta, 'rad') cpp = pimms.mag(spatial_frequency, 'turns/deg') / d2p # go ahead and setup x and y values (in degrees) for the images dim = np.round(d2p * 2.0 * maxecc) # Luminance grating im = luminance_grating(pixels=dim, cpp=cpp, theta=theta, contrast=contrast) # Contrast grating if None not in [modth, mod_spatial_frequency, mod_contrast]: mod_cpp = pimms.mag(mod_spatial_frequency, 'turns/deg') / d2p mod_im = luminance_grating(dim, cpp=mod_cpp, theta=mod_theta, contrast=mod_contrast) # multiply the two together im = (im - 0.5) * mod_im + 0.5 return im
def test_units(self): ''' test_units ensures that the various pimms functions related to pint integration work correctly; these functions include pimms.unit, .mag, .quant, .is_quantity, etc. ''' # make a few pieces of data with types x = np.asarray([1.0, 2.0, 3.0, 4.0]) * pimms.units.mm y = pimms.quant([2, 4, 6, 8], 'sec') for u in [x, y]: self.assertTrue(pimms.is_quantity(u)) for u in ('abc', 123, 9.0, []): self.assertFalse(pimms.is_quantity(u)) for u in [x, y]: self.assertFalse(pimms.is_quantity(pimms.mag(u))) self.assertTrue(pimms.like_units(pimms.unit(x), pimms.unit('yards'))) self.assertTrue(pimms.like_units(pimms.unit(y), pimms.unit('minutes'))) self.assertFalse(pimms.like_units(pimms.unit(y), pimms.unit('mm'))) z = x / y self.assertTrue(pimms.is_vector(x, 'real')) self.assertTrue(pimms.is_vector(y, 'real')) self.assertFalse(pimms.is_vector(x, 'int')) self.assertTrue(pimms.is_vector(y, 'int')) self.assertTrue(pimms.is_vector(y, 'float')) self.assertTrue(pimms.is_vector(z, 'real'))
def test_stimuli(stimulus_directory, max_eccentricity=10.0, pixels_per_degree=6.4, orientations=None, spatial_frequencies=None, contrasts=None, densities=None, base_density=10, base_contrast=0.75, base_spatial_frequency=0.8, base_orientation=0): ''' test_stimuli(stimulus_directory) creates a variety of test png files in the given directory. The image filenames are returned as a list. The images created are as follows: * A blank gray image * A variety of sinusoidal gratings; these vary in terms of: * contrast * spatial frequency * orientation * A variety of modulated sinusoidal gratings; in which the modulated spatial frequency is held at 1 cycle / degree and the modulated contrast is as high as possible; these vary in terms of: * contrast * spatial frequency * orientation * modulated orientation The following options may be given: * max_eccentricity (default: 10.0) specifies the maximum eccentricity in degrees to include in the generated images. * pixels_per_degree (default: 6.4) specifies the pixels per degree of the created images. * orientations (default: None) specifies the orientations (NOTE: in degrees) of the various gratings generated; if None, then uses [0, 30, 60, 90, 120, 150]. * spatial_frequencies (defaut: None) specifies the spatial frequencies to use; by default uses a set of 5 spatial frequencies that depend on the resolution specified by pixels_per_degree. * contrasts (default: None) specifies the contrasts of the images to make; if None, then uses [0.25, 0.5, 0.75, 1.0]. ''' import skimage.io, warnings # Process/organize arguments maxecc = pimms.mag(max_eccentricity, 'deg') d2p = pimms.mag(pixels_per_degree, 'px/deg') sdir = stimulus_directory thetas = np.arange( 0, 180, 22.5) if orientations is None else np.asarray(orientations) sfreqs = (d2p/4) * (2**np.linspace(-4.0, 0.0, 5)) if spatial_frequencies is None else \ pimms.mag(spatial_frequencies, 'cycles/degree') ctsts = [0.25, 0.5, 0.75, 1.0] if contrasts is None else contrasts dnsts = [3, 6, 12, 24, 48] if densities is None else densities cpd0 = pimms.mag(base_spatial_frequency, 'cycles/degree') th0 = pimms.mag(base_orientation, 'deg') * np.pi / 180 ct0 = base_contrast dn0 = base_density # go ahead and setup x and y values (in degrees) for the images dim = np.round(d2p * 2.0 * maxecc) center = 0.5 * (dim - 1) # in pixels # x/y in pixels x = np.arange(0, dim, 1) # x/y in degrees x = (x - center) / d2p # mesh grid... (x, y) = np.meshgrid(x, x) # how we save images flnms = [] fldat = [] def _imsave(nm, im): flnm = os.path.join(sdir, nm + '.png') flnms.append(flnm) return imsave(flnm, im) def _immeta(meta, **kw): meta = pimms.merge(meta, kw) fldat.append(meta) return meta # have to catch the UserWarnings for low contrast images with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=UserWarning) # Generate the basic sin-grating-based images # First we want varying orientation, all at cpd=1 meta = { 'cpd': cpd0, 'contrast': ct0, 'type': 'sin_orientation', 'theta': th0 } for theta_deg in sorted(thetas): theta = np.pi / 180 * theta_deg im = 0.5 * (1 + np.sin( (np.cos(theta) * x - np.sin(theta) * y) * 2 * np.pi * cpd0)) if ct0 < 1: im = (im - 0.5) * ct0 + 0.5 _imsave('sin[theta=%06.3f]' % theta, im) _immeta(meta, theta=theta) # okay, now for theta=0 and variable cpd for cpd in reversed(sorted(sfreqs)): im = 0.5 * (1 + np.sin( (np.cos(th0) * x - np.sin(th0) * y) * 2 * np.pi * cpd)) if ct0 < 1: im = (im - 0.5) * ct0 + 0.5 # write it out _imsave('sin[cpd=%06.3f]' % cpd, im) _immeta(meta, cpd=cpd, type='sin_frequency') # Now we can look at the requested contrasts for ct in sorted(ctsts): # save the simple grating first im = 0.5 * (1 + np.sin( (np.cos(th0) * x - np.sin(th0) * y) * 2 * np.pi * cpd0)) if ct < 1: im = (im - 0.5) * ct + 0.5 _imsave('sin[contrast=%06.3f]' % ct, im) _immeta(meta, contrast=ct, type='sin_contrast') # Next, plaids; these are relatively easy: x0 = np.cos(th0) * x - np.sin(th0) * y y0 = np.sin(th0) * x + np.cos(th0) * y im0 = 0.25 * (2 + np.sin(x0 * 2 * np.pi * cpd0) + np.sin(y0 * 2 * np.pi * cpd0)) im0 = (im0 - 0.5) / np.max(np.abs(im0 - 0.5)) + 0.5 for ct in sorted(ctsts): if ct == 0: continue if ct < 1: im = (im0 - 0.5) * ct + 0.5 else: im = im0 _imsave('plaid[contrast=%06.3f]' % ct, im) _immeta(meta, contrast=ct, type='plaid_contrast') # okay, now the sum of sixteen gratings const = 2 * np.pi * cpd0 for ct in sorted(ctsts): if ct == 0: continue im0 = np.mean([ 0.5 * (1 + np.sin(const * (np.random.rand() - 0.5 + np.cos(th) * x - np.sin(th) * y))) for th in np.linspace(0, 2 * np.pi, 17)[:-1] ], axis=0) im0 = (im0 - 0.5) / np.max(np.abs(im0 - 0.5)) + 0.5 if ct < 1: im = (im0 - 0.5) * ct + 0.5 else: im = im0 _imsave('circ[contrast=%06.3f]' % ct, im) _immeta(meta, contrast=ct, type='circ_contrast') # Okay, make the noise pattern images dmeta = pyr.pmap(meta).discard('orientation').set('cpd', cpd0) for dn in dnsts: if dn <= 0: continue im = noise_pattern_stimulus(x.shape[0], dn, ct0, cpd0 / 2) _imsave('noise[density=%02d]' % dn, im) _immeta(dmeta, density=dn, type='noise_density') # Make fldat into a pimms itable fldat = [m.set('filename', flnm) for (m, flnm) in zip(fldat, flnms)] tbl = pimms.itable({ k: np.asarray([ff[k] for ff in fldat]) for k in six.iterkeys(fldat[0]) }) return tbl
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}
def calc_pRF_contrasts(pRFs, normalized_contrast_images, scaled_pixels_per_degree, spatial_frequency_sensitivity_function, gabor_spatial_frequencies, contrast_constants, multiprocess=True): ''' calc_pRF_contrasts is a calculator that is responsible for calculating both the first order and the second order contrasts within each pRF. The first order contrast (pRF_FOC) is just the weighted mean of the contrasts within each pRF. The second order contrast is the second moment of the contrast about (c * pRF_FOC) where c is the contrast_constant for the pRF. The calculations are made using normalized_contrast_images, which represents the contrast after divisive normalization. Required afferent parameters: * pRFs * normalized_contrast_images * scaled_pixels_per_degree * contrast_constants * gabor_spatial_frequencies @ spatial_frequency_sensitivity_function Must be a function of two arguments: a PRFSpec object and a a numpy array of spatial frequencies. The return value should be a list of weights, one per spatial frequency, for the particular pRF. Provided efferent parameters: @ pRF_FOC Will be an array of the first-order-contrast values, one per pRF per image; these will be stored in an (n x m) matrix where n is the number of pRFs and m is the number of images. @ pRF_SOC Will be an array of the second-order-contrast values, one per pRF per image; these will be stored in an (n x m) matrix where n is the number of pRFs and m is the number of images. Note that this is not the variance of the contrast exactly, but is the weighted second moment of the contrast about the weighted mean (pRF_FOC). ''' sfsf = spatial_frequency_sensitivity_function if pimms.is_str(sfsf): sfsf = global_lookup(sfsf) gabor_spatial_frequencies = pimms.mag(gabor_spatial_frequencies, 'cycles/degree') # Okay, we're going to walk through the pRFs ok = False if multiprocess: pool = None try: import multiprocessing as mp foc = np.zeros((len(pRFs), len(normalized_contrast_images[0])), dtype=np.float) soc = np.zeros((len(pRFs), len(normalized_contrast_images[0])), dtype=np.float) pool = mp.Pool(mp.cpu_count() if multiprocess is True else multiprocess) idcs = np.array(np.round(np.linspace(0, len(pRFs), pool._processes + 1)), dtype=np.int) idcs = np.asarray([ii for ii in zip(idcs[:-1],idcs[1:])]) tmp = [(pRFs[i0:ii], normalized_contrast_images, scaled_pixels_per_degree, contrast_constants[i0:ii], gabor_spatial_frequencies, sfsf) for (i0,ii) in idcs] tmp = pool.map(_pRF_contrasts, tmp) for ((f,s),(i0,ii)) in zip(tmp, idcs): foc[i0:ii] = f soc[i0:ii] = s ok = True except: pass finally: if pool is not None: pool.close() if not ok: (foc, soc) = _pRF_contrasts((pRFs, normalized_contrast_images, scaled_pixels_per_degree, contrast_constants, gabor_spatial_frequencies, sfsf)) # That's all! foc.setflags(write=False) soc.setflags(write=False) return (foc, soc)
def calc_images(pixels_per_degree, stimulus_map, stimulus_ordering, max_image_size=250, background=0.5, aperture_radius=None, aperture_edge_width=None): ''' calc_images() is a the calculation that converts the imported_stimuli value into the normalized images value. Required afferent parameters: @ pixels_per_degree Must specify the number of pixels per degree in the input images; note that all stimulus images must have the same pixels_per_degree value. @ stimulus_map Must be a map whose values are 2D image matrices (see import_stimuli). @ stimulus_ordering Must be a list of the stimulus filenames or IDs (used by calc_images to ensure the ordering of the resulting image_array datum is correct; see also import_stimuli). Optional afferent parameters: @ background Specifies the background color of the stimulus; by default this is 0.5 (gray); this is only used if an aperture is applied. @ aperture_radius Specifies the radius of the aperture in degrees; by default this is None, indicating that no aperture should be used; otherwise the aperture is applied after normalizing the images. @ aperture_edge_width Specifies the width of the aperture edge in degrees; by default this is None; if 0 or None, then no aperture edge is used. Output efferent values: @ image_array Will be the 3D numpy array image stack; image_array[i,j,k] is the pixel in image i, row j, column k @ image_names Will be the list of image names in the same order as the images in image_array; the names are derived from the keys of the stimulus_map. @ pixel_centers Will be an r x c x 2 numpy matrix with units of degrees specifying the center of each pixel (r is the number of rows and c is the number of columns). ''' # first, let's interpret our arguments deg2px = float(pimms.mag(pixels_per_degree, 'px/deg')) imgs = stimulus_map maxdims = [np.max([im.shape[i] for im in six.itervalues(imgs)]) for i in [0,1]] # Then apply the aperture if aperture_radius is None: aperture_radius = 0.5 * np.max(maxdims) / deg2px if aperture_edge_width is None: aperture_edge_width = 0 rad_px = 0 try: rad_px = pimms.mag(aperture_radius, 'deg') * deg2px except: try: rad_px = pimms.mag(aperture_radius, 'px') except: raise ValuerError('aperture_radius given in unrecognized units') aew_px = 0 try: aew_px = pimms.mag(aperture_edge_width, 'deg') * deg2px except: try: aew_px = pimms.mag(aperture_edge_width, 'px') except: raise ValuerError('aperture_edge_width given in unrecognized units') bg = background imgs = {k:image_apply_aperture(im, rad_px, fill_value=bg, edge_width=aew_px) for (k,im) in six.iteritems(imgs)} # Separate out the images and names and imar = np.asarray([imgs[k] for k in stimulus_ordering], dtype=np.float) imar.setflags(write=False) imnm = pyr.pvector(stimulus_ordering) # Finally, note the pixel centers (rs,cs) = (imar.shape[1], imar.shape[2]) x0 = (0.5*rs, 0.5*cs) (r0s, c0s) = [(np.asarray(range(u)) - 0.5*u + 0.5) / deg2px for u in [rs,cs]] pxcs = np.asarray([[(c,-r) for c in c0s] for r in r0s], dtype=np.float) pxcs.setflags(write=False) return {'image_array': imar, 'image_names': imnm, 'pixel_centers': pxcs}
def calc_retinotopy(benson14_polar_angles, benson14_eccentricities, benson14_labels, benson14_hemispheres, benson14_cortex_indices, benson14_cortex_coordinates, measured_polar_angles, measured_eccentricities, measured_labels, measured_hemispheres, measured_cortex_indices, measured_cortex_coordinates, freesurfer_cortex_affine, measured_cortex_affine, modality, import_filter=None): ''' calc_retinotopy is a calculator that unifies the various modes of importing retinotopic data and yields a set of general values (polar angle, eccentricity, labels, etc) that are used downstream by the SCO model. Afferent values: @ import_filter If specified, may give a function that accepts four parameters: f(polar_angle, eccentricity, label, hemi); if this function fails to return True for the appropriate values of a particular vertex/voxel, then that vertex/voxel is not included in the prediction. ''' b14 = (benson14_polar_angles, benson14_eccentricities, benson14_labels, benson14_hemispheres, benson14_cortex_indices, benson14_cortex_coordinates, freesurfer_cortex_affine) msd = (measured_polar_angles, measured_eccentricities, measured_labels, measured_hemispheres, measured_cortex_indices, measured_cortex_coordinates, measured_cortex_affine) if all(x is not None for x in msd): dat = msd elif all(x is not None for x in b14): dat = b14 else: raise ValueError( 'Neither benson14 nor measured retinotopy import succeeded') (angles, eccens, labels, hemis, idcs, coords, tx) = dat if tx is None and freesurfer_cortex_affine is not None: tx = freesurfer_cortex_affine angles = pimms.mag(angles, 'deg') eccens = pimms.mag(eccens, 'deg') if import_filter is not None: sels = [ i for (i, (p, e, l, h)) in enumerate(zip(angles, eccens, labels, hemis)) if import_filter(p, e, l, h) ] (angles, eccens, labels, hemis, idcs, coords) = [ x[sels] for x in (angles, eccens, labels, hemis, idcs, coords) ] res = { 'polar_angles': pimms.quant(angles, 'deg'), 'eccentricities': pimms.quant(eccens, 'deg'), 'labels': labels, 'cortex_indices': idcs, 'cortex_coordinates': coords, 'hemispheres': hemis, 'cortex_affine': tx } return res
def cortical_image(prediction, labels, pRFs, max_eccentricity, image_number=None, visual_area=1, image_size=200, n_stds=1, size_gain=1, method='triangulation', axes=None, smoothing=None, cmap='afmhot', clipping=None, speckle=None): ''' cortical_image(pred, labels, pRFs, maxecc) yields an array of figures that reconstruct the cortical images for the given sco results datapool. The image is always sized such that the width and height span 2 * max_eccentricity degrees (where max_eccentricity is stored in the datapool). The following options may be given: * visual_area (default: 1) must be 1, 2, or 3 and specifies whether to construct the cortical image according to V1, V2, or V3. * image_number (default: None) specifies which image to construct a cortical image of. If this is given as None, then a list of all images in the datapool is returned; otherwise only the figure for the image number specified is returned. * image_size (default: 200) specifies the width and height of the image in pixels (only for method equal to 'pRF_projection'. * n_stds (default: 1) specifies how many standard deviations should be used with each pRF when projecting the values from the cortical surface back into the image. * size_gain (default: 1) specifies a number to multiply the pRF size by when projecting into the image. * method (default: 'triangulation') should be either 'triangulation' or 'pRF_projection'. The former makes the plot by constructing the triangulation of the pRF centers and filling the triangles in while the latter creates an image matrix and projects the pRF predicted responses into the relevant pRFs. * cmap (default: matplotlib.cm.afmhot) specifies the colormap to use. * smoothing (default: None) if a number between 0 and 1, smoothes the data using the basic mesh smoothing routine in neurpythy with the given number as the smoothing ratio. * clipping (defaut: None) indicates the z-range of the data that should be plotted; this may be specified as None (don't clip the data) a tuple (minp, maxp) of the minimum and maximum percentile that should be used, or a list [min, max] of the min and max value that should be plotted. * speckle (default: None), if not None, must be an integer that gives the number points to randomly add before smoothing; these points essentially fill in space on the image if the vertices for a triangulation are sparse. This is only used if smoothing is also used. ''' import matplotlib import matplotlib.pyplot as plt import matplotlib.tri as tri import matplotlib.cm as cm # if not given an image number, we want to iterate through all images: if image_number is None: return np.asarray([ cortical_image(datapool, visual_area=visual_area, image_number=ii, image_size=image_size, n_stds=n_stds, method=method, axes=axes) for ii in range(len(datapool['image_array'])) ]) if axes is None: axes = plt.gca() # Some parameter handling: maxecc = float(pimms.mag(max_eccentricity, 'deg')) centers = np.asarray([ pimms.mag(p.center, 'deg') if l == visual_area else (0, 0) for (p, l) in zip(pRFs, labels) ]) sigs = np.asarray([ pimms.mag(p.radius, 'deg') if l == visual_area else 0 for (p, l) in zip(pRFs, labels) ]) z = prediction[:, image_number] (x, y, z, sigs) = np.transpose([(xx, yy, zz, ss) for ((xx, yy), zz, ss, l) in zip(centers, z, sigs, labels) if l == visual_area and not np.isnan(zz)]) clipfn = (lambda zz: (None,None)) if clipping is None else \ (lambda zz: np.percentile(z, clipping)) if isinstance(clipping, tuple) else \ (lambda zz: clipping) cmap = getattr(cm, cmap) if pimms.is_str(cmap) else cmap if method == 'triangulation': if smoothing is None: t = tri.Triangulation(x, y) else: if speckle is None: t = tri.Triangulation(x, y) else: n0 = len(x) maxrad = np.sqrt(np.max(x**2 + y**2)) (rr, rt) = (maxrad * np.random.random(speckle), np.pi * 2 * np.random.random(speckle)) (x, y) = np.concatenate( ([x, y], [rr * np.cos(rt), rr * np.sin(rt)]), axis=1) z = np.concatenate((z, np.full(speckle, np.inf))) t = tri.Triangulation(x, y) # make a cortical mesh coords = np.asarray([x, y]) msh = ny.Mesh(coords, t.triangles.T) z = ny.mesh_smooth( msh, z, smoothness=smoothing, ) (mn, mx) = clipfn(z) axes.tripcolor(t, z, cmap=cmap, shading='gouraud', vmin=mn, vmax=mx) axes.axis('equal') axes.axis('off') return plt.gcf() elif method == 'pRF_projection': # otherwise, we operate on a single image: img = np.zeros((image_size, image_size, 2)) img_center = (float(image_size) * 0.5, float(image_size) * 0.5) img_scale = (img_center[0] / maxecc, img_center[1] / maxecc) for (xx, yy, zz, ss) in zip(x, y, z, sigs): ss = ss * img_scale[0] * size_gain exp_const = -0.5 / (ss * ss) row = yy * img_scale[0] + img_center[0] col = xx * img_scale[1] + img_center[1] if row < 0 or col < 0 or row >= image_size or col >= image_size: continue r0 = max([0, int(round(row - ss))]) rr = min([image_size, int(round(row + ss))]) c0 = max([0, int(round(col - ss))]) cc = min([image_size, int(round(col + ss))]) (mesh_xs, mesh_ys) = np.meshgrid( np.asarray(range(c0, cc), dtype=np.float) - col, np.asarray(range(r0, rr), dtype=np.float) - row) gaus = np.exp(exp_const * (mesh_xs**2 + mesh_ys**2)) img[r0:rr, c0:cc, 0] += zz img[r0:rr, c0:cc, 1] += gaus img = np.flipud(img[:, :, 0] / (img[:, :, 1] + (1.0 - img[:, :, 1].astype(bool)))) fig = plotter.figure() (mn, mx) = clipfn(img.flatten()) plotter.imshow(img, cmap=cmap, vmin=mn, vmax=mx) plotter.axis('equal') plotter.axis('off') return fig else: raise ValueError('unrecognized method: %s' % method)