Perceived Depth Images

Dror Bar-Natan

Department of Mathematics

Princeton University

(Currently at the Institute of Mathematics, The Hebrew University, Jerusalem 91904)







PDIPlot[Sin[Abs[x+I y]],{x,-10,10},{y,-10,10}, PlotPoints -> 120, AspectRatio -> Automatic]

What is this thing? It's a Perceived Depth Image (PDI). It looks like a random mess, but it isn't. It's a three dimensional plot of the graph of sin|z|, z=x+iy, in the range -10<=x,y<=10. To see it, stare straight through the page, as if the journal you are holding weren't there, and let your eyes relax until the two guiding blocks at the bottom of the figure appear to separate and become four. Focus your eyes at some plane far behind the figure, and make two of the four blocks coincide so that you now see exactly three blocks. Now raise your eyes slowly up to the main part of the figure, without refocusing them. What you see now is a real, live, almost touchable, three dimensional picture of sin|z| - a round valley right at the center, with a circular hill surrounding it, a circular valley surrounding the hill, ..., all seen from above. Just the same view could be plotted by Plot3D[Sin[Abs[x+ I y]],{x,-10,10},{y,-10,10}] , only that a PDI really comes out of the page towards you.

Don't be concerned if you can't see it the first time; it takes some training. Most people need 5-10 minutes to get their first PDI, and after a few more PDI's they can focus at the right distance almost instantaneously. Some people find it easier to start looking at the PDI from about an inch away, and then slowly move the figure away until the image appears. Others find crossing their eyes easier than staring at infinity - you can also view the PDI by holding a pencil in front of the two guiding blocks, focusing your eyes on it, moving it until the two blocks appear to be three, and than looking up the PDI itself.

How does it work? Looking more closely at the random mess that makes a PDI, you see that it isn't completely random. Each PDI can be divided into a certain number of vertical strips (6 for the sin|z| picture, for example). The first such strip is randomly filled, the second looks just like the first only a bit distorted, the third is just like the second only distorted some more, and so on. Let us look at a smaller scale example:

SetOptions[PDIPlot,Periods -> 3, PlotPoints -> 12, Guides -> False] Show[SeedRandom[1]; PDIPlot[0,{x,-1,1},{y,-1,1}, BasicBlock :> (Line[ {#1+{1,1}#2,#1+{1,-1}#2,#1+{-1,-1}#2,#1+{-1,1}#2,#1+{1,1}#2}]&)], SeedRandom[1]; PDIPlot[2y,{x,-1,1},{y,-1,1}, BasicBlock :> (Rectangle[#1-.5#2,#1+.5#2]&)], AspectRatio -> Automatic]

In this example, we first printed a PDI plot of the function 0 (using the PDIPlot option BasicBlock to make the basic building block of the picture be an unfilled square), and then, on top of it, a plot of the function 2y (using smaller blocks). For clarity, we used the PDIPlot options Periods and PlotPoints to reduce the resolution of this plot. We also used the command SeedRandom[1] twice - to make sure that the two plots would fit neatly one on top of the other. The pattern is now clear - in the plot of 0, we simply get three identical vertical strips. In the plot of 2y we can also identify the three vertical strips, but we can see that they are distorted - the distances between the blocks in the upper rows, where 2y is positive, are made smaller than the distances in the lower rows, where 2y is negative.

So what? A quick look at figure 1 now explains what happens.



Figure 1: A top view of an observer looking at a PDI.



When we focus our eyes on the plane P, the left eye sees the left guiding block and the right eye sees the right, but our brain interprets it as if there were a single block, on the plane P, and that both our eyes are looking at it. Following now the dotted lines in the figure, it is clear that changing the distance between these two blocks can be interpreted as moving the ``fused'' image block in our brain nearer or farther! A PDI is nothing but a systematic application of this simple phenomenon. See also figure 2.

Figure 2: A slightly aperiodic pattern, with the eyes focused behind the picture, appears to lie on three different planes. The spaces between consecutive points relate as 8:9:9:8:7:7:8.



So how do we make a PDI? Well, it's not very hard. A very simple (but crude) code that does a decent job is the following:

z := (-35-30I+x+I y)/30 ; f = Abs[z]*Sin[5Arg[z]] Show[Graphics[Table[ xsample=Join @@ Position[Table[Random[]<0.5,{10}],True]; xv=Outer[Plus,Table[i,{i,0,50,10}],xsample-1]; xv=xv-Accumulate[Plus,Map[(f /. {y->yi,x->#})&,xv,{2}]]; Point[{#,yi}]& /@ Flatten[xv], {yi,60}]]]







The first line is the definition of the expression to be plotted - essentially it's |z|sin arg z, only with the variables scaled and shifted a bit. Then comes the main loop - the PDI is plotted row by row, and yi , that goes from 1 to 60, is the current value of y. Each row is made of six repetitions of the same pattern, each distorted by a little relative to the previous one. The first line of the loop defines xsample to be a random subset of the integers 1-10. These are the x coordinates of the pixels that will be turned on in this row of the left most strip. Then xv is defined to be a list of six copies of xsample , each shifted by a different amount to form the current row of the six strips. The next line is the main computation - the function f is evaluated at each of the points of xv , and each point x 0 in xv is then shifted, left or right, by the sum of the values of f on the images of x 0 in the strips to its left. This simply means that the distance between x 0 and the point corresponding to it on the next strip to the left is changed by f(x 0 ,y) - so when these two points are matched by the observer's eye, f(x 0 ,y) becomes the depths of the resulting image point. The last line of the loop just plots the results of the computation in the line before.

Mandelbrot[c_?NumberQ,n_:32]:=Block[{d,z}, For[z=0.; d=0, Abs[z]<2 && d<n, d++, z=z^2+c]; d] PDIPlot[Log[2,Mandelbrot[x+I y]],{x,-2.,1.},{y,-1.5,1.5}, PlotRange -> {-5,5}, PlotPoints -> 300, BasicBlock -> (Point[#1]&), ApparentDepth -> 1.5, AspectRatio -> Automatic, Prolog -> {PointSize[.004]}]





PDIPlot[Abs[x+I y]Arg[(x+I y)^8]/5,{x,-1,1},{y,-1,1}, PlotPoints -> 120, BasicBlock -> (Point[#1]&), AspectRatio -> Automatic, Prolog -> {PointSize[.01]}]





TorusKnot[m_Integer,n_Integer,z_?NumberQ]:= If[z==0,0, Block[{distance,knottime,width,i}, width=.25/n^2 // N; Max[0,Table[ knottime=(Arg[z]+2Pi i)m/n; distance=(1+.5Sin[knottime]-Abs[z])^2 // N; If[distance>width,0, N[.6+.5Cos[knottime]+1.5Sqrt[width-distance]]], {i,n}]] ]]; plot[m_,n_,pts_]:=PDIPlot[TorusKnot[m,n,x+I y],{x,-2,2},{y,-2,2}, BasicBlock -> (Point[#1]&), Prolog -> {PointSize[1.2/pts]}, AspectRatio -> Automatic,PlotPoints -> pts] plot[3,2,120]





pzol[m_,n_,pts_]:=PDIPlot[1-TorusKnot[m,n,x+I y],{x,-2,2},{y,-2,2}, BasicBlock -> (Point[#1]&), Prolog -> {PointSize[1.2/pts]}, AspectRatio -> Automatic,PlotPoints -> pts] pzol[3,2,120]





PDIPlot[Cos[x]Cos[y],{x,-15.7,15.7},{y,-15.7,15.7},ApparentDepth->.4, BasicBlock->(Point[#1]&),AspectRatio->Automatic,PlotPoints->300, Prolog->{PointSize[.004]}]







There are many other directions to check - the reader is welcome to try to plot other functions, color PDI's, animated PDI's, .... The reader is advised to first try PDI's of relatively low resolution (say PlotPoints -> 48 or so), before attempting to reproduce the PDI's in this article. These were produced on a fast workstation, and each took few hours of computing time.

PDI.m

PDI.m

Graphics

PDIArray[basicblock_,pts_]

pts

basicblock

pts

PDIPlot

PDIArray

PDIArray

For a primitive but working 9 line version of that package, click here.

BeginPackage["PDI`"] PDIPlot::usage ="

PDIPlot[exp,{x,xmin,xmax},{y,ymin,ymax}] returns a perceived-depth-image

of the graph of the function given by exp. Options:

PlotPoints (can be an integer or pair of integers; defaults to 60);

Periods (6); PlotRange ({-1,1}); Density (.5); ApparentDepth (1.);

Guides (True); BasicBlock (Rectangle[#1-#2,#1+#2]&).

Other options are passed on to Graphics." SetAttributes[PDIPlot,HoldFirst]; Options[PDIPlot] := {PlotPoints -> 60, Periods -> 6, Guides -> True, PlotRange -> {-1,1}, Density -> .5, ApparentDepth -> 1., BasicBlock :> (Rectangle[#1-#2,#1+#2]&)} Begin["`private`"] (* A new Graphics primitive *) Unprotect[Display] Display[channel_,graphics_?(!FreeQ[#,PDIArray]&)] := (Display[channel,graphics /. (PDIArray[basicblock_,size_,pts_] :> (basicblock[#,size]& /@ (Flatten[Outer[List,First[#],{Last[#]}],1])& /@ pts))]; graphics) Protect[Display] PDIPlot[expr_,{x_Symbol,xmin_?NumberQ,xmax_?NumberQ}, {y_Symbol,ymin_?NumberQ,ymax_?NumberQ},opts___] := Block[{plotpts =(PlotPoints /. {opts} /. Options[PDIPlot]), periods =(Periods /. {opts} /. Options[PDIPlot]), zrange =(PlotRange /. {opts} /. Options[PDIPlot]), density =(Density /. {opts} /. Options[PDIPlot]), depth =(ApparentDepth /. {opts} /. Options[PDIPlot]), basicblock =(BasicBlock /. {opts} /. Options[PDIPlot]), guides =(Guides /. {opts} /. Options[PDIPlot]), xres,xpts,xv,xsample,xsteps,xfull,xshift, ypts,i,yv,zmin,zmax,zmid,zscale}, (* Some scaling calculations *) {zmin,zmax} = zrange ; xres=Floor[xpts/periods]; zmid = (zmax+zmin)/2 ; zscale = .06depth(xmax-xmin)/(zmax-zmin); {xpts,ypts} = If[Length[plotpts]==2,plotpts,{plotpts,plotpts}]; (* The strips *) xsteps=xres*Table[i,{i,-.5,periods-1.5}]; (* The main program *) Show[Graphics[{PDIArray[basicblock,{(xmax-xmin)/xpts,(ymax-ymin)/ypts}/2, (* Loop over the values of yv *) Table[ (* Filling the current row of the left-most strip randomly *) xsample=Flatten[Position[Table[Random[]<density,{xres}],True]]; (* The x values to be used in this row *) xv=N[xmin+Outer[Plus,xsteps,xsample-1]*(xmax-xmin)/xpts]; (* Computing the left/right shift of every block *) z=N[Map[(expr /. {y->yv,x->#})&,xv,{2}]]-zmid; xshift=zscale Accumulate[Plus,z]; (* Final positioning of the blocks *) xfull=xv-((#-Last[xshift]/2)& /@ xshift); {Flatten[xfull],yv}, {yv,ymin,ymax,(ymax-ymin)/ypts}]], (* If guides==True, add guiding rectangles *) If[guides, (Rectangle @@ #)& /@ Map[ ({.5xmax+.5xmin,1.1ymin-.1ymax}+{xmax-xmin,ymax-ymin}#/2/periods)&, {{{-2.1,-.1},{-1.9,.1}},{{-.1,-.1},{.1,.1}}},{2}], {}]}, Sequence@@Select[{opts},!MemberQ[First /@ Options[PDIPlot],First[#]]&] ]] ] End[] EndPackage[]

Exercise Take a second look at the PDI plot of sin|z|. You can see that the second circular hill, the last that can be seen in the picture, is a bit `squarish'. Why is it so? How can it be avoided?

Notes. Our PDI's are a slightly modified version of B. Julesz's `Random-dot stereograms' [2], invented by D. S. Falk, D. R. Brill and D. G. Stork [1, 3]. The TLA PDI was coined by a company, 3D-Hardcopy of Salt Lake City, Utah, which is producing full size PDI posters of exactly the same type as ours. To order, call Jeremy Westover at (503) 345-9359. I wish to thank S. Levy for his advice and editorial help.

References

1 D. S. Falk, D. R. Brill and D. G. Stork, Seeing the light : optics in nature, photography, color, vision, and holography, Harper & Row, New York, 1986. 2 B. Julesz, Foundations of Cyclopean Perception, The University of Chicago Press, Chicago 1971. 3 D. G. Stork and C. Rocca, Software for generating auto-random-dot stereograms, Behavior Research Methods, Instruments, & Computers 21 (1989) 525-534.

This document was generated from a TeX source on November 21, 1998. The generation of this document was assisted by the LaTeX2 HTML translator Version 96.1-h (September 30, 1996) Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.

Postscript

On March 24, 1999, I got the image below from John Sullivan, who found it at Andrei Gnepp's, who got it from an unknown source. It serves as a wonderful postscript to this paper: