Ejemplo n.º 1
0
 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)
Ejemplo n.º 2
0
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')
Ejemplo n.º 3
0
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
    }
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
 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'))
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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}
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
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}
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)