图8-1 一张令人费解的图像,可能让你感到痛楚[2]
8.1 事情事理三维立体画的事情事理是改变图像中图案之间的线性间距,从而产生深度的错觉。在不雅观看三维立体画中的重复图案时,大脑会将间距阐明为深度信息,如果有多个图案和不同的间距,尤其会这样。
8.1.1 感知三维立体画中的深度如果你的眼睛汇聚在图像背后一个假想的点,大脑将左眼看到的一些点与右眼看到的另一些点匹配起来,你将会看到这些点位于图像之后的一个平面上。到该平面的感知间隔取决于图案中的间距的数量。例如,图8-2展示了3行A。这些A每行间的间隔相等,但它们的水平间距从上至下增加。
如果用“墙眼”的办法来看,图8-2中最上面一行该当涌如今纸后面,中间行该当看起来像在第一行后面一点,底部一行该当涌如今最远的位置。文本“floating text”该当看起来“浮在”这几行顶部。
为什么大脑将这些图案的间距解读为深度?常日情形下,如果看远处的物体,你的双眼协作,聚焦并汇聚在同一点,双眼向内转,直接指向目标点。但用“墙眼”办法不雅观看三维立体画时,聚焦和汇聚发生在不同的位置。眼睛专注于三维立体画,但大脑将重复的模式算作来自同一个虚拟(虚构的)工具,眼睛汇聚在图像背后的一个点,如图8-3所示。解耦的聚焦和汇聚叠加在一起,让你在三维立体画中看到深度。
图8-2 线性间距和深度知觉
图8-3 在三维立体画中看到深度
三维立体画的感知深度取决于像素的水平间距。由于图8-2中的第一行具有最近的间隔,它涌如今其他行的前面。然而,如果点的间距在图像中是变革的,大脑将认为每个点处于不同的深度,以是我们会看到一个虚拟的三维图像。
8.1.2 深度图“深度图”是这样一幅图像:个中每个像素的值表示深度值,即从眼睛到该像素表示的工具部分的间隔。深度图每每表现为一幅灰度图,亮的区域表示近的点,暗的区域表示远的点,如图8-4所示。
图8-4 深度图
把稳,鲨鱼的鼻子是图像中最亮部分,彷佛最靠近你。朝向尾部的较暗区域看起来最远。
由于深度图表示从每个像素中央到眼睛的深度或间隔,以是可以用它来得到与图像中像素位置干系联的深度值。我们知道,在图像中,水平偏移被认为是深度。以是,如果按照对应像素值深度值的比例,来偏移(图案)图像中的像素,就会对该像素产生与深度图同等的深度知觉。如果对所有像素这样做,终极就会将全体深度图编码到图像中,天生三维立体画。
深度图的每个像素存储了深度值,并且该值的分辨率取决于表示它的位数。由于本章采取常见的8位图像,深度值的范围是[0,255]。
顺便说一下,图8-4中的图像便是用于创建图8-1中的三维立体画的深度图。你很快就能学会自己如何做到这一点。
该项目的代码将遵照以下步骤:
1.读入深度图;
2.读入一幅平铺图像或创建一个“随机点”平铺图像;
3.通过重复平铺图像创建一幅新图像。该图像的尺寸与深度图同等;
4.对新图像中的每个像素,根据该像素干系联的深度值,将它按比例地向右移;
5.将三维立体画写入一个文件。
8.2 所需模块本项目利用Pillow读取图片,访问它们的底层数据,创建和修正图像。
8.3 代码为了从输入的深度图天生三维立体画,首先重复一幅给定的平铺图像,天生一幅中间图像。接下来,天生一幅充满随机点的平铺图像。然后进入天生三维立体画的核心代码,即利用所供应的深度图中的信息,移动输入的图像。要查看完全的项目,请直接跳到8.4节。
8.3.1 重复给定的平铺图像我们从利用createTiledImage()方法开始,通过平铺一个图形文件,创建一幅新的图像。图像尺寸由dims元组指定,该元组形式为(width, height)。
# tile a graphics file to create an intermediate image of a set sizedef 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, (jw, ih)) # output the image return img
在❶行,利用供应的尺寸(dims)创建新的Python图像库(PIL)Image工具。新图像的尺寸由元组dims给出,形式是(width, height)。接着,保存平铺图像和输出文件的宽度和高度。在❷行,确定列数,在❸行,确定中间图像所需的行数,方法是用终极图像的尺寸除以平铺图像的尺寸。除的结果每次加1,如果输出图像的尺寸不是恰好是平铺图像的整数倍,这也能确保右边末了的平铺图像不会缺失落。如果没有这种预防方法,图像的右边可能被割断。然后,在❹行,循环遍历行和列,并用平铺图像添补它们。通过乘积(jw, ih),确定平铺图像左上角的位置,这样它能对准行和列。完成后,该方法返回指定尺寸的Image工具,用输入图像tile平铺。
8.3.2 从随机圆创建平铺图像如果用户不供应平铺图像,就利用createRandomTile()方法,用随机圆圈创建一张平铺图像。
# create an image tile filled with random circlesdef 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
在❶行,用dim给出的尺寸创建新的Image工具。用ImageDraw.Draw() ❷在该图像中画圆圈,用宽或高中较小值的1/100作为半径,画圆圈❸(Python的运算符将dim元组中的宽度和高度值解包,这样就能传入到min()方法中)。
在❹行,设置要画的圆圈数为1000。然后调用random.randint(),得到范围为[0, width-r]和[0, height-r]的两个随机整数,从而算出每个圆圈的x和y坐标❺。“-r”确保天生的圆圈保持在width×height的图像矩形内部。不带-r,画的圆圈可能就在图像边缘,这意味着它会被切掉一部分。如果平铺这样的图像来创建三维立体画,结果不会好看,由于两个平铺图像之间没有空间。
要天生一个随机圆圈,先画出轮廓,然后添补颜色。在❻行,在[0,255]的范围内随机选取RGB值,用选择颜色添补。末了,在❼行,用draw中的ellipse()方法绘制每个圆圈。该方法的第一个参数是圆的边界矩形,它由左上角和右下角指定,分别为(x-r, y-r)和(x+r, y+r),个中(x, y)是该圆的圆心,r是半径。
让我们在Python阐明器中测试这种方法。
>>> import autos>>> img = autos.createRandomTile((256, 256))>>> img.save('out.png')>>> exit()
图8-5展示了测试的输出。
图8-5 考试测验运行createRandomTile()
正如你在图8-5中看到的,我们已经创建了随机点的平铺图像。可以利用它来创建的三维立体画。
8.3.3 创建三维立体画现在,让我们创建一些三维立体画。createAutostereogram()方法完成了大部分事情,如下所示:
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
在❶行,进行完全性检讨,确保深度图和图像具有相同的尺寸。在❷行,如果用户没有供应平铺图像,就创建随机圆圈平铺图像。在❸行,创建一张平铺好的图像,符合供应的深度图的大小。然后,在❹行天生这张平铺好的图像的副本。
在❺行,调用Image.load()方法,将图像数据加载到内存中。该方法许可用形如[i, j]的二维数组来访问图像像素。在❻行,将图像的尺寸保存为行数和列数,将图像算作单个像素构成的网格。
三维立体画创建算法的核心在于,根据从深度图中网络的信息,移动平铺图像中像素的办法。要做到这一点,遍历平铺图像,处理每一个像素。在❼行,根据深度图pixD中的干系像素,查找偏移的值。然后将这个深度值除以10,由于这里用的是8位深度图,这意味着深度的范围是0到255。如果除以10,得到的深度值范围是0到25。由于深度图输入图像的尺寸常日是几百像素,以是这些偏移值很得当(考试测验改变除数,看看它如何影响终极图像)。
在❽行,打算像素的新x位置,用平铺图像添补三维立体画。每隔w个像素,像素的值不断重复,由公式ai = ai + w表示,个中的ai是在x轴下标i处的给定像素的颜色(由于考虑的是像素行,而不是列,以是忽略y方向)。
要创建深度感,就要让间隔(或重复的间距)与该像素的深度图值成正比。这样在终极的三维立体画图像中,每个像素和它前一次(周期地)涌现比较,偏移了delta_i。这可以表示为bi=bi-w+δt
这里,bi表示末了的三维立体画图像中,下标i处给定像素的颜色值。这正是❽行所做的事。深度图值为0(玄色)的像素没有偏移,被视为背景。
在❿行,用偏移的值更换每个像素。在❾行,检讨确保没有试图访问不在图像中的像素,由于偏移,在图像边缘可能发生这种情形。
8.3.4 命令行选项现在,我们来看看该程序的main()方法,个中供应了一些命令行选项。
# 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)
在❶行,像以前的项目一样,利用argparse为程序定义了一些命令行选项。一个必需的参数是深度图文件,两个可选的参数是平铺图像文件名和输出文件名。如果未指定平铺图像,程序会天生随机圆圈平铺图像。如果未指定输出文件名,则三维立体画会输出到as.png文件。
8.4 完全代码下面是完全的三维立体画程序。也可以从https://github.com/electronut/pp/blob/ master/autos/autos.py下载这段代码。
import sys, random, argparsefrom PIL import Image, ImageDraw# create spacing/depth exampledef 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 + j10), 10 + j100)) img.save('sdepth.png')# create an image filled with random circlesdef 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 sizedef 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, (jw, ih)) # output the image return img# create a depth map for testingdef 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 depthdef createDepthShiftedImage(dmap, img): # size check assert dmap.size == img.size# create shifted imagesImg = img.copy()# get pixel accesspixD = dmap.load()pixS = sImg.load()# shift pixels output based on depth mapcols, rows = sImg.sizefor 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 depthdef 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() functiondef 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 mainif __name__ == '__main__': main()
8.5 运行三维立体画天生程序
现在,我们用凳子(stool-depth.png)的深度图运行该程序。
$ python3 autos.py --depth data/stool-depth.png
图8-6左边展示了深度图,右边展示了天生的三维立体画。由于没有为平铺供应图像,这张三维立体画利用了随机天生的平铺图像。
图8-6 autos.py运行示例
现在,让我们给定一个平铺图像作为输入。像前面一样利用stool-depth.png深度图,但这一次,供应图像escher-tile.jpg[3]作为平铺图像。
$ python3 autos.py --depth data/stool-depth.png –tile data/escher-tile.jpg
图8-7展示了输出。
图8-7 利用平铺图像的autos.py运行示例
8.6 小结在本项目中,我们学习了如何创建三维立体画。给定深度图的图像,我们现在可以创建随机点的三维立体画,或用供应的图像来平铺。
如果你想知道如何利用编程来理解和探索想法。那么你可以看看这本《Python极客项目编程》,这本书的项目假设你理解基本的Python语法和基本的编程观点,并假设你熟习高中数学知识。我已经尽了最大的努力,详细阐明了所有项目中须要的数学知识。
《Python极客项目编程》
《Python极客项目编程(异步图书出品)》([美],Mahesh,Venkitachalam)【择要 书评 试读】- 京东图书item.jd.com
本书包含了一组富有想象力的编程项目,它们将勾引你用Python 来制作图像和音乐、仿照现实天下的征象,并与
Arduino 和树莓派这样的硬件进行交互。你将学习利用常见的Python 工具和库,如numpy、matplotlib 和pygame,
来完成以下事情:
● 利用参数方程和turtle模块天生万花尺图案;
● 通过仿照频率泛音在打算机上创作音乐;
● 将图形图像转换为ASCII文本图形;
● 编写一个三维立体画程序,天生隐蔽在随机图案下的3D图像;
● 通过探索粒子系统、透明度和广告牌技能,利用OpenGL着色器制作逼真的动画;
● 利用来自CT和MRI扫描的数据实现3D可视化;
● 将打算机连接到Arduino编程,创建相应音乐的激光秀。
通过本书,你可以享受作为极客的真正乐趣!
作者通过一系列不大略的项目,向你展示如何用Python来办理各种实际问题。在学习这些项目时,你将探索Python编程措辞的细微差别,并学习如何利用一些盛行的Python库。但大概更主要的是,你将学习如何将问题分解成几个部分,开拓一个算法来办理这个问题,然后从头用Python来实现一个办理方案。办理现实天下的问题可能很难,由于它们每每是开放式的,并且须要各个领域的专业知识。但Python供应了一些工具,帮忙办理问题。战胜困难,探求实际问题的办理方案,这是成为专家级程序员的旅途中最主要的环节。
让我们来看看有哪些练手项目?第一部分:热身运动第1章展示了如何解析iTunes播放列表文件,并从中网络有用的信息,如音轨长度和共同的音轨。在第2章中,我们利用参数方程及海龟作图法,绘制类似万花尺产生的那些曲线。
第1章 解析iTunes播放列表第2章 万花尺第二部分:仿照生命这部分是用数学模型来仿照征象。在第3章中,我们将学习如何实现Conway游戏的生命游戏算法,产生动态的模式来创建其他模式,以仿照一种人工生命。第4章展示了如何用Karplus-Strong算法来创建逼真的弹拨音。然后,在第5章中,我们将学习如何实现类鸟群算法,仿照鸟类的聚拢行为。
第3章 Conway生命游戏第4章 用Karplus-Strong算法产生音乐泛音第5章 类鸟群:仿真鸟群第三部分:图像之乐这部分先容利用Python读取和操作2D图像。第6章展示了如何根据图像创建ASCII码艺术图。在第7章中,我们将进行照片拼接。在第8章中,我们将学习如何天生三维立体图,它让人产生3D图像的错觉。
第6章 ASCII文本图形第7章 照片马赛克第8章 三维立体画第四部分:走进三维这一部分的项目利用OpenGL的3D图形库。第9章先容利用OpenGL创建大略3D图形的基本知识。在第10章中,我们将创建粒子仿照的烟花喷泉,它用数学和OpenGL着色器来打算和渲染。在第11章中,我们将利用OpenGL着色器来实现立体光芒投射算法,来渲染立体数据,该技能常用于医疗影像,如MRI和CT扫描。
第9章 理解OpenGL第10章 粒子系统第11章 体渲染第五部分:玩转硬件在末了一部分中,我们将用Python来探索Arduino微掌握器和树莓派。在第12章中,我们将利用Arduino,通过一个大略电路读取并标绘传感器数据。在第13章中,我们将利用Python和Arduino来掌握两个旋转镜和激光器,天生响应声音的激光秀。在第14章中,我们将利用树莓派打造一个基于网络的气候监测系统。
第12章 Arduino简介第13章 激光音乐秀第14章 基于树莓派的景象监控器