def sample_binary_image()

in lucid/misc/stimuli.py [0:0]


def sample_binary_image(size, alias_factor=10, color_a=(1,1,1), color_b=(0,0,0),
                        boundary_line=False, boundary_width=1,
                        blur_beyond_radius=None, fade_beyond_radius=None,
                        fade_over_distance=10, fade_color=(.5, .5, .5), **kwds):
  """Highly flexible tool for sampling binary images.

  Many stimuli of interest are "binary" in that they have two regions. For
  example, a curve stimuli has an interio and exterior region. Ideally, such a
  stimulus should be rendered with antialiasing. Additionlly, there are many
  styling options that effect how one might wish to render the image: selecting
  the color for interior and exterior, showing the boundary between the regions
  instead of interior vs exterior, and much more.

  This function provides a flexible rendering tool that supports many options.
  We assume the image is reprented in the "f-rep" or implicit funciton
  convention: the image is represented by a function which maps (x,y) values
  to a sclar, with negative representing the object interior and positive
  representing the exterior.

  The general usage would look smething like:

      @sample_binary_image(size, more_options)
      def img(x,y):
        return (negative if interior, positive if exterior)

  Or alternatively:

      sampler = sample_binary_image(size, more_options)
      def img_f(x,y):
        return (negative if interior, positive if exterior)
      img = sampler(img_f)

  Args:
    size: Size of image to be rendered in pixels.
    alias_factor: Number of samples to use in aliasing.
    color_a: Color of exterior. A 3-tuple of floats between 0 and 1. Defaults
      to white (1,1,1).
    color_b: Color of interior or boundary. A 3-tuple of floats between 0 and 1.
      Defaults to black (0,0,0).
    boundary_line: Draw boundary instead of interior vs exterior.
    boundary_width: If drawing boundary, number of pixels wide boundary line
      should be. Defaults to 1 pixel.
    blur_beyond_radius: If not None, blur the image outside a given radius.
      Defaults to None.
    fade_beyond_radius: If not None, fade the image to fade_color outside a
      given radius. Defaults to None.
    fade_over_distance: Controls rate of fading.
    fade_color: Color to fade to, if fade_beyond_radius is set. Defaults to
      (.5, .5, .5).

  Returns:
    A function which takes a function mapping (x,y) -> float and returns a
    numpy array of shape [size, size, 3].
  """


  # Initial setup
  color_a, color_b = np.asarray(color_a).reshape([1,1,3]), np.asarray(color_b).reshape([1,1,3])
  fade_color = np.asarray(fade_color).reshape([1,1,3])
  X = (np.arange(size) - size//2)
  X, Y = X[None, :], X[:, None]
  alias_offsets = [ tuple(np.random.uniform(-.5, .5, size=2)) for n in range(alias_factor) ]
  boundary_offsets = [ (boundary_width*np.cos(2*np.pi*n/16.), boundary_width*np.sin(2*np.pi*n/16.)) for n in range(16) ]

  # Setup for blur / fade stuff
  radius = np.sqrt(X**2+Y**2)
  offset_scale = 1
  fade_coef = 0
  if blur_beyond_radius is not None:
    offset_scale += np.maximum(0, radius-blur_beyond_radius)
  if fade_beyond_radius is not None:
    fade_coef = np.maximum(0, radius-fade_beyond_radius)
    fade_coef /= float(fade_over_distance)
    fade_coef = np.clip(fade_coef, 0, 1)[..., None]

  # The function we'll return.
  # E is an "energy function" mapping (x,y) -> float
  # (and vectorized for numpy support)
  # such that it is negative on interior reigions and positive on exterior ones.
  def sampler(E):

    # Naively smaple an image
    def sample(x_off, y_off):
      # note: offset_scale controls blurring
      vals = E(X + offset_scale * x_off, Y + offset_scale * y_off)
      return np.greater_equal(vals, 0).astype("float32")

    def boundary_sample(x_off, y_off):
      imgs = [sample(x_off + bd_off_x, y_off + bd_off_y)
              for bd_off_x, bd_off_y in boundary_offsets]
      # If we are on the boundary, some smaples will be zero and others one.
      # as a result, the mean will be in the middle.
      vals = np.mean(imgs, axis=0)
      vals = 2*np.abs(vals-0.5)
      return np.greater_equal(vals, 0.99).astype("float32")

    # Sample anti-aliased image
    sampler = boundary_sample if boundary_line else sample
    img = np.mean([sampler(*offset) for offset in alias_offsets], axis=0)
    img = np.clip(img, 0, 1)[..., None]

    # final transformations to colorize and fade
    img = img*color_a + (1-img)*color_b
    img = (1-fade_coef)*img + fade_coef*fade_color
    return img
  return sampler