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 u if dmap.mode is not 'L':
dmap = dmap.convert('L')
# if no image is specified for a tile, create a random circles tile v if not tile:
tile = createRandomTile((100, 100)) # create an image by tiling
w img = createTiledImage(tile, dmap.size)
# create a shifted image using depth map values x sImg = img.copy()
# get access to image pixels by loading the Image object first y pixD = dmap.load()
pixS = sImg.load()
# shift pixels horizontally based on depth map z cols, rows = sImg.size
At u, 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 v. At w, you create a tile that matches the size of the supplied depth map image. You then make a copy of this tiled image at x.
At y, 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 z, 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 pro-cess 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 approxi-mate 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 auto-stereogram 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 col-umns, 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 auto-stereogram image, every pixel is shifted by delta_i compared to the previous (periodic) appearance of the same pixel. You can express this as bi =bi w− +δ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
u 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 u, 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))
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):
# tile a graphics file to create an intermediate image of a set size def createTiledImage(tile, dims):
# 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
# given a depth map (image) and an input image,
# create a new image with pixels shifted according to depth def createAutostereogram(dmap, tile):
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
# 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()