8

AUTOSTEREOGRAMS

Image

Stare at Figure 8-1 for a minute. Do you see anything other than random dots? Figure 8-1 is an autostereogram, a two-dimensional image that creates the illusion of three dimensions. Autostereograms usually consist of repeating patterns that resolve into three dimensions on closer inspection. If you can’t see any sort of image, don’t worry; it took me a while and a bit of experimentation before I could. (If you aren’t having any luck with the version printed in this book, try the color version here: https://github.com/electronut/pp/images/. The footnote to the caption reveals what you should see.)

In this project, you’ll use Python to create an autostereogram. Here are some of the concepts covered in this project:

• Linear spacing and depth perception

• Depth maps

• Creating and editing images using Pillow

• Drawing into images using Pillow

Image

Figure 8-1: A puzzling image that might gnaw at you1

The autostereograms you’ll generate in this project are designed for “wall-eyed” viewing. The best way to see them is to focus your eyes on a point behind the image (such as a wall). Almost magically, once you perceive something in the patterns, your eyes should automatically bring it into focus, and when the three-dimensional image “locks in,” you will have a hard time shaking it off. (If you’re still having trouble viewing the image, see Gene Levin’s article “How to View Stereograms and Viewing Practice”2 for help.)

How It Works

An autostereogram works by changing the linear spacing between patterns in an image, thereby creating the illusion of depth. When you look at repeating patterns in an autostereogram, your brain can interpret the spacing as depth information, especially if there are multiple patterns with different spacing.

Perceiving Depth in an Autostereogram

When your eyes converge at an imaginary point behind the image, your brain matches the points seen with your left eye with a different group seen by your right eye, and you see these points lying on a plane behind the image. The perceived distance to this plane depends on the amount of spacing in the pattern. For example, Figure 8-2 shows three rows of As. The As are equidistant within each row, but their horizontal spacing increases from top to bottom.

Image

Figure 8-2: Linear spacing and depth perception

When this image is viewed “wall-eyed,” the top row in Figure 8-2 should appear to be behind the paper, the middle row should look like it’s a little behind the first row, and the bottom row should appear farthest from your eye. The text that says floating text should appear to “float” on top of these rows.

Why does your brain interpret the spacing between these patterns as depth? Normally, when you look at a distant object, your eyes work together to focus and converge at the same point, with both eyes rotating inward to point directly at the object. But when viewing a “wall-eyed” autostereogram, focus and convergence happen at different locations. Your eyes focus on the autostereogram, but your brain sees the repeated patterns as coming from the same virtual (imaginary) object, and your eyes converge on a point behind the image, as shown in Figure 8-3. This combination of decoupled focus and convergence allows you to see depth in an autostereogram.

Image

Figure 8-3: Seeing depth in autostereograms

The perceived depth of the autostereogram depends on the horizontal spacing of pixels. Because the first row in Figure 8-2 has the closest spacing, it appears in front of the other rows. However, if the spacing of the points were varied in the image, your brain would perceive each point at a different depth, and you could see a virtual three-dimensional image appear.

Depth Maps

A depth map is an image where the value of each pixel represents a depth value, which is the distance from the eye to the part of the object represented by that pixel. Depth maps are often shown as a grayscale image with light areas for nearby points and darker areas for points farther away, as shown in Figure 8-4.

Image

Figure 8-4: A depth map

Notice that the nose of the shark, the lightest part of the image, seems closest to you. The darker area toward the tail seems farthest away.

Because the depth map represents the depth or distance from the center of each pixel to the eye, you can use it to get the depth value associated with a pixel location in the image. You know that horizontal shifts are perceived as depth in images. So if you shift a pixel in a (patterned) image proportionally to the corresponding pixel’s depth value, you would create a depth perception for that pixel consistent with the depth map. If you do this for all pixels, you will end up encoding the entire depth map into the image, creating an autostereogram.

Depth maps store depth values for each pixel, and the resolution of the value depends on the number of bits used to represent it. Because you will be using common 8-bit images in this chapter, depth values will be in the range [0, 255].

By the way, the image in Figure 8-4 is the same depth map used to create the first autostereogram shown in Figure 8-1. You will soon learn how to do this yourself.

The code for this project will follow these steps:

1. Read in a depth map.

2. Read in a tile image or create a “random dot” tile.

3. Create a new image by repeating the tile. The dimensions of this image should match those of the depth map.

4. For each pixel in the new image, shift the pixel to the right by an amount proportional to the depth value associated with the pixel.

5. Write the autostereogram to a file.

Requirements

In this project, you’ll use Pillow to read in images, access their underlying data, and create and modify images.

The Code

To create an autostereogram from an input depth map image, you’ll first generate an intermediate image by repeating a given tile image. Next you’ll create a tile image filled with random dots. You’ll then go through the core code that creates an autostereogram by shifting an input image using information from a supplied depth map image. To see the complete project, skip ahead to “The Complete Code” on page 125.

Repeating a Given Tile

Let’s start by using the createTiledImage() method to tile a graphics file and create a new image with the dimensions specified by the tuple dims of the form (width, height).

   # tile a graphics file to create an intermediate image of a set size
   def createTiledImage(tile, dims):
       # create the new image
      img = Image.new('RGB', dims)
       W, H = dims
       w, h = tile.size
       # calculate the number of tiles needed
      cols = int(W/w) + 1
      rows = int(H/h) + 1
       # paste the tiles into the image
       for i in range(rows):
           for j in range(cols):
              img.paste(tile, (j*w, i*h))
       # output the image
       return img

At , you create a new Python Imaging Library (PIL) Image object using the supplied dimensions (dims). The dimensions of the new image are given as the tuple dims of the form (width, height). Next, store the width and height of both the tile and the output files. At , you determine the number of columns, and at , you determine the number of rows you need to have in the intermediate image by dividing the final image dimension by those of the tile. Add 1 to each measurement to make sure that the last tile on the right is not missed when the output image dimension is not an exact integer multiple of the tile dimension. Without this precaution, the right side of the image might be cut off. Then, at , loop through the rows and columns and fill them with tiles. Determine the location of the top-left corner of the tile by multiplying (j*w, i*h) so it aligns with the rows and columns. Once complete, the method returns an Image object of the specified dimensions, tiled with the input image tile.

Creating a Tile from Random Circles

If the user doesn’t provide a tile image, create a tile with random circles using the createRandomTile() method.

   # create an image tile filled with random circles
   def createRandomTile(dims):
       # create image
      img = Image.new('RGB', dims)
      draw = ImageDraw.Draw(img)
       # set the radius of a random circle to 1% of
       # width or height, whichever is smaller
      r = int(min(*dims)/100)
       # number of circles
      n = 1000
       # draw random circles
       for i in range(n):
           # -r makes sure that the circles stay inside and aren't cut off
           # at the edges of the image so that they'll look better when tiled
          x, y = random.randint(0, dims[0]-r), random.randint(0, dims[1]-r)
          fill = (random.randint(0, 255), random.randint(0, 255),
                   random.randint(0, 255))
          draw.ellipse((x-r, y-r, x+r, y+r), fill)
       return img

At , you create a new Image with the dimensions given by dim. Use ImageDraw.Draw() to draw circles inside the image with an arbitrarily chosen radius of 1/100th of either the width or the height of the image, whichever is smaller . (The Python * operator unpacks the width and height values in the dims tuple so that it can be passed into the min() method.)

At , set the number of circles to draw to 1000. Then calculate the x- and y-coordinates of each circle by calling random.randint() to get random integers in the range [0, width-r] and [0, height-r] . The -r makes sure the generated circles stay inside the image rectangle of dimensions width × height. Without the -r, you could end up drawing a circle right at the edge of the image, which means it would be partly cut off. If you tiled such an image to create the autostereogram, the result wouldn’t look good because the circles at the edge between two tiles would have no spacing between them.

To create a random circle, first draw the outline and then fill in a color. At , you select a color for the fill by randomly choosing RGB values from the range [0, 255]. Finally, at , you use the ellipse() method in draw to draw each of your circles. The first argument to this method is the bounding box of the circle, which is given by the top-left and bottom-right corners as (x-r, y-r) and (x+r, y+r), respectively, where (x, y) is the center of the circle and r is its radius.

Let’s test this method in the Python interpreter.

>>> import autos
>>> img = autos.createRandomTile((256, 256))
>>> img.save('out.png')
>>> exit()

Figure 8-5 shows the output from the test.

Image

Figure 8-5: Sample run of createRandomTile()

As you can see in Figure 8-5, you have created a tile image with random dots. You’ll use this to create the autostereogram.

Creating Autostereograms

Now let’s create some autostereograms. The createAutostereogram() method does most of the work. Here it is:

   def createAutostereogram(dmap, tile):
       # convert the depth map to a single channel if needed
      if dmap.mode is not 'L':
           dmap = dmap.convert('L')

       # if no image is specified for a tile, create a random circles tile
      if not tile:
           tile = createRandomTile((100, 100))
       # create an image by tiling
      img = createTiledImage(tile, dmap.size)
       # create a shifted image using depth map values
      sImg = img.copy()
       # get access to image pixels by loading the Image object first
      pixD = dmap.load()
       pixS = sImg.load()
       # shift pixels horizontally based on depth map
      cols, rows = sImg.size
       for j in range(rows):
           for i in range(cols):
              xshift = pixD[i, j]/10
              xpos = i - tile.size[0] + xshift
              if xpos > 0 and xpos < cols:
                  pixS[i, j] = pixS[xpos, j]
       # display the shifted image
       return sImg

At , you perform a sanity check to ensure that the depth map and the image have the same dimensions. If the user doesn’t supply an image for the tile, you create a random circle tile at . At , you create a tile that matches the size of the supplied depth map image. You then make a copy of this tiled image at .

At , you use the Image.Load() method, which loads image data into memory. This method allows accessing image pixels as a two-dimensional array in the form [i, j]. At , you store the image dimensions as a number of rows and columns, treating the image as a grid of individual pixels.

The core of the autostereogram creation algorithm lies in the way you shift the pixels in the tiled image according to the information gathered from the depth map. To do this, iterate through the tiled image and process each pixel. At , you look up the value of the shift associated with a pixel from the depth map pixD. You then divide this depth value by 10 because you are using 8-bit depth maps here, which means the depth varies from 0 to 255. If you divide these values by 10, you get depth values in the approximate range of 0 to 25. Since the depth map input image dimensions are usually in the hundreds of pixels, these shift values work fine. (Play around by changing the value you divide by to see how it affects the final image.)

At , you calculate the new x position of the pixel by filling the autostereogram with the tiles. The value of a pixel keeps repeating every w pixels and is expressed by the formula ai = ai + w, where ai is the color of a given pixel at index i along the x-axis. (Because you’re considering rows, not columns, of pixels, you ignore the y-direction.)

To create a perception of depth, make the spacing, or repeat interval, proportional to the depth map value for that pixel. So in the final autostereogram image, every pixel is shifted by delta_i compared to the previous (periodic) appearance of the same pixel. You can express this as bi = biw+δt

Here, bi represents the color value of a given pixel at index i for the final autostereogram image. This is exactly what you are doing at . Pixels with a depth map value of 0 (black) are not shifted and are perceived as the background.

At , you replace each pixel with its shifted value. At , check to make sure you’re not trying to access a pixel that’s not in the image, which can happen at the image’s edges because of the shift.

Command Line Options

Now let’s look at main() method of the program, where we provide some command line options.

       # create a parser
       parser = argparse.ArgumentParser(description="Autosterograms...")
       # add expected arguments
      parser.add_argument('--depth', dest='dmFile', required=True)
       parser.add_argument('--tile', dest='tileFile', required=False)
       parser.add_argument('--out', dest='outFile', required=False)
       # parse args
       args = parser.parse_args()
       # set the output file
       outFile = 'as.png'
       if args.outFile:
           outFile = args.outFile
       # set tile
       tileFile = False
       if args.tileFile:
           tileFile = Image.open(args.tileFile)

At , as with previous projects, you define the command line options for the program using argparse. The one required argument is the depth map file, and the two optional arguments are the tile filename and the name of the output file. If a tile image is not specified, the program will generate a tile of random circles. If the output filename is not specified, the output is written to an autostereogram file named as.png.

The Complete Code

Here is the complete autostereogram program. You can also download this code from https://github.com/electronut/pp/blob/master/autos/autos.py.

import sys, random, argparse
from PIL import Image, ImageDraw

# create spacing/depth example
def createSpacingDepthExample():
    tiles = [Image.open('test/a.png'), Image.open('test/b.png'),
             Image.open('test/c.png')]
    img = Image.new('RGB', (600, 400), (0, 0, 0))
    spacing = [10, 20, 40]

    for j, tile in enumerate(tiles):
        for i in range(8):
            img.paste(tile, (10 + i*(100 + j*10), 10 + j*100))
    img.save('sdepth.png')

# create an image filled with random circles
def createRandomTile(dims):
    # create image
    img = Image.new('RGB', dims)
    draw = ImageDraw.Draw(img)
    # set the radius of a random circle to 1% of
    # width or height, whichever is smaller
    r = int(min(*dims)/100)
    # number of circles
    n = 1000
    # draw random circles
    for i in range(n):
        # -r makes sure that the circles stay inside and aren't cut off
        # at the edges of the image so that they'll look better when tiled
        x, y = random.randint(0, dims[0]-r), random.randint(0, dims[1]-r)
        fill = (random.randint(0, 255), random.randint(0, 255),
                random.randint(0, 255))
        draw.ellipse((x-r, y-r, x+r, y+r), fill)
    # return image
    return img

# tile a graphics file to create an intermediate image of a set size
def createTiledImage(tile, dims):
    # create the new image
    img = Image.new('RGB', dims)
    W, H = dims
    w, h = tile.size
    # calculate the number of tiles needed
    cols = int(W/w) + 1
    rows = int(H/h) + 1
    # paste the tiles into the image
    for i in range(rows):
        for j in range(cols):
            img.paste(tile, (j*w, i*h))
    # output the image
    return img

# create a depth map for testing
def createDepthMap(dims):
    dmap = Image.new('L', dims)
    dmap.paste(10, (200, 25, 300, 125))
    dmap.paste(30, (200, 150, 300, 250))
    dmap.paste(20, (200, 275, 300, 375))
    return dmap

# given a depth map image and an input image,
# create a new image with pixels shifted according to depth
def createDepthShiftedImage(dmap, img):
    # size check
    assert dmap.size == img.size

    # create shifted image
    sImg = img.copy()
    # get pixel access
    pixD = dmap.load()
    pixS = sImg.load()
    # shift pixels output based on depth map
    cols, rows = sImg.size
    for j in range(rows):
        for i in range(cols):
            xshift = pixD[i, j]/10
            xpos = i - 140 + xshift
            if xpos > 0 and xpos < cols:
                pixS[i, j] = pixS[xpos, j]
    # return shifted image
    return sImg

# given a depth map (image) and an input image,
# create a new image with pixels shifted according to depth
def createAutostereogram(dmap, tile):
    # convert the depth map to a single channel if needed
    if dmap.mode is not 'L':
        dmap = dmap.convert('L')
    # if no image is specified for a tile, create a random circles tile
    if not tile:
        tile = createRandomTile((100, 100))
    # create an image by tiling
    img = createTiledImage(tile, dmap.size)
    # create a shifted image using depth map values
    sImg = img.copy()
    # get access to image pixels by loading the Image object first
    pixD = dmap.load()
    pixS = sImg.load()
    # shift pixels horizontally based on depth map
    cols, rows = sImg.size
    for j in range(rows):
        for i in range(cols):
            xshift = pixD[i, j]/10
            xpos = i - tile.size[0] + xshift
            if xpos > 0 and xpos < cols:
                pixS[i, j] = pixS[xpos, j]
    # return shifted image
    return sImg

# main() function
def main():
    # use sys.argv if needed
    print('creating autostereogram...')
    # create parser
    parser = argparse.ArgumentParser(description="Autosterograms...")
    # add expected arguments
    parser.add_argument('--depth', dest='dmFile', required=True)
    parser.add_argument('--tile', dest='tileFile', required=False)
    parser.add_argument('--out', dest='outFile', required=False)
    # parse args
    args = parser.parse_args()

    # set the output file
    outFile = 'as.png'
    if args.outFile:
        outFile = args.outFile
    # set tile
    tileFile = False
    if args.tileFile:
        tileFile = Image.open(args.tileFile)
    # open depth map
    dmImg = Image.open(args.dmFile)
    # create stereogram
    asImg = createAutostereogram(dmImg, tileFile)
    # write output
    asImg.save(outFile)

# call main
if __name__ == '__main__':
    main()

Running the Autostereogram Generator

Now let’s run the program using a depth map of a stool (stool-depth.png).

python3 autos.py --depth data/stool-depth.png

Figure 8-6 shows the depth map image on the left and the generated autostereogram on the right. Because you haven’t supplied a graphic for the tile, this autostereogram is created using random tiles.

Image

Figure 8-6: Sample run of autos.py

Now let’s give a tile image as input. Use the stool-depth.png depth map as you did earlier, but this time, supply the image escher-tile.jpg3 for the tiles.

python3 autos.py --depth data/stool-depth.png –tile data/escher-tile.jpg

Figure 8-7 shows the output.

Image

Figure 8-7: Sample run of autos.py using tiles

Summary

In this project, you learned how to create autostereograms. Given a depth map image, you can now create either a random dot autostereogram or one tiled with an image you supply.

Experiments!

Here are some ways to further explore autostereograms.

1. Write code to create an image similar to Figure 8-2 that demonstrates how changes in the linear spacing in an image can create illusions of depth. (Hint: use image tiles and the Image.paste() method.)

2. Add a command line option to the program to specify the scale to be applied to the depth map values. (Remember that the code divides the depth map value by 10.) How does changing the value affect the autostereogram?

3. Learn to create your own depth maps from three-dimensional models using a tool like SketchUp (http://sketchup.com/) or access the many ready-made SketchUp models online. Use SketchUp’s Fog option to create your depth maps. If you need help, check out this YouTube video: https://www.youtube.com/watch?v=fDzNJYi6Bok/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset