Python-图形学教程-全-

Python 图形学教程(全)

原文:Python Graphics

协议:CC BY-NC-SA 4.0

一、基本 Python 命令和函数

在本章中,您将学习制作本书所示插图所需的基本 Python 命令和函数。您将学习如何使用 Python 的基本绘图功能,设置绘图区域,创建一组二维坐标轴,以及使用基本绘图原语(点、线和箭头),这些是您将在本书中用来构建图像的构建块。在第二章中,你将学习如何使用这些图元来构建二维图像,然后对其进行*移和旋转。在第三章中,你将把这些概念扩展到三维空间。在本章中,你还将学习颜色,如何将文本应用到你的绘图中,包括 Latex 命令的使用,以及列表和数组的使用。到最后一章,你将能够创建如图 1-1 的图像。

A456962_1_En_1_Fig1_HTML.jpg

图 1-1

Saturn

1.1 编程风格

首先,请注意本书中使用的编程风格。谈到风格,我们都有自己的偏好。我喜欢清晰、自上而下、开放的风格。许多程序员试图将他们的代码减少到尽可能少的行。这在实践中可能没问题,但在教学文本中,比如我们这里的,我认为最好是以小而简单的步骤慢慢进行。目的是让一切都清晰易懂。因为我不知道读者的技能水*,也因为我想让尽可能多的读者能够接触到这本书,所以我通常从初级水*开始每个主题,尽管我假设对 Python 语言有些熟悉。如果你刚刚开始学习 Python,你会从第一章的材料中受益。如果你是一个精通 Pythoner 的人,你可能会跳过它,继续阅读第二章。

一些 Python 开发人员提倡对变量使用长的描述性名称,比如“temperature”而不是“t”。我发现过长的变量名会使代码难以阅读。这是个人喜好的问题。对于像本书中这样相对较短的程序,没有必要进行复杂的编程。尝试采用稳健而非优雅但脆弱的风格。

我的程序通常有相同的结构。前几条语句一般是 import numpy 作为 np,import matplotlib.pyplot 作为 plt,等等。有时我会从数学库导入正弦,余弦,弧度,*方。这些是图形编程中常用的函数。以这种方式分别导入它们消除了在 np.sin()中使用前缀的需要;用 sin()就可以了。然后我最常用 plt.axis([0,150,100,0])定义绘图区域。如第 1.2 节所述,这些值(其中 x 轴比 y 轴宽 50%)会产生一个圆形和一个方形,而不会减小绘图区域的大小。此时,如果需要,可以给轴加标签,给图加标题。接下来,我通常定义参数(比如直径、时间常数等等)和列表。然后我定义函数。最后,在冗长的程序中,我在底部放了一个控制部分,以正确的顺序调用函数。

包括 plt.axis('on ')绘制轴;plt.grid(True)绘制网格。在开发图形时,它们是非常方便的选择。但是,如果我不想在最终输出中显示轴或网格,我会用 plt.axis('off ')和 plt.grid(False)替换这些命令。语法必须如下所示。如果对 Python 的默认设置不满意,请参见 1.10 节了解如何创建自己的网格线。

我经常通过使用 scatter()函数开始开发图形,这个函数产生我称之为散点的东西。它们快速易用,在开发阶段非常有用。如果保持足够小并且间隔紧密,点可以产生可接受的直线和曲线。然而,它们有时会显得有点模糊,因此,当我一切正常后,我会经常使用箭头通过 plt.arrow()或线条通过 plt.plot()返回并用短线段替换点。选择点或线还有另一个方面:哪一个画得多。你不会想用点来创造一些东西,然后用线来覆盖它。这将在 1.14 节中讨论。

Python 的一些变体需要在结尾处使用 plt.show()语句来绘制图形。我的设置 Anaconda with Spyder and Python 3.5(安装说明见附录 A)不需要这个,但是我还是把它包括进来了,因为它是程序结束的标志。最后,按 F5 键或单击顶部的 Run 按钮,看看您创建了什么。满意后,可以通过右键单击并指定位置来保存该图。

关于列表、元组和数组的使用,它们会有很大的帮助,尤其是在进行涉及大量数据点的图形编程时。第 1.19.5 节对此进行了解释。对它们的理解,以及本章中介绍的一些基本的图形命令和技术,是你创作本书中的插图和图像所需要的。

1.2 标绘区域

图 1-2 所示为二维坐标系的计算机显示。在本例中,x,y 坐标轴的原点(x=0,y=0)位于屏幕的中心。正 x 轴从原点向右运行;y 轴从原点垂直向下延伸。很快您就会看到,原点的位置可以改变,x 轴和 y 轴的方向也可以改变。还示出了坐标(x,y)处的点 p,其与 x 和 y 轴相关。

图 1-2 中 y 轴指向下方的方向可能有点不寻常。在绘制 y=cos(x)或 y=exp(x)等数据或函数时,我们通常认为 y 是指向上的。但是在做技术图形的时候,尤其是三维的时候,你后面会看到,y 轴指向下更直观。这也与旧版本的 BASIC 一致,在旧版本中,x 轴沿屏幕顶部从左到右延伸,y 轴沿左侧延伸。正如您将看到的,您可以将 y 定义为向上或向下,无论哪一个最适合您正在绘制的内容。

1.3 确定绘图区域的大小

绘图区域包含图形图像。在 Spyder 输出窗格中显示时,它总是显示相同的物理大小。Spyder 是编程环境(参见附录 A)。但是,绘图区域的数值大小以及绘图区域内点、线和箭头定义的值可以指定为任意值。在进行任何绘图之前,您必须首先确定该区域的数值大小。还必须指定坐标系原点的位置和坐标轴的方向。举例来说,清单 1-1 在第 8 行使用 plt.axis([x1,x2,y1,y2])函数设置一个从 x=-10 到+10 的区域;y = 10 至+10。稍后将解释脚本的其余部分。

A456962_1_En_1_Fig2_HTML.jpg

图 1-2

A two-dimensional x,y coordinate system with its origin (0,0) centered in the screen. A point p is shown at coordinates (x,y) relative to x,y.

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  x1=-10
5  x2=10
6  y1=-10
7  y2=10
8  plt.axis([x1,x2,y1,y2])
9
10 plt.axis('on')
11 plt.grid(True)
12
13 plt.show()
Listing 1-1Program PLOTTING_AREA

列表 1-1 产生如图 1-3 所示的绘图区域。它的水*宽度为 20,垂直高度为 20。我可以将这些数字设为 200 和 200,该区域将在输出窗格中显示为相同的物理大小,但在轴上有不同的数值。第 13 行包含命令 plt.show()。这样做的目的是在输出窗格中显示程序的结果。对于现代版本的 Python,这是不需要的,因为程序运行时会自动显示绘图。对于旧版本,可能会显示,也可能不会显示。plt.show()也可以放在程序中,以便显示执行过程中创建的图。尽管可能没有必要,但在脚本末尾包含这个命令是个好主意,因为它可以作为程序结束的方便标记。清单 1-1 中的第 1、2、10 和 11 行将在以下章节中解释。这些命令,或者它们的变体,将会出现在我们所有的程序中。

A456962_1_En_1_Fig3_HTML.jpg

图 1-3

Plotting area produced by Listing 1-1 with (0,0) located in the center of the area

1.4 导入打印命令

虽然 Python 有许多可用的内置命令和函数,但一些数学和图形命令必须导入。清单 1-1 中的第 1 行和第 2 行就是这样做的。第 1 行的 import numpy as np 语句导入数学函数,如 sin(ϕ)、e α 等等。此语句中的 np 是一个缩写,在提到 numpy 函数时可以使用。当在程序中使用时,这些函数必须被识别为来自 numpy。例如,如果你要编写 v=e α ,程序语句将是 v=np.exp(α),其中α已经预先定义好了。您不必写出完整长度的 numpy.exp(α),因为您已经在第 1 行定义了 numpy 的简写 np。图形命令的处理方式类似。当 plt 导入包含图形命令的 pyplot 库时,语句 import matplotlib.pyplot。plt 是 pyplot 的缩写。例如,如果你想在 x,y 上画一个点,你可以写 plt.scatter(x,y)。我很快会谈到 plt.scatter()。

函数也可以直接从 numpy 导入。numpy import sin,cos,radians 中的语句导入 sin()、cos()和 radians()函数。当以这种方式导入时,它们可以不带 np 前缀使用。还有一个数学库也以类似的方式运行。例如,从 math 导入 sin,cos,radians 等效于从 numpy 导入。您将在接下来的程序中使用所有这些变体。

还有一个名为 glib 的图形库,包含图形命令。glib 使用与 pyplot 不同的语法。由于 pyplot 的使用更加广泛,您将在这里的工作中使用它。

清单 1-1 ,plt.axis([x1,x2,y1,y2])中的第 8 行是设置绘图区域的标准命令形式。这来自 pyplot 库,前面是 plt。前缀。这个命令有一些属性,也有其他定义绘图区域的方法,特别是 linspace()命令,但是第 8 行中的形式对于大多数目的来说已经足够了,也是您将使用的形式。x1 和 x2 分别定义绘图区域左侧和右侧的值;y1 和 y2 分别定义底部和顶部。通过第 8-11 行的数值,你可以得到如图 1-3 所示的绘图区域。x1、x2、y1 和 y2 的位置始终如图 1-3 所示。也就是说,x1 和 y1 总是指左下角,y2 指 y 轴的另一端,x2 指 x 轴的另一端。它们的值可以改变,但它们总是指这些位置。它们可能是负的,如图 1-4 所示。

A456962_1_En_1_Fig4_HTML.jpg

图 1-4

Plotting area with (0,0) located in the center, positive y direction pointing down

因为第 4-7 行中指定的 x 和 y 值在 x 和 y 方向上都是对称的(即 10,+10),所以该绘图区域的中点位于(x=0,y=0)之间。在这种情况下,该区域的中心将作为坐标绘制的参考原点。从 x1 < x2, the positive direction of the x axis will run horizontally from left to right. Similarly, since y1 < y2, the positive direction of the y axis will go vertically up. But earlier I said we want the positive y direction to go vertically down. You can do that by reversing the y values to y1=10, y2=−10. In this case, you get the area shown in Figure 1-4 开始,其中正 x 轴仍然从左向右,但正 y 轴现在指向下。中心仍然在绘图区域的中间。

通过操纵 x1、x2、y1 和 y2,可以将坐标系的原点移离中心。例如,要将 x=0 点一直移动到左侧,可以指定 x1=0,x2=20。要将(x=0,y=0)点移动到左下角,可以指定 x1=0,x2=20,y1=0,y2=20。但这会使 y 轴的正方向朝上。你希望它指向下,这可以通过使 y2=0,y1=20 来实现。这将使原点出现在左上角。您可以自由地将(0,0)点定位在任何位置,更改正 x 和 y 的方向,并缩放坐标轴的数值以适合您将尝试创建的图像。这里使用的数值可以是任何值。Python 制作的剧情物理尺寸会是一样的;只有图像坐标的值会改变。

1.5 显示绘图区域

在清单 1-1 的第 10 行,语句 plt.axis('on ')显示绘图区域及其框架和数值。如果省略此命令,框架仍将显示数值。那么为什么要包含这个命令呢?因为,当创建一个情节时,有时需要关闭框架。为此,请将 plt.axis('on ')替换为 plt.axis('off ')。提前使用该命令可以很容易地在“on”上键入“off ”,反之亦然,从而在显示和不显示帧之间进行切换。此外,完成绘图后,您可能希望在文档中使用它,在这种情况下,您可能不需要框架。请注意,“on”和“off”必须用单引号或双引号引起来。

1.6 标绘网格

清单 1-1 的第 11 行 plt.grid(True)打开了网格虚线,这在构建一个图时是有帮助的,尤其是在定位文本信息的时候。如果不包含此命令,将不会显示网格线。要关闭网格线,请将 True 更改为 False。注意 True 和 False 中的第一个字母大写。真与假不会出现在引号中。与 plt.axis()一样,使用 plt.grid(True)和 plt.grid(False)命令可以很容易地来回切换。再次注意,True 和 False 的第一个字母都必须大写,并且不能出现在引号中。

1.7 保存地块

保存出现在输出窗格中的图的最简单方法是将光标放在它上面并右键单击。将出现一个窗口,允许您为其命名并指定保存位置。它将被保存。png 格式。如果您计划在诸如 Photoshop 之类的程序中使用它。png 格式的作品。一些文字处理和文档程序可能需要。eps(封装的 Postscript)格式。如果是这样,请将其保存在。png 格式,在 Photoshop 中打开它,然后在。eps 格式。有点繁琐但是很管用。

1.8 网格颜色

plt.grid()命令有一些选项。您可以使用 color='color '属性更改网格线的颜色。例如,plt.grid(True,color="b ")绘制蓝色网格。更多颜色选项将很快定义。

1.9 刻度线

plt.grid(True)命令将使用 Python 自己选择的间距创建格网,这可能不太方便。可以使用 plt.xticks(xmin,xmax,dx)和 plt.yticks(ymin,ymax,dy)命令改变间距。最小值和最大值是刻度的范围;dx 和 dy 是间距。虽然通常您希望刻度线出现在 x 和 y 的整个范围内,但如果您愿意,也可以让它们出现在较小的范围内。这些命令出现在清单 1-2 的第 23 行和第 24 行。

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  #————————————————plotting area
5  x1=-10
6  x2=140
7  y1=90
8  y2=-10
9  plt.axis([x1,x2,y1,y2])
10 plt.axis('on')
11
12 #——————————————————grid
13 plt.grid(True,color='b')
14 plt.title('Tick Mark Sample')
15
16 #————————————————tick marks
17 xmin=x1
18 xmax=x2
19 dx=10
20 ymin=y1
21 ymax=y2
22 dy=-5
23 plt.xticks(np.arange(xmin, xmax, dx))
24 plt.yticks(np.arange(ymin, ymax, dy))
25
26 plt.show()
Listing 1-2Program TICK_MARKS

输出如图 1-5 所示。在第 23 行中,xmin 和 xmax 是沿 x 轴的刻度范围的开始和结束,类似于控制 y 轴刻度的第 24 行。第 19 行中的 dx 将标记从 x1=-10(第 5 行)到 x2=140(第 6 行)间隔 10 个单位。第 22 行中 dy 是-5。它是负的,因为 y2 = 10(第 8 行),而 y1=+90(第 7 行)。因此,随着程序从 y1 进行到 y2,y 的值减小;因此 dy 必须是负的。

A456962_1_En_1_Fig5_HTML.jpg

图 1-5

User-defined tick mark

1.10 自定义网格线

由 plt.grid(True)命令生成的自动生成的网格并不总是令人满意,尤其是当您想要在绘图中包含文本时。准确放置文本元素通常不够精细。但是如果使用 xtick()和 ytick()命令来减小间距,沿轴的数字会变得混乱。数字可以被删除,但这样你就不能使用它们来定位文本信息,例如在标记图上的项目时。如果增量更小,图 1-3 所示的网格会更有帮助。您可以生成自己的网格线,并以任何方式控制它们。清单 1-3 中的代码产生图 1-6 ,一个网格线间距更细的绘图区域。

A456962_1_En_1_Fig6_HTML.jpg

图 1-6

Plotting area with custom grid

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  x1=-5
5  x2=15
6  y1=-15
7  y2=5
8  plt.axis([x1,x2,y1,y2])
9
10 plt.axis('on')
11
12 dx=.5                                     #x spacing
13 dy=.5                                     #y spacing
14 for x in np.arange(x1,x2,dx):             #x locations
15      for y in np.arange(y1,y2,dy):        #y locations
16      plt.scatter(x,y,s=1,color='grey')    #plot a grey point at x,y
17
18 plt.show()
Listing 1-3Program CUSTOM_GRID

清单 1-3 的第 16 行中的 scatter()函数在每个 x,y 位置绘制一个灰点。我将在后面更深入地讨论 scatter()。注意,这个程序中没有使用 plt.grid(True)。第 1-10 行像以前一样产生带有轴的绘图区域。这一次,您没有使用 plt.grid(True)命令,而是在第 12-16 行生成了您自己的定制网格。第 12 行和第 13 行指定了间距。从第 14 行开始的循环在步骤 dx 中从左向右水*前进。从第 15 行开始的循环在垂直方向上也是如此。第 16 行的 s=1 属性将点的大小指定为 1。这可以改变:s=.5 将给出一个更小的点;s=5 将给出更大的值。color='grey '属性将点颜色设置为灰色。您可以尝试不同大小的点、颜色和间距。有时,将 Grid(True)生成的网格与自定义网格一起使用会很有好处。

1.11 轴的标签

轴可以用 plt.xlabel('label ')和 plt.ylabel('label ')函数来标记。作为一个例子,这些线

plt.xlabel('this is the x axis')

plt.ylabel('this is the y axis')

当添加到清单 1-3 的第 10 行后,产生图 1-7 ,其中通过使用 plt.scatter()函数中的属性 color='lightgrey '将自定义网格点更改为浅灰色。

A456962_1_En_1_Fig7_HTML.jpg

图 1-7

Plotting area with axis labels and custom grid

在图 1-8 中,你可以看到 matplotlib 网格。Python 网格和自定义网格的组合为定位元素提供了一个方便的工作表面。

A456962_1_En_1_Fig8_HTML.jpg

图 1-8

Plotting area with axis labels, the Python grid, and a custom grid

1.12 情节标题

用 plt.title('title ')语句可以很容易地给你的图加标题。插入下面一行产生图 1-9 :

A456962_1_En_1_Fig9_HTML.jpg

图 1-9

Plotting area with axis labels, Python grid, custom grid, and title

plt.title('this is my plot')

1.13 颜色

随着本书的深入,您将充分利用 Python 的彩色绘图能力。一些可用的颜色有

  • k 代表黑色
  • ' b '代表蓝色
  • “c”代表青色
  • g 代表绿色
  • “m”代表洋红色
  • r 代表红色
  • y 代表黄色
  • “灰色”或“灰色”
  • " lightgray "或" lightgrey "

例如,以下语句将在坐标 x,y 处绘制一个绿点:

plt.scatter(x,y,color='g')

更多颜色的样本可以在

matplotlib.org/examples/color/named_colors.html

颜色属性可以与其他属性一起用在散点()、绘图()和箭头()函数中。

颜色混合

您可以使用规范 color=(r,g,b)从红(r)、绿(g)和蓝(b)原色中混合自己的色调,其中 r、g、b 是混合中红、绿和蓝的值,每个值的范围为 0 到 1。例如,color=(1,0,0)给出纯红色;color=(1,0,1)给出品红色,一种红色和蓝色的略带紫色的混合物;color=(0,1,0)给出绿色;颜色(. 5,0.1)在洋红色中给出更多的红色和更少的蓝色;color(0,0,0)给出黑色;颜色(1,1,1)给出白色。保持 r、g、b 值不变,随着这些值的增加,会产生从黑到白的灰度。也就是说,color=(.1,. 1,.. 1)产生深灰色,color(.7,. 7,.. 7)产生浅灰色,color(.5,. 9,.. 5)产生浅绿色。请注意,当指定“grey”时,它也可以拼写为“gray”。

清单 1-4 展示了如何在程序中混合颜色。第 7-9 行确定了每种颜色在 0-1 范围内的分数。第 7 行中的红色分量依赖于 x,范围从 1-100。在该混合中,绿色和蓝色分量的值均为 0。第 10 行在 x 处从上到下画了一条垂直线,颜色混合由属性 color=(r,g,b)指定。结果如图 1-10 所示。左边的色调几乎是黑色的。这是因为混合中每种颜色的数量为 0 或接*于 0(r = . 01,g=0,b=0)。右边的色调是纯红色,因为在那一侧 r=1,g=0,b = 0;也就是说,红色是完整的强度,没有被绿色或蓝色污染。

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  plt.axis([0,100,0,10])
5
6  for x in np.arange(1,100,1):
7       r=x/100
8       g=0
9       b=0
10      plt.plot([x,x],[0,10],linewidth=5,color=(r,g,b))
11
12 plt.show()
Listing 1-4Program COLORS

A456962_1_En_1_Fig10_HTML.jpg

图 1-10

Red color band produced by Listing 1-4 with r=x/100, g=0, b=0

图 1-11 显示了将蓝色添加到混合中的结果。图 1-12 显示了红色加上绿色的结果。将三原色均匀混合会产生从黑色到白色的灰色阴影,如图 1-13 所示。

A456962_1_En_1_Fig13_HTML.jpg

图 1-13

Grey color band with r=x/100, g=x/100, b=x/100

A456962_1_En_1_Fig12_HTML.jpg

图 1-12

Yellow color band with r=x/100, g=x/100, b=0

A456962_1_En_1_Fig11_HTML.jpg

图 1-11

Purple color band with r=x/100, g=0, b=x/100

每种原色有 256 个可用值。像我在这里做的那样,将它们混合,得到 256 种 3 ,差不多有 1700 万种不同的色调。

颜色强度

颜色的强度可以用 alpha 属性来控制,如清单 1-5 中的第 6-8 行所示,这产生了图 1-14 。alpha 可以在 0 到 1 之间变化,1 表示色调最强,0 表示色调最弱。

A456962_1_En_1_Fig14_HTML.jpg

图 1-14

Color intensity controlled by the attribute alpha shown in Listing 1-5

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  plt.axis([0,100,0,10])
5
6  plt.scatter(60,50,s=1000,color='b',alpha=1)
7  plt.scatter(80,50,s=1000,color='b',alpha=.5)
8  plt.scatter(100,50,s=1000,color='b',alpha=.1)
9
10 plt.show()
Listing 1-5Program COLOR_INTENSITY

1.14 过度绘制

您通常使用函数 plt.scatter()创建点,plt.plot()创建线,plt.arrow()创建箭头和线(无头箭头)。重要的是要知道哪一个会超过哪一个。你不想创建一个精心制作的图像,却发现它被其他东西过度渲染了。图 1-15 显示了一些例子。在(A)中,先有一条红线(1),然后是一条绿线(2)。请注意,第二行覆盖了第一行。在(B)中,首先绘制一个蓝点(1),然后绘制一条红线(2)。这条线画多了点。然后绘制另一个蓝点(3)。它没有过分渲染。在(C)中,首先绘制红点(1),然后是蓝点(2),然后是黄点(3)。他们互相吹捧。总之,

A456962_1_En_1_Fig15_HTML.jpg

图 1-15

Overplotting with lines and dots

  • 新线覆盖旧线。
  • 线条覆盖圆点。
  • 新点覆盖旧点。

这些示例是由以下代码创建的:

#————————————————————(A)
plt.text(45,10,'(A)')
plt.plot([20,60],[20,20],linewidth=5,color='r')
plt.text(13,21,'1')
plt.plot([30,30],[10,30],linewidth=5,color='g')
plt.text(28,6,'2')

#————————————————————(B)
plt.text(45,75,'(B)')
plt.scatter(40,60,s=800,color='midnightblue')
plt.text(38,50,'1')
plt.plot([20,60],[60,60],linewidth=5,color='r')
plt.text(13,61,'2')
plt.scatter(60,60,s=800,color='b')
plt.text(58,50,'3')

#————————————————————(C)
plt.text(108,56,'(C)')
plt.scatter(100,40,s=800,color='r')
plt.text(98,30,'1')
plt.scatter(110,40,s=800,color='b')
plt.text(108,30,'2')
plt.scatter(120,40,s=800,color='y')
plt.text(118,30,'3')

图 1-16 为箭头。在(A)中,先放下一条红线,然后放下一个绿色箭头。箭没有超过线。然后画一个蓝色的箭头。红线仍然优先并覆盖蓝色箭头。在(B)中,首先画出深蓝色的点,然后画出红色的箭头。箭头遮住了深蓝色的点。然后画一个蓝点。箭头仍然优先并覆盖蓝点。在(C)中,先画一个红色箭头,然后画一个蓝色箭头。新的蓝色箭头覆盖旧的红色箭头。因此,我们可以得出结论

  • 线条覆盖箭头。
  • 箭头覆盖圆点。
  • 新箭盖旧箭。

一般来说,我们可以说线条过度描绘了一切,甚至是更古老的线条;圆点不会过度绘制任何东西,除了较老的圆点;和箭头过度绘制点和旧的箭头,而不是线。

A456962_1_En_1_Fig16_HTML.jpg

图 1-16

Overplotting with lines, arrows, and dots

产生图 1-16 的代码是

#——————————————————————(A)
plt.plot([20,60],[20,20],linewidth=5,color='r')
plt.text(13,21,'1')
plt.arrow(30,30,0,-20,linewidth=5,head_length=4,head_width=2,color='g')
plt.text(22,10,'2')
plt.arrow(50,30,0,-20,linewidth=5,head_length=4,head_width=2,color='b')
plt.text(54,10,'3')
#——————————————————————(B)
plt.scatter(40,60,s=800,color='midnightblue')
plt.text(39,51,'1')
plt.arrow(20,60,60,0,linewidth=5,head_length=4,head_width=2,color='r')

plt.text(12,61,'2')
plt.scatter(60,60,s=800,color='b') plt.text(58,51,'3')
#——————————————————————(C)
plt.arrow(90,40,40,0,linewidth=5,head_length=4,head_width=2,color='r')
plt.text(82,41,'1')
plt.arrow(100,50,0,-20,linewidth=5,head_length=4,head_width=2,color='b')
plt.text(92,29,'2')

1.15 背景颜色

前面的部分提供了绘制背景的含义。通常,图像是在白色背景下用彩色绘制在计算机屏幕上的。在黑色或深蓝色背景下绘图有时会很有用。图 1-17 显示了取自章节 6 的示例。通过首先用黑线覆盖绘图区域来获得黑色背景。然后用绿线绘制球体,绿线覆盖黑线。你也可以用散点()来画背景,但是线条需要更少的计算机处理时间。如果你选择用点来画球体,背景线会把它们盖住。如果你真的用点绘制了球体,你可以先用点绘制背景,新的球体点会覆盖它们。

A456962_1_En_1_Fig17_HTML.jpg

图 1-17

Sphere plotted against a black background

1.16 绘图区形状

当使用 plt.axis()命令设置绘图区时,它通常在输出窗格中显示为矩形而不是正方形,即使 x 和 y 轴尺寸指示它应该是正方形。这显示在图 1-18 中,它是通过列出 1-6 创建的,其中第 7 行中的值指示面积应该是正方形。这种扭曲有时可能会有问题,因为它会扭曲对象。例如,一个数学上正确的圆可能显示为椭圆形,或者一个数学上正确的正方形可能显示为矩形,如图 1-18 所示。

A456962_1_En_1_Fig18_HTML.jpg

图 1-18

Distortion of a mathematically correct square

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  plt.grid(True)
5  plt.axis('on')
6
7  plt.axis([-10,10,10,-10])
8
9  #————————————————custom grid
10 x1=-10
11 x2=10
12 y1=10
13 y2=-10
14
15 dx=.5
16 dy=-.5
17 for x in np.arange(x1,x2,dx):
18      for y in np.arange(y1,y2,dy):
19           plt.scatter(x,y,s=1,color='lightgray')
20
21 #————————————————square box
22 plt.plot([-5,5],[-5,-5],linewidth=2,color='k')
23 plt.plot([5,5],[-5,5],linewidth=2,color='k')
24 plt.plot([5,-5],[5,5],linewidth=2,color='k')
25 plt.plot([-5,-5],[5,-5],linewidth=2,color='k')
26
27 plt.show()

Listing 1-6Program SQUARE

如图 1-19 所示,您可以通过包含该命令来纠正这种失真

plt.axes().set_aspect('equal')

在第 7 行之后的清单 1-6 中。这将通过对绘图区域求*方来对方框求*方。不幸的是,它也缩小了绘图区的宽度。对于某些图像来说,这可能不太方便,因为在这些图像中,您可能希望绘制区域的整个宽度没有伴随的扭曲。

A456962_1_En_1_Fig19_HTML.jpg

图 1-19

Distortion corrected by equalizing axes

1.17 如何纠正形状扭曲

图 1-20 再次说明了这个问题,这一次当你试图画一个圆的时候。您有一个 x 和 y 尺寸数值相等的绘图区域,每个尺寸的范围为 100 个单位。当你画一个数学上正确的圆时,你会得到图 1-20 ,一个椭圆。清单 1-7 制作图 1-20 。

A456962_1_En_1_Fig20_HTML.jpg

图 1-20

Distortions of a mathematically correct circle

1  plt.axis([0,100,100,0])
2
3  r=40
4  alpha1=radians(0)
5  alpha2=radians(360)
6  dalpha=radians(2)
7  xc=50
8  yc=50
9  plt.scatter(xc,yc,s=10,color='k')
10 for alpha in np.arange(alpha1,alpha2,dalpha):
11      x=xc+r*cos(alpha)
12      y=yc+r*sin(alpha)
13      plt.scatter(x,y,s=5,color='k')
Listing 1-7Program DISTORTED_CIRCLE

显然,这是行不通的。你必须想办法得到一个真正的圆,而不是椭圆。

1.17.1 绘图时应用比例因子

图 1-20 中的圆圈由散点()构成。您可以尝试在绘制时对每个点的 x 坐标应用一个校正因子,即 sfx 的比例因子。你如何得到 sfx?使用尺子,在显示器屏幕上测量 x 和 y,这是椭圆圆的 x 和 y 显示范围。由于显示器的水*和垂直像素间距不同,因此您需要使用一把尺子。假设 x = 7.5cm 厘米,y = 5 厘米。应用于每个点的 x 坐标的比例因子是 sfx =∏y/∏x = 5/7.5≊。将清单 1-6 中的第 11 行替换为

x=xc+sfxrcos(alpha)

其中 sfx=.67,你得到图 1-21 。这种方法的问题是,要绘制的每个 x 坐标都必须乘以 sfx。

A456962_1_En_1_Fig21_HTML.jpg

图 1-21

Distortion corrected by applying a scale factor to each point as it is plotted

1.17.2 最佳方法:在 plt.axis()中缩放轴

校正失真的最佳方法是通过 plt.axis()函数将比例因子应用于 x 轴。以上面的圆为例,应用于 x 轴的比例因子为 x/y = 7.5/5 = 1.5。在 plt.axis()函数中使用它,它变成

plt.axis([0,150,100,0])

产生图 1-22 的圆代码,现在变成清单 1-8 。

A456962_1_En_1_Fig22_HTML.jpg

图 1-22

Distortion corrected by applying a scale factor to the x axis

1  plt.axis([0,150,100,0])
2
3  r=40
4  alpha1=radians(0)
5  alpha2=radians(360)
6  dalpha=radians(2)
7  xc=75
8  yc=50
9  plt.scatter(xc,yc,s=10,color='k')
10 for alpha in np.arange(alpha1,alpha2,dalpha):
11      x=xc+r*cos(alpha)
12      y=yc+r*sin(alpha)
13      plt.scatter(x,y,s=5,color='k')
14
15 plt.show()
Listing 1-8THE_BEST_WAY_TO_CORRECT_DISTORTIONS

这给了你图 1-22 ,一个真正的圆。清单 1-8 中的第 1 行确保 x 轴的数值长度是 y 轴的 1.5 倍。y 轴可以具有任何数值长度。按照 plt.axis()函数的定义,只要 x 轴是 y 轴的 1.5 倍,您仍然会得到一个真正的圆或正方形。比如 plt.axis([0,1800,1200,0])就可以。在本书的大多数示例程序中,您将使用由 plt.axis([0,150,100,0])定义的标准绘图区域。1.5 的缩放因子可能需要针对您的显示器进行微调。

1.18 坐标轴

正如您所看到的,为了构建图形图像,点、线和箭头被放置在绘图区域的坐标上,这些坐标具有相对于 x=0,y=0 处的原点的数值。虽然没有必要显示坐标轴或其原点,但它们通常有助于创建图像,因为它们指示(0,0)点的位置以及正 x 和 y 值的方向。图 1-23 显示了使用列表 1-9 第 23 和 24 行中的 plt.arrow()函数绘制的轴。

A456962_1_En_1_Fig23_HTML.jpg

图 1-23

A convenient working surface: 100x150 plotting area, Python grid, custom grid, frame out of the way

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  x1=-10 #——Δx=150
5  x2=140
6  y1=90 #—-Δy=100
7  y2=-10
8  plt.axis([x1,x2,y1,y2])
9
10 plt.axis('on')
11 plt.grid(True)
12
13 plt.title('Sample Axes')
14
15 #—————————————————grid
16 dx=5
17 dy=-5
18 for x in np.arange(x1,x2,dx):
19      for y in np.arange(y1,y2,dy):
20           plt.scatter(x,y,s=1,color='lightgray')
21
22 #—————————————-coordinate axes
23 plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
24 plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
25
26 plt.show()

Listing 1-9Program COORDINATE_AXES

1.19 常用的绘图命令和功能

在前面几节中,您已经看到了几个绘图命令和函数的使用。在接下来的几节中,您将更深入地研究这些命令以及其他命令。您还将了解这些函数的一些可选属性。请注意,我不会列出所有可用的属性,因为它们中的大多数通常都不会用到;我在这里只包括了创建本书插图所需的最重要的属性。

1.19.1 使用散点图的点和圆点( )

plt.scatter(x,y,s=size,color='color ')

scatter()在坐标 x,y 处绘制实心点。size 是点的大小:s=.5 表示一个小点;s=10 则更大。我们用点这个术语来描述一个小点。点相对于绘图的物理大小将取决于绘图区域的比例。确定一个点的最合适大小的最好方法是通过放大或缩小来进行实验,直到你得到你想要的。颜色是点的颜色。scatter()还有其他属性,但我们不会在本书中使用它们。

我在前面关于颜色的部分讨论了颜色;对于大多数正常的应用程序,这些颜色应该是令人满意的。例如,color='r '给出一个红点,color='k '给出一个黑点。如前所述,您还可以使用语句 color=(r,g,b)混合 rgb 颜色,其中 r=红色,g =绿色,b =蓝色。

A456962_1_En_1_Fig24_HTML.jpg

图 1-24

Green scatter() dot at x=40,y=20

这三个参数的值可以从 0 到 1。虽然颜色有时很有用,但用“k”(黑色)、“灰色”和“浅灰色”可以做很多事情。一般来说,给一幅画添加色彩对传达信息有很大的帮助。然而,过多的颜色会造成混乱。以 scatter()为例,

plt.scatter(40,20,s=2,color='g')

在 x=40,y=20 处绘制一个大小为 2 的绿点,如图 1-24 所示。注意,这些 x,y 坐标是相对于坐标轴的原点的。

1.19.2 使用 plot()的线条

plt.plot([x1,x2],[y1,y2],linewidth=linewidth,
                       color='color ',linestyle='linestyle')

该命令绘制一条从 x1,y1 到 x2,y2 的直线。它的宽度由 linewidth 指定,颜色由 color 指定,样式由 linestyle 指定。关于线宽,线条宽度的外观将取决于绘图的比例,因此最好通过实验来确定。关于线型,图 1-25 所示的通常就足够了。

A456962_1_En_1_Fig25_HTML.jpg

图 1-25

Line styles

图 1-25 中的线条由以下代码创建:

plt.plot([40,100],[20,20],linewidth=2,color='r')
plt.plot([40,100],[30,30],linewidth=4,color='g',linestyle=':')
plt.plot([40,100],[40,40],linewidth=6,color='b',linestyle='–')
plt.plot([40,100],[50,50],linewidth=2,color='k',linestyle='-.')

还有其他可用的线条样式,可以通过互联网搜索找到。

箭头

A456962_1_En_1_Fig26_HTML.jpg

图 1-26

Arrows

plt.arrow(x,y,∆x,∆y,line_width='linewidth',
                    head_length='headlength',
                    head_width='headwidth',
                    color='color ')

图 1-26 中所示的箭头是用以下命令绘制的:

plt.arrow(40,20,60,0,linewidth=1,color='r',head_length=5,
     head_width=3)

plt.arrow(40,30,60,0,linewidth=1,color='g',linestyle=':',
     head_length=10,head_width=5)

plt.arrow(40,40,60,0,linewidth=1,color='b',linestyle='–',
     head_length=8,head_width=4)

plt.arrow(40,50,60,0,linewidth=4,color='k',linestyle='-',
     head_length=8,head_width=3)

∏x 和∏y 是 x 和 y 从箭杆开始到结束的变化;线宽决定了箭杆的粗细。head_width 指定头部的宽度;head_length 指定了它的长度。箭头的长度增加了箭头的总长度。将箭杆长度与箭头长度相加得到箭的总长度,这对于垂直和水*箭来说不是什么大问题。例如,要绘制总长为 13 的水*箭头,需要指定 x 为 7,head_length=3。但是,当构造必须符合特定长度的斜箭头时,这可能会很棘手。在这种情况下,最好的办法是使用试错法调整 x、y 和 head_length,直到结果正确。通常你会希望 head_length 和 head_width 保持固定,所以通常改变的是 x 和 y。

箭头也可以用来画线。数据输入的形式有时比 plt.plot([x1,x2],[y1,y2])函数更方便。要获得没有箭头的行,只需省略 head_length 和 head_width 属性。也就是说,编写以下内容:

plt.arrow(x,y,x,y,line_width='linewidth ',color='color ')

文本

Python 将文本视为图形元素。在 Python 绘图中放置文本的方法是使用 plt.text()函数。图 1-27 中显示的文本样本是由清单 1-10 中的代码生成的。第 30 行和第 31 行显示了如何旋转文本:

A456962_1_En_1_Fig27_HTML.jpg

图 1-27

Text samples

plt.text(x,y,'text',color='color ',size='size',fontweight='fontweight ',
     fontstyle='fontstyle',rotation=degrees)

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  x1=-10
5  x2=140
6  y1=90
7  y2=-10
8  plt.axis([x1,x2,y1,y2])
9
10 plt.axis('on')
11 plt.grid(False)
12
13 plt.title('Text Samples')
14
15
16 #—————————————————text samples
17 plt.text(20,10,'small text',size='small')
18 plt.text(20,15,'normal text')
19 plt.text(20,20,'large text',size='large')
20
21 plt.text(20,30,'large bold text',size='large',fontweight='bold')
22 plt.text(20,35,'large bold,italic
23 text',size='large',fontweight='bold',fontstyle='italic')
24 plt.text(20,40,'large, pure, bold italic
25 text',size='large',fontweight='bold',fontstyle='italic',color=(.5,0,.5))

26 plt.text(20,45,'large, light purple, bold italic
27 text',size='large',fontweight='bold',fontstyle='italic',color=(.8,0,.8))
28 plt.text(20,50,'light purple text',color=(.8,0,.8))
29
30 plt.text(100,50,'text at 45 degrees',rotation=45,color='k')
31 plt.text(90,-3,'text at -60 degrees',rotation=-60,color='g')
32
33 plt.text(20,65,r'$P(\lambda)=2 \pi c^{2} h
       \int_{\lambda1}^{\lambda2}\frac{\lambda^{-5}\epsilon}
       {e^{\frac{hc}{\lambda k t}}-1}d\lambda$',size='large')
34
35 plt.show()
Listing 1-10Program TEXT_SAMPLES

图 1-27 底部的方程是马克斯·普朗克黑体辐射方程,它给出了波长从λ1 → λ2 的黑体辐射功率。清单 1-9 中的第 33 行绘制了该等式的文本。Python 显示这个等式的能力说明了 Python 的一些图形能力。Python 可以将 Latex 可以完成的大部分工作绘制成文本。注意,在第 33 行,单引号之间的 Latex 文本前面是小写的 r。前面的 r 告诉 Python 将该字符串视为原始字符串,从而保留 Latex 所需的反斜杠。因为美元符号,matplotlib 知道它是 Latex。Latex 代码放在美元符号之间。显然,可以显示更多的 Latex 文本。事实上,这整本书都是用 Latex 编写和格式化的。里面所有的插图都是用 Python 创作的。

列表、元组和数组

用单独的线条绘制一个对象(如一个盒子)通常需要大量的输入。例如,要绘制一个方形框,您可以用

plt.plot([-20,20],[-20,-20],linewidth=2,linestyle='–',color='r')
plt.plot([20,20],[-20,20],linewidth=2,linestyle='–',color='r')
plt.plot([20,-20],[20,20],linewidth=2,linestyle='–',color='r')
plt.plot([-20,-20],[20,-20],linewidth=2,linestyle='–',color='r')

更有效的方法是使用列表:

x=[-20,20,20,-20,-20]
y=[-20,-20,20,20,-20]
plt.plot(x,y,linewidth=2,linestyle='–',color='g')

这些列表中的每个 x[i],y[i]对代表一个点的坐标。plt.plot(x,y…)函数自动连接点 x[i],y[i]与 x[i+1],y[i+1]。这些列表中的第 5 个元素与元素 0 具有相同的坐标。这就关闭了盒子。

方括号中的有限数字序列,如 x=[x1,x2,x3,x4,x5]和 y=[y1,y2,y3,y4,y5]称为列表。列表非常有用,尤其是在计算机图形学中。x,y 对(x1,y1),(x2,y2),(x3,y3)....在这些列表中,替换各个 plt.plot 函数中的语法([x1,x2],[y1,y2])。你可以用它们画出几乎任何形状;这些线将按顺序连接。

列表元素可以如上单独定义,也可以按以下结构指定:

1  x=[ ]
2  for i in range(10):
3        x.append(i*i)
4
5  print(x)
6
7  [0,1,4,9,16,25,36,49,64,81]

第 1 行定义了一个空列表 x,它不包含任何元素。没有规定列表的长度。从第 2 行开始的循环将 I 从 0 递增到 9 (10 个元素)。第 3 行将 i*i 作为从元素 0 开始的循环中的一个附加元素添加到列表中。第 7 行显示了结果。

另一种方法是预定义列表元素,如下面的第 1 行所示。列表中的数字可以是任何数字;它们只是用来定义列表的长度。在从第 3 行开始的循环中,第 4 行将每个元素的值更改为 i*i。

1  x=[0,1,2,3,4,5,6,7,8,9]
2
3  for i in range(10):
4       x[i]=(i*i)
5
6  print(x)
7
8  [0,1,4,9,16,25,36,49,64,81]

列表的长度也可以定义为

g=[0]*10

其中列表 g 被定义为具有 10 个元素,每个元素的值为 0。要获取列表的长度,请使用函数

len(x)

它返回列表 x 的长度,长度是列表中元素的数量。例如,在下面的脚本中,循环将处理列表 x 中从元素 0 到 x 的最后一个元素的所有元素,每个元素加 3:

x=[4,0,7,1]

for i in range(len(x)):
     x[i]=x[i]+3

print(x)

[7,3,10,4]

您将在接下来的程序中使用所有这些方法。

元组是一个数字序列,例如 x=(x0,x1,x2,x3,x4),类似于列表。区别在于,除了括号的样式,元组中的元素是不可变的,这意味着它们不能被改变(变异)。另一方面,列表中的元素是可以改变的。元组可以不带括号使用。例如,v=7,12 等价于 v=(7,12),它定义了一个具有两个元素的元组;第一个值为 7,第二个值为 12。

使用列表和元组肯定是一种更有效的编码方法,而不是走很长的路;也就是说,对图形的每条边使用单独的 np.plot()行。另一方面,他们有时会有问题。例如,如果您有很长的 x 和 y 列表或元组,并且您的绘图结果不正确,那么找到违规元素可能是一个冗长的过程。使用复制和粘贴可以加快长时间的速度。复制第一行并将其作为第二行粘贴到代码中,然后更改 x 和 y 坐标值以生成第二条线段,对其余的行依此类推。显然,如果您有很多点要处理,您不会想要一遍又一遍地复制和粘贴 plt.plot()函数,在这种情况下,列表或元组可能会成为更可行的选项。是使用列表和元组,还是用长方法,这是个人的偏好。

如果您只想绘制一条线段,可以使用以下语法

x=[x1,x2]
y=[y1,y2]
plt.plot(x,y)

或者

plt.plot([x1,x2],[y1,y2])

要绘制两条线段,您可以使用

x=[x1,x2,x3]
y=[y1,y2,y3]
plt.plot(x,y)

或者

plt.plot([x1,x2],[y1,y2])
plt.plot([x2,x3],[y2,y3])

等等。每种方法都有其优点。在这篇课文中你会用到这两个词。

事实上,清单 1-11 使用了这两种方法。它首先使用单独的 np.plot 命令为每一边绘制一个红色方块,然后使用列表绘制一个绿色方块。输出如图 1-28 所示。

A456962_1_En_1_Fig28_HTML.jpg

图 1-28

Green box plotted using lists; red box plotted without lists

1  import numpy as np
2  import matplotlib.pyplot as plt
3
4  plt.axis([-75,75,50,-50])
5
6  plt.axis('on')
7  plt.grid(True)
8
9  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
10 plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
11
12 plt.text(22,-3,'x')
13 plt.text(-5,25,'y')
14
15 #—————————————————red box
16 plt.plot([-20,20],[-20,-20],linewidth=2,color='r')
17 plt.plot([20,20],[-20,20],linewidth=2,color='r')
18 plt.plot([20,-20],[20,20],linewidth=2,color='r')
19 plt.plot([-20,-20],[-20,20],linewidth=2,color='r')
20
21 #——————————————green list box
22 x=[-30,30,30,-30,-30]
23 y=[-30,-30,30,30,-30]
24 plt.plot(x,y,linewidth=2,color='g')
25
26 plt.show()
Listing 1-11Program LISTS

很明显,用长方法(第 16-19 行)比使用列表(第 22-24 行)需要更多的输入。虽然列表和元组有一些节省时间的特性,但使用起来可能会很棘手。一个常见的陷阱是忘记在这两个元素中,第一个元素不是元素 1,而是元素 0。例如,有一个列表

x = [1,2,3,4,5] (1.1)

如果你用 print('x[4])= ',x[4])语句将它包含在一个程序中,你将得到一个 5 的答案。如果你要求 x[1],你会得到 2。元组具有相同的特质。这种特殊的特性非常容易出错,在使用列表和元组时必须牢记在心。顺便说一句,当请求列表或元组中元素的值时,必须使用方括号,而不是圆括号。例如,要得到上面 x 中的第三个元素,你需要 x[2]。

阵列

以下是一个典型的数组:

a = NP . array[[x0,y0,z0],[x1,y1,z1],[x2,y2,z2],…………………。[xn、yn、zn] ]

array 是一个 numpy 函数,前面必须有 np 前缀。如你所见,上面的数组 A 有 n+1 个元素,每个元素都是一个包含三项的列表。每个元素可以代表三维空间中一个点的 x,y,z 坐标。假设您有一个包含三个点的数组,如

A=np.array([ [7,3,9],[34,21,65],[19,21,3] ])

其中的每个元素代表一个点。例如,要打印第二个点(点 1)的 x、y、z 坐标,

print(A[1])

34,21,65

结果当然不是 7,3,9,因为那是 0 点。要打印点 1 的 z 坐标,

print(A[1,2])

65

1 是点 1 (0,1,2);2 是 z 坐标(x,y,z)。对数组的其他操作类似于对列表的操作。做三维图形的时候用数组非常方便。你将在后面的章节中用到它们。

1.19.7 阿兰格( )

arange()是一个 numpy 函数。这对于在极限之间增加浮点变量很有用。它必须与 np 一起使用。前缀,除非它是用 from Numpy 导入范围显式导入的。语法是

for x in np.arange(start,stop,step):

这将从起点到终点以步长增量产生 x 值。所有值都是浮点数。冒号必须包含在末尾。举个例子,

for x in np.arange(1,5,2):
     print(x)
1
3

5 号怎么了?你不是应该得到 1,1+2=3,3+2=5 吗?5 由于计算机内的小舍入误差而丢失。也就是说,当你的电脑加 3+2 时,它可能会得到比 5 稍微大一点或小一点的东西,这意味着你可能会得到也可能不会得到 5。这说明了 arange()的一个缺点。解决方法是使止损值比你想要的稍大(或者如果是负向的话,比你想要的稍小)。

for x in np.arange(1,5.1,2):
     print(x)
1
3
5

如果您通过从 0 到 360 度递增一个角度来绘制一个圆,并且您发现该圆没有闭合,而是留下了一个小间隙,则 np.arange 函数中的舍入误差可能是问题所在。

开始、停止和步进可以有正值也可以有负值。如果停止小于开始,步长应为负值。

1.19.8 范围( )

range()非常有用,尤其是在循环中,用于在一个范围内递增整数变量。这是一个标准的 Python 函数,不需要前缀。语法是

for x in range(start,stop,step):

其中所有值都是整数。举个例子,

for x in range(1,5,1):
     print(x)

1
2
3
4

再说一遍,5 号怎么了?有悖常理的是,Python 选择让 range()返回值只比 stop 少一步。要得到 5,你必须将 stop 延长一步。

for x in range(1,6,1):
     print(x)
1
2
3
4
5

与 range()一样,start、stop 和 step 可能有负值。如果停止小于开始,步长应为负值。

1.20 摘要

在这一章中,你回顾了 Python 的基本命令,这些命令是 Python 的基础,也是图形编程的专用命令。现在你已经拥有了理解以下章节和制作本书插图所需的所有编程工具。所有的图形都是通过恰当地使用三个基本组成部分创造出来的:点、线和箭头。一旦您理解了如何在 Python 程序中使用它们,主要的困难就变成了二维和三维向量数学和几何的使用,这将在接下来的工作中无处不在。

二、二维图形

在这一章中,你将学习如何用点和线构建二维图像。在第一章中,你学习了用 Python 创建图像的基本工具。在这一章中,你将在这一点上展开并学习在二维空间中创建、*移和旋转形状的方法。你还将学习相对坐标的概念,这将在本书的其余部分广泛使用。像往常一样,您将通过示例程序探索这些概念。

2.1 从点到线

您看到了如何使用命令创建一行

plt.plot([x1,x2],[y1,y2],attributes)

这将绘制一条从(x1,y1)到(x2,y2)的线,其属性指定线的宽度、颜色和样式。有时,可能需要使用点来构建一条线。图 2-1 和 2-2 显示了几何图形:一条从点 1 开始到点 2 结束的斜线。它的长度是 q。图 2-2 中显示的直线是坐标 x,y 处的点 p。绘制直线时,从 1 开始,逐步向 2 推进,计算每一步 p 的坐标,并在每一步绘制一个点。这种分析利用了向量,向量将在以后广泛使用。

请注意,在这些模型中没有坐标轴。这种分析是一般性的;它适用于任何二维正交坐标方向。

A456962_1_En_2_Fig1_HTML.jpg

图 2-1

Geometry for creating a line from dots (a)

A456962_1_En_2_Fig2_HTML.jpg

图 2-2

Geometry for creating a line from dots (b)

要从点 1 向点 2 前进,首先要确定从 1 到 2 的方向。这将表示为一个单位矢量(单位矢量将以带帽的粗体显示;粗体全向量):

$$ \widehat{\mathbf{u}}=ux\widehat{\mathbf{i}}+uy\widehat{\mathbf{j}} $$

(2-1)其中 Iˇ和 jˇ为 x、y 方向的单位向量;ux 和 uy 是在 x 和 y 方向上的标量分量。

ux 是与 x 轴夹角的余弦值;uy 是与 y 轴之间角度的余弦值。ux 和 uy 通常被称为方向余弦。很容易证明它们是余弦:与 x 轴之间角度的余弦为 UX/| 0 |,其中| 0 |是的标量幅度。由于\是单位向量,| \ | = 1;。角度的余弦则为 ux/(1)=ux。uy 也一样。

记住

$$ \left|\widehat{\mathbf{u}}\right|=1 $$

(2-2)是很重要的,因为这个特性使你能够乘以一个量级来得到一个位置向量。例如,通过将乘以 L,可以得到从点 1 到 p 的向量 v1p,其中 L 是从 1 到 p 的距离,L 给出向量的大小,而给出向量的方向。那么从点 1 到 p 的向量就是

$$ \mathbf{v}\mathbf{l}\mathbf{p}=L\left(ux;\widehat{\mathbf{i}}+uy;\widehat{\mathbf{j}}\right) $$

(2-3)

可以从坐标值计算 ux 和 uy 为

$$ ux=A/Q=\left(x2\hbox{-} x1\right)/Q $$

(2-4)

$$ uy=B/Q=\left(y2\hbox{-} y1\right)/Q $$

(2-5)其中(x1,y1)和(x2,y2)为点 1 和 2 的坐标,

$$ Q=\sqrt{{\left(x2\hbox{-} x1\right)}²+{\left(y2\hbox{-} y1\right)}²} $$

(2-6)

清单 2-1 给出了两个用点画线的例子。结果如图 2-3 所示。更小的点和更*的间距将产生更细的线(绿色),这几乎与使用 plt.plot([x1,x2],[y1,y2])函数获得的线一样好。

A456962_1_En_2_Fig3_HTML.jpg

图 2-3

Dot lines created by Listing 2-1

1   """
2   DOTLINE
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7
8   plt.axis([-20,130,80,-20])
9
10  plt.axis('on')
11  plt.grid(True)
12
13  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
14  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
15  plt.text(15,-3,'x')
16  plt.text(-5,15,'y')
17
18  #———————————————————green line
19  x1=20
20  x2=120
21  y1=40
22  y2=20
23
24  q=np.sqrt((x2-x1)**2+(y2-y1)**2)
25  ux=(x2-x1)/q
26  uy=(y2-y1)/q
27
28  for l in np.arange(0,q,.5):
29        px=x1+l*ux
30        py=y1+l*uy
31        plt.scatter(px,py,s=1,color='g')
32
33  #———————————————————————————————————————————blue line
34  x1=20
35  x2=120
36  y1=45
37  y2=25
38
39  q=np.sqrt((x2-x1)**2+(y2-y1)**2)
40  ux=(x2-x1)/q
41  uy=(y2-y1)/q
42
43  for l in np.arange(0,q,2):
44        px=x1+l*ux
45        py=y1+l*uy
46        plt.scatter(px,py,s=1,color='b')
47
48  plt.show()
Listing 2-1Program DOTLINE

这个程序应该是不言自明的,因为定义与先前的分析是一致的。

2.2 点画

有趣的图案可以通过将点排列成几何图案来创建。图 2-4 显示了一些例子。在所有这三种情况下,这些点都以二维 x,y 矩阵排列。您可以改变点的大小、颜色以及矩阵的 x 和 y 限制。每个矩阵都创建有嵌套的 for 循环,如清单 2-2 ,第 20-22、25- 35 和 40-45 行所示。这些嵌套循环在 x 方向扫描,然后在每个 x 处,在 y 方向扫描,从而填充一个矩形区域。蒙德里安是由三个单独的点矩形加上一个大红点。

A456962_1_En_2_Fig4_HTML.jpg

图 2-4

Dot art created created by Listing 2-2

在第 7 行,您导入了 random。这是一个随机函数库,您可以在第 42、43 和 44 行中使用它来产生随机的原色 r、g、b 分量。它们混合在第 45 行中。您将使用 random 的 random.randrange(a,b,c)函数来获取随机值。您也可以使用 numpy 中包含的随机函数,尽管语法有点不同。这里使用随机库是为了说明除了 numpy 还有其他数学库。

random.randrange(a,b,c)返回介于 a 和 b 之间的随机数,增量为 c。a、b 和 c 必须是整数。为了获得广泛的随机数选择,在第 42-44 行中设 a=1,b=100,c=1。但是第 42 行中的 rr 必须在 0 和 1.0 之间,所以在第 42 行中除以 100。这为颜色混合的红色分量 rr 提供了一个介于 0 和 1.0 之间的随机值。类似地,对于第 43 和 44 行中的 rg 和 rb,绿色和蓝色分量。如你所见,Klee 中的结果相当有趣。

1   """
2   DOTART
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   import random
8
9   plt.axis([10,140,90,-10])
10
11  plt.axis('off')
12  plt.grid(False)
13
14  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
15  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
16  plt.text(15,-3,'x')
17  plt.text(-5,15,'y')
18
19  #————————————————————————————————————————————-plot Seurat

20  for x in np.arange(20,40,4):
21        for y in np.arange(10,60,4):
22              plt.scatter(x,y,s=8,color='b')
23
24  #————————————————————————————————————————————–plot Mondrian

25  for x in np.arange(60,80,1):
26        for y in np.arange(10,40,1):
27              plt.scatter(x,y,s=8,color='y')
28
29  for x in np.arange(60,80,1):
30        for y in np.arange(40,60):
31              plt.scatter(x,y,s=8,color='g')
32
33  for x in np.arange(65,80,1):
34        for y in np.arange(25,30,1):
35              plt.scatter(x,y,s=8,color='b')
36
37  plt.scatter(70,30,s=50,color='r')
38
39  #————————————————————————————————————————————plot Klee

40  for x in np.arange(100,120,2):
41        for y in np.arange(10,60,2):
42              rr=random.randrange(0,100,1)/100 #–random red 0<=rr<=1
43              rg=random.randrange(0,100,1)/100 #–random green 0<=rg<=1
44              rb=random.randrange(0,100,1)/100 #–random blue 0<=rb<=1
45        plt.scatter(x,y,s=25,color=(rr,rg,rb))
46
47  #————————————————————————————————————————————labels
48  plt.text(105,67,'Klee')
49  plt.text(60,67,'Mondrian')
50  plt.text(21,67,'Seurat')
51
52  plt.show()
Listing 2-2Program DOTART

2.3 由点形成的圆弧

清单 2-3 使用点绘制圆弧。这是你第一个处理圆坐标,角度和三角函数的程序。清单 2-3 所用的几何形状如图 2-5 所示。输出如图 2-6 所示。

清单 2-3 中的第 25-31 行绘制了弧线。曲率中心在(xc,yc)处,如第 20 行和第 21 行中所定义的。线 22 中的曲率半径是 r。圆弧从点 1 开始,该点相对于 x 轴成角度 p1。它终止于点 2,该点成角度 p2。这些角度分别为 20 度和 70 度,在第 25 行和第 26 行中设置,它们被转换为弧度,这是 np.sin()和 np.cos()所需的单位。在后面的程序中,您将使用 radians()函数,该函数将参数从角度转换为弧度。如线 27 所示,弧上的点间隔开角度增量 dp。dp 设置为圆弧 p2-p1 所跨越的总角度除以 100。更宽的间距,比如(p2-p1)/20,特别是当与更小的点尺寸结合时,将给出更粗糙的弧。从第 28 行到第 31 行的循环使用 arange()函数将每个点的角度增加 dp。第 29 行和第 30 行计算每个点相对于全局 x,y 系统的坐标,该系统的原点在(0,0)。全局坐标是用于绘图的坐标。xp=rnp.cos(p)和 yp=rnp。(sin(p)是 p 沿弧相对于(xc,yc)处弧的曲率中心的坐标。这些是本地坐标。必须将曲率中心的坐标(xc,yc)添加到局部坐标中,以获得相对于 x=0,y=0 的全局坐标。这是在第 29 行和第 30 行完成的。第 31 行使用全局坐标在每个位置绘制了一个大小为 1 的绿点。结果如图 2-5 所示,代码如清单 2-4 所示。

A456962_1_En_2_Fig6_HTML.jpg

图 2-6

Circular arc created with np.scatter() dots

A456962_1_En_2_Fig5_HTML.jpg

图 2-5

Geometric model used for creating a circular arc with scatter() dots, created by Listing 2-4

1   """
2   PARC
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   plt.axis([-10,140,90,-10])
9
10  plt.axis('on')
11  plt.grid(True)
12
13  #—————————————————————————————————————————————axes
14  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
15  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
16
17  plt.text(16,-3,'x')
18  plt.text(-5,17,'y')
19
20  xc=20

21  yc=20

22  r=40

23
24  #——————————————————————————————————————————plot arc
25  p1=20*np.pi/180

26  p2=70*np.pi/180

27  dp=(p2-p1)/100

28  for p in np.arange(p1,p2,dp):

29      x=xc+r*np.cos(p)

30      y=yc+r*np.sin(p)

31      plt.scatter(x,y,s=1,color='g')

32
33  #———————————————————————————————————————————labels
34  plt.text(61,34,'(x1,y1)')
35  plt.text(16,60,'(x2,y2)')
36  plt.scatter(xc,yc,s=10,color='k')
37  plt.text(xc+4,yc-4,'(xc,yc)',color='k')
38
39  plt.show()
Listing 2-3Program PARC

(以下是创建图形 2-5 的程序)

1   """
2   PARCGEOMETRY
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   plt.axis([-10,140,90,-10])
9
10  plt.axis('off')
11  plt.grid(False)
12
13  #—————————————————————————————————————coordinate axes
14  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
15  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
16
17  #———————————————————————————————————————————labels
18  plt.text(16,-3,'x')
19  plt.text(-5,17,'y')
20
21  #———————————————————————————————————————main arc
22  xc=20
23  yc=20
24  r=40
25  plt.scatter(xc,yc,color='b',s=5)
26
27  phi1=20*np.pi/180.
28  phi2=70*np.pi/180.
29  dphi=(phi2-phi1)/20.
30  for phi in np.arange(phi1,phi2,dphi):
31       x=xc+r*np.cos(phi)
32       y=yc+r*np.sin(phi)
33       plt.scatter(x,y,s=2,color='g')
34
35  plt.plot([xc,xc+r*np.cos(phi1)],[yc,yc+r*np.sin(phi1)],color='k')
36
37  x1=xc+(r+3)*np.cos(phi1)
38  x2=xc+(r+10)*np.cos(phi1)
39  y1=yc+(r+3)*np.sin(phi1)
40  y2=yc+(r+10)*np.sin(phi1)
41  plt.plot([x1,x2],[y1,y2],color='k')
42
43  x1=xc+(r+3)*np.cos(phi2)
44  x2=xc+(r+30)*np.cos(phi2)
45  y1=yc+(r+3)*np.sin(phi2)
46  y2=yc+(r+30)*np.sin(phi2)
47  plt.plot([x1,x2],[y1,y2],color='k')
48
49  plt.plot([xc,xc+r*np.cos(phi2)],[yc,yc+r*np.sin(phi2)],color='k')
50
51  phihalf=(phi1+phi2)*.5
52  phi3=phihalf-dphi/2
53  phi4=phihalf+dphi/2
54
55  plt.plot([xc,xc+r*np.cos(phi3)],[yc,yc+r*np.sin(phi3)],color='k')
56  plt.plot([xc,xc+r*np.cos(phi4)],[yc,yc+r*np.sin(phi4)],color='k')
57
58  x1=xc+(r+3)*np.cos(phi3)
59  x2=xc+(r+15)*np.cos(phi3)
60  y1=yc+(r+3)*np.sin(phi3)
61  y2=yc+(r+15)*np.sin(phi3)
62  plt.plot([x1,x2],[y1,y2],color='k')
63
64  x1=xc+(r+3)*np.cos(phi4)
65  x2=xc+(r+15)*np.cos(phi4)
66  y1=yc+(r+3)*np.sin(phi4)
67  y2=yc+(r+15)*np.sin(phi4)
68  plt.plot([x1,x2],[y1,y2],color='k')
69
70  #———————————————————————————————————————————P1 arc
71  dphi=(phi3)/100.
72  for phi in np.arange(0,phi1/2-3.2*np.pi/180,dphi):
73       x=xc+(r+5)*np.cos(phi)
74       y=yc+(r+5)*np.sin(phi)
75       plt.scatter(x,y,s=.1,color='k')
76
77  for phi in np.arange(phi1/2+3.3*np.pi/180,phi1,dphi):
78       x=xc+(r+5)*np.cos(phi)
79       y=yc+(r+5)*np.sin(phi)
80       plt.scatter(x,y,s=.1,color='k')
81
82  #————————————————————————————————————————————P2 arc
83  dphi=(phi3)/100.
84  for phi in np.arange(0,phi2/2-3.2*np.pi/180,dphi):
85       x=xc+(r+25)*np.cos(phi)
86       y=yc+(r+25)*np.sin(phi)
87       plt.scatter(x,y,s=.1,color='k')
88
89  dphi=(phi3)/100.
90  for phi in np.arange(phi2/2+3.2*np.pi/180,phi2,dphi):
91       x=xc+(r+25)*np.cos(phi)
92       y=yc+(r+25)*np.sin(phi)
93       plt.scatter(x,y,s=.1,color='k')
94
95  #————————————————————————————————————————————P arc
96  dphi=(phi3)/100.
97  for phi in np.arange(0,phi3/2-.5*np.pi/180,dphi):
98       x=xc+(r+13)*np.cos(phi)
99       y=yc+(r+13)*np.sin(phi)
100      plt.scatter(x,y,s=.1,color='k')
101
102 dphi=(phi3)/100.
103 for phi in np.arange(phi3/2+9.*np.pi/180,phi3,dphi):
104      x=xc+(r+13)*np.cos(phi)
105      y=yc+(r+13)*np.sin(phi)
106      plt.scatter(x,y,s=.1,color='k')
107
108 #————————————————————————————————————————dp arc
109 dphi=(phi3)/100.
110 for phi in np.arange(phi3+5*dphi,phi3+25*dphi,dphi):
111      x=xc+(r+13)*np.cos(phi)
112      y=yc+(r+13)*np.sin(phi)
113      plt.scatter(x,y,s=.1,color='k')
114
115 plt.plot([xc,100],[yc,yc],'k')
116 plt.plot([xc,xc],[yc,80],'k')
117
118 #————————————————————————————————————————labels
119 plt.text(71,58,'p2',size='small')
120 plt.text(66,44,'p',size='small')
121 plt.text(63,29,'p1',size='small')
122 plt.text(45,66,'dp',size='small')
123 plt.text(41,26,'r')
124 plt.text(3,17,'(xc,yc)',size='small')
125 plt.plot([xc+r*np.cos(phi3),xc+r*np.cos(phi3)],[yc-8,yc+r*np.sin(phi3)],'k:')
126 plt.plot([xc,xc],[yc-2,yc-8],'k:')
127 plt.text(25,17,'R*cos(p)',size='small')
128
129 plt.plot([xc-8,xc+r*np.cos(phi3)],[yc+r*np.sin(phi3),yc+r*np.sin(phi3)],'k:')
130 plt.plot([xc-2,xc-8],[yc,yc],'k:')
131 plt.text(13,27,'R*sin(p)',size='small',rotation=90)
132
133 plt.text(49,30,'(x1,y1)',size='small')
134 plt.text(20,62,'(x2,y2)',size='small')
135 plt.text(51,49,'(xp,yp)',size='small')
136
137 #——————————————————————————————————————————arrow heads
138 plt.arrow(47,79,-2,1,head_length=3,head_width=2,color='k')
139 plt.arrow(62,53,-2,2,head_length=2.9,head_width=2,color='k')
140 plt.arrow(64,31,-.9,3,head_length=2,head_width=2,color='k')
141 plt.arrow(52,63,3,-3,head_length=2,head_width=2,color='k')
142
143 plt.show()
Listing 2-4Program PARCGEOMETRY

2.4 线段圆弧

您可以使用点之间的直线段来创建更精细的弧,而不是使用 np.scatter()在沿弧的点上绘制点。如果将清单 2-3 中从第 24 行开始的“绘制圆弧”例程替换为

24  #—————————————————————————————————plot arc
25  p1=20*np.pi/180
26  p2=70*np.pi/180
27  dp=(p2-p1)/100
28  xlast=xc+r*np.cos(p1)

29  ylast=yc+r*np.sin(p1)

30  for p in np.arange(p1+dp,p2,dp):

31        x=xc+r*np.cos(p)

32        y=yc+r*np.sin(p)

33        plt.plot([xlast,x],[ylast,y],color='g')

34        xlast=x

35        ylast=y

你会得到如图 2-7 所示的圆弧。在上面代码的第 28 和 29 行中,您定义了 xlast 和 ylast。这些是绘制在上一个线段末端的最后 x 和 y 坐标值。因为在循环开始之前,您刚刚开始绘制圆弧,所以它们最初被设置为等于 p=p1 处的圆弧起点。您将需要他们绘制第 33 行中的第一个弧段。参数 p、p1、p2 和 dp 与之前相同。想象循环 30-35 刚刚开始运行。第 31 行和第 32 行计算第一个线段末端的全局坐标,该线段 dp 到圆弧中。使用先前设置的值 xlast 和 ylast,它们是 28 和 29 中该线段起点的坐标,第 33 行绘制了第一条线段。第 34 行和第 35 行将第一段的终点坐标更新为 xlast,ylast。这些将被用作第二条线段的起始坐标。循环继续到弧的终点,使用前一段的终点作为下一段的起点。请注意,在第 30 行中,循环从 p1+dp 开始,这是第一条线段的结束角度。这实际上没有必要,循环的起点可以像以前一样设置为 p1,在这种情况下,第一条线段的长度为零。该循环将像以前一样继续到弧的末端。

A456962_1_En_2_Fig7_HTML.jpg

图 2-7

Circular arc created with plt.plot() line segments

在以后的工作中,您有时会使用由点而不是线段构成的曲线。尽管点不会产生很好的结果,但它们避免了复杂的绘图算法,这种算法有时会模糊脚本的逻辑。但是,线段确实会产生较好的结果,所以您也将使用它们。

2.5 圈

一整圈不过是 360 弧。通过将上一部分中圆弧的起点和终点角度更改为 p1=0 度和 p2=360 度,可以形成一个完整的圆。这是在清单 2-5 的第 24 和 25 行中完成的。输出如图 2-8 所示。在不同的位置绘制了三个圆和一个实心圆盘。它们有不同的颜色和宽度。绿色圆圈的一半用实线绘制,另一半用虚线 29-37 绘制。绘制实线或虚线的决定由第 32 行和第 35 行之间的 if 逻辑做出。这将更改第 33 行的 linestyle 属性。蓝色实心圆盘是通过绘制半径从 r1=0 到圆盘外径 r2 的同心圆制成的。当然,您也可以使用 np.scatter()函数制作一个实心磁盘。通过查看清单 2-5 中的脚本,您应该能够理解这里使用的逻辑来创建各种圆。

这个程序本可以通过使用函数来缩短。为了清楚起见,通过使用剪切和粘贴来复制冗余代码的部分,使其保持开放。

A456962_1_En_2_Fig8_HTML.jpg

图 2-8

Circles created by Listing 2-5

1   """
2   CIRCLES
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   plt.axis([-75,75,50,-50])
9
10  plt.axis('on')
11  plt.grid(True)
12
13  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
14  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
15
16  plt.text(16,-3,'x')
17  plt.text(-5,17,'y')
18
19  #——————————————————————————————————–green circle
20  xc=0
21  yc=0
22  r=40
23
24  p1=0*np.pi/180

25  p2=360*np.pi/180

26  dp=(p2-p1)/100
27  xlast=xc+r*np.cos(p1)
28  ylast=yc+r*np.sin(p1)
29  for p in np.arange(p1,p2+dp,dp):
30        x=xc+r*np.cos(p)
31        y=yc+r*np.sin(p)
32        if p > 90*np.pi/180 and p < 270*np.pi/180:

33             plt.plot([xlast,x],[ylast,y],color='g',linestyle=':')

34        else:

35             plt.plot([xlast,x],[ylast,y],color='g')

36        xlast=x
37        ylast=y
38
39  plt.scatter(xc,yc,s=15,color='g')
40
41  #————————————————————————————————————————red circle
42  xc=-20
43  yc=-20
44  r=10
45
46  p1=0*np.pi/180
47  p2=360*np.pi/180
48  dp=(p2-p1)/100
49  xlast=xc+r*np.cos(p1)
50  ylast=yc+r*np.sin(p1)
51  for p in np.arange(p1,p2+dp,dp):
52        x=xc+r*np.cos(p)
53        y=yc+r*np.sin(p)
54        plt.plot([xlast,x],[ylast,y],linewidth=4,color='r')
55        xlast=x
56        ylast=y
57
58  plt.scatter(xc,yc,s=15,color='r')
59
60  #—————————————————————————————————————————purple circle
61  xc=20
62  yc=20
63  r=50
64
65  p1=0*np.pi/180
66  p2=360*np.pi/180
67  dp=(p2-p1)/100
68  xlast=xc+r*np.cos(p1)
69  ylast=yc+r*np.sin(p1)
70  for p in np.arange(p1,p2+dp,dp):
71        x=xc+r*np.cos(p)
72        y=yc+r*np.sin(p)
73        plt.plot([xlast,x],[ylast,y],linewidth=2,color=(.8,0,.8))
74        xlast=x
75        ylast=y
76
77  plt.scatter(xc,yc,color=(.5,0,.5))
78
79  #———————————————————————————————————————————blue disc
80  xc=-53
81  yc=-30
82  r1=0
83  r2=10
84  dr=1
85
86  p1=0*np.pi/180
87  p2=360*np.pi/180
88  dp=(p2-p1)/100
89  xlast=xc+r1*np.cos(p1)
90  ylast=yc+r1*np.sin(p1)
91  for r in np.arange(r1,r2,dr):
92        for p in np.arange(p1,p2+dp,dp):
93              x=xc+r*np.cos(p)
94              y=yc+r*np.sin(p)
95              plt.plot([xlast,x],[ylast,y],linewidth=2,color=(0,0,.8))
96              xlast=x
97              ylast=y
98
99  plt.show()
Listing 2-5Program CIRCLES

2.6 点光盘

图 2-9 中显示了用不同点模式创建的两个圆盘。标记为“r,p”的圆盘是通过在传统的极坐标 r,p 阵列中放置点来绘制的,其中 r 是距中心的半径,p 是角度。该算法从清单 2-6 中的第 21 行开始。清单 2-6 中的脚本应该是不言自明的。这个图的唯一问题是,点不是均匀分布的,而是随着半径的增加而进一步分开。在某些情况下,这可能是不希望的。

A456962_1_En_2_Fig9_HTML.jpg

图 2-9

Discs created by different dot patterns in Listing 2-6 where “r,p” contains simple polar coordinates and “equal arc” has modified polar coordinates

从第 38 行开始的“等弧”圆盘在视觉上看起来更好。与“r,p”光盘一样,这些点在径向上等距分布。然而,在“等弧”盘中,在每个径向位置的圆周方向上的点的数量随着半径的增加而变大,从而保持点之间的圆周弧间距恒定。使用的模型如图 2-10 所示。dc 是 rmax 处点 a 和 b 之间的圆周间距,rmax 是磁盘的外边缘。dp 是半径 a 和 b 之间的角间距,为了在圆盘上获得更均匀的间距,在所有半径上保持 dc 不变。典型的径向位置显示为 r=rmax/2。该半径处的 dc 与 rmax 处的相同,等于 dc。为了适应这种间距,相邻点之间的角度必须增加到 drp。

在清单 2-6 的第 44 行,圆盘的外径被设置为 20。第 45 行的径向间距设置为 2。请记住,圆弧上两点之间的圆周间距是 r×dp,其中 r 是半径,dp 是两点之间的角度,第 46 行计算 dc,其中您已将 rmax 处的点数任意设置为每π弧度 40(整个圆周 80)。从线 48 开始的循环从 r=dr 开始,并在径向上以步长 dr 前进到 rmax。在每个值或 r 处,在线 49 中计算保持圆周间距等于 dc 所需的点之间的角度 dpr。然后,从第 50 行开始的循环沿圆周方向放置这些点。

A456962_1_En_2_Fig10_HTML.jpg

图 2-10

Model for “equal arc” disc used by Listing 2-6

1   """
2   DOTDISCS
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   import random as rnd
8
9   plt.axis([0,150,100,0])
10
11  plt.axis('off')
12  plt.grid(False)
13
14  plt.arrow(0,0,20,0,head_length=4,head_width=3,color='k')
15  plt.arrow(0,0,0,20,head_length=4,head_width=3,color='k')
16
17  plt.text(16,-3,'x')
18  plt.text(-5,17,'y')
19
20  #—————————————————————————————————————————simple r,p dot pattern
21  xc=40
22  yc=25
23
24  p1=0
25  p2=2*np.pi
26  dp=np.pi/20
27
28  rmax=20
29  dr=2
30
31  for r in np.arange(dr,rmax,dr):
32        for p in np.arange(p1,p2,dp):
33              x=xc+r*np.cos(p)
34              y=yc+r*np.sin(p)
35              plt.scatter(x,y,s=2,color='k')
36
37  #—————————————————————————————————————————equal arc length dot pattern
38  xc=40
39  yc=70
40
41  p1=0
42  p2=2*np.pi
43
44  rmax=20
45  dr=2
46  dc=np.pi*rmax/40

47
48  for r in np.arange(dr,rmax,dr):

49       dpr=dc/r

50       for p in np.arange(p1,p2,dpr):

51            x=xc+r*np.cos(p)

52            y=yc+r*np.sin(p)

53            plt.scatter(x,y,s=2,color='k')

54
55  #————————————————————————————————————————————————————labels
56  plt.text(38,66,'r,p')
57  plt.text(95,66,'equal arc')
58
59  plt.show()
Listing 2-6Program DOTDISCS

2.7 椭圆

椭圆如图 2-12 所示。他们是通过列举 2-7 得出的。清单 2-7 使用的型号如图 2-11 所示。这是由清单 2-8 绘制的。尺寸 a 称为半长,因为它指的是较大宽度的一半;b 是半小调。2a 和 2b 是主要尺寸和次要尺寸。

我们都很熟悉的椭圆的方程是,

$$ \frac{x²}{a²}+\frac{y²}{b²}=1 $$

(2-7)

在 a=b=r 的特殊情况下,这退化为一个圆,如在

$$ {x}²+{y}²={r}² $$

(2-8)中,其中 r 是半径。

绘制椭圆时可以使用的一种策略是从 x=-a 开始,使用等式 2-7 在+x 方向前进,计算每个 x 处的 y,然后像过去一样,从最后一步绘制一个点或一条线段。y 坐标很容易从方程 2-7 导出为

$$ y=b\sqrt{1-\frac{x²}{a²}} $$

(2-9)

这似乎很容易。图 2-12 中的绿色椭圆就是这样画出来的。但是,有一个问题。看清单 2-7 ,第 48、49、50 行;当 x 接*+a 时,等式 2-9 和第 48 行中的*方根给出不确定的结果,第 48 行试图取一个非常接*零的数的*方根。这是由 Python 计算中的舍入误差造成的。这种现象表现为椭圆+a 侧的间隙。在绿色椭圆的算法中,该间隙由线 54 和 55 封闭。这样你可以得到一个像样的椭圆,但你必须小心。

另一种方法是使用极坐标,如图 2-11 所示。您希望将椭圆上某点的坐标(xp,yp)确定为角度 p 的函数。通过改变 p,您将获得绘制椭圆所需的信息。要确定(xp,yp)与 p 的关系,请注意它位于椭圆和径向线的交点上。这一点用红点表示。顺便说一句,正如可以看到的,该点似乎并不正好位于交叉点上。这是因为用于调整清单 2-8 第 8 行 x 轴值的比例因子有点偏离。您使用尺子进行粗略测量,然后对计算结果进行四舍五入以确定比例因子。由此产生的微小误差在这里显示出来。直线的方程可以从下面确定:

$$ xp=r; cos(p) $$

(2-10)

$$ yp=r; sin(p) $$

(2-11)

综合以上,

$$ \frac{yp}{xp}=\frac{r; sin(p)}{r; cos(p)}= tan(p) $$

(2-12)

$$ yp=xp; tan(p) $$

(2-13)

你知道(xp,yp)位于直线和椭圆的交点。这就是 xp 和 yp 满足直线和椭圆方程的地方。将方程 2-13 代入方程 2-7 、

$$ \frac{x{p}²}{a²}+\frac{x{p}²ta{n}²p}{b²}=1 $$

(2-14)即可确定该点的坐标,得出

$$ xp= ab{\left[{b}²+{a}²ta{n}²(p)\right]}^{-\frac{1}{2}} $$

、【2-15】、$$ yp= ab{\left[{a}²+{b}²\frac{1}{ta{n}²(p)}\right]}^{-\frac{1}{2}} $$、、【2-16】

A456962_1_En_2_Fig11_HTML.jpg

图 2-11

Model created by Listing 2-8 and used by Listing 2-7

等式 2-15 和 2-16 在清单 2-7 中实现,以绘制第 20 行和第 36 行之间的红色椭圆、第 39 行和第 55 行之间的绿色椭圆以及第 58 行和第 69 行中的蓝色椭圆。输出如图 2-12 所示。当绘制绿色椭圆时,程序从-a 循环到+a,并使用等式 2-9 计算 y 值。如前所述,这可能会在 x=+a 处的椭圆端点附*导致舍入误差,从而在椭圆中留下一个间隙。这在第 54 行和第 55 行得到了纠正,它们画了一些短线来缩小这个间隙。请注意,蓝色椭圆是填充的。这是通过线 69 完成的,它画出了从椭圆顶部到底部的垂直线。

A456962_1_En_2_Fig12_HTML.jpg

图 2-12

Ellipses created by Listing 2-7

1   """
2   ELLIPSES
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   plt.axis([-75,75,50,-50])
9
10  plt.axis('on')
11  plt.grid(True)
12
13  plt.arrow(0,0,60,0,head_length=4,head_width=3,color='k')
14  plt.arrow(0,0,0,45,head_length=4,head_width=3,color='k')
15
16  plt.text(58,-3,'x')
17  plt.text(-5,44,'y')
18
19  #————————————————————————————————————————red ellipse
20  a=40
21  b=20.
22  p1=0
23  p2=180*np.pi/180
24  dp=.2*np.pi/180
25
26  xplast=a
27  yplast=0
28  for p in np.arange(p1,p2,dp):
29          xp=np.abs(a*b*(b*b+a*a*(np.tan(p))**2.)**-.5)
30          yp=np.abs(a*b*(a*a+b*b/(np.tan(p)**2.))**-.5)
31          if p > np.pi/2:
32               xp=-xp
33          plt.plot([xplast,xp],[yplast,yp],color='r')
34          plt.plot([xplast,xp],[-yplast,-yp],color='r')
35          xplast=xp
36          yplast=yp
37
38  #————————————————————————————————————————green ellipse
39  a=20.
40  b=40.
41  xp1=-a
42  xp2=a
43  dx=.1
44
45  xplast=-a
46  yplast=0
47  for xp in np.arange(xp1,xp2,dx):
48       yp=b*(1-xp**2./a**2.)**.5

49       plt.plot([xplast,xp],[yplast,yp],linewidth=1,color='g')

50       plt.plot([xplast,xp],[-yplast,-yp],linewidth=1,color='g')

51       xplast=xp
52       yplast=yp
53
54  plt.plot([xplast,a],[yplast,0],linewidth=1,color='g'

55  plt.plot([xplast,a],[-yplast,0],linewidth=1,color='g'

56
57  #—————————————————————————————————————blue ellipse
58  a=5.
59  b=15.
60  p1=0
61  p2=180*np.pi/180
62  dp=.2*np.pi/180
63
64  for p in np.arange(p1,p2,dp):
65        xp=np.abs(a*b*(b*b+a*a*(np.tan(p))**2.)**-.5)
66        yp=np.abs(a*b*(a*a+b*b/(np.tan(p)**2.))**-.5)
67        if p > np.pi/2:
68             xp=-xp
69        plt.plot([xp,xp],[yp,-yp],linewidth=1,color='b')

70
71  plt.show()
Listing 2-7
Program

ELLIPSES

(以下程序用于创建图 2-11 。)

1   """
2   ELLIPSEMODEL
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   plt.axis([-75,75,50,-50])
9
10  plt.axis('on')
11  plt.grid(True)
12
13  plt.arrow(0,0,60,0,head_length=4,head_width=3,color='k')
14  plt.arrow(0,0,0,40,head_length=4,head_width=3,color='k')
15
16  plt.text(58,-3,'x')
17  plt.text(-5,40,'y')
18
19  #——————————————————————————————————————ellipse
20  a=50.
21  b=30.
22  p1=0.
23  p2=180.*np.pi/180.
24  dp=(p2-p1)/180.
25
26  xplast=a
27  yplast=0
28  for p in np.arange(p1,p2+dp,dp):
29        xp=np.abs(a*b*(b*b+a*a*(np.tan(p))**2.)**-.5)
30        yp=np.abs(a*b*(a*a+b*b/(np.tan(p)**2.))**-.5)
31        if p > np.pi/2:
32             xp=-xp
33        plt.plot([xplast,xp],[yplast,yp],color='k')
34        plt.plot([xplast,xp],[-yplast,-yp],color='k')
35        xplast=xp
36        yplast=yp
37
38  #———————————————————————————————————————————line
39  plt.plot([0,40],[0,40],color='k')
40
41  #———————————————————————————————————————————point
42  p=45.*np.pi/180.
43  xp=np.abs(a*b*(b*b+a*a*(np.tan(p))**2.)**-.5)
44  yp=np.abs(a*b*(a*a+b*b/(np.tan(p)**2.))**-.5)
45  plt.scatter(xp,yp,s=20,color='r')
46
47  #—————————————————————————————————————————labels
48  plt.text(23,-3,'a',color='k')
49  plt.text(-5,15,'b',color='k')
50  plt.text(32,28,'(xp,yp)')
51  plt.text(30,12,'p')
52  plt.text(10,18,'r')
53
54  #——————————————————————————————————————————p arc
55  p1=0
56  p2=45*np.pi/180
57  dp=(p2-p1)/180
58  r=30
59  for p in np.arange(p1,p2,dp):
60        x=r*np.cos(p)
61        y=r*np.sin(p)
62        plt.scatter(x,y,s=.1,color='r')
63
64  plt.arrow(25,17.5,-1,1,head_length=3,head_width=2,color='r')
65
66  plt.show()
Listing 2-8Program ELLIPSEMODEL

2.8 2D 译本

在二维空间中,物体具有三个独立的自由度:它可以绕一个垂直于*面的轴方向旋转,并且可以在*面内的两个方向(x 和 y)*移。纯*移意味着物体移动而不旋转;纯旋转意味着物体旋转而不*移。图 2-13 中的对象是纯翻译的例子。三角形(黑色)在没有旋转的情况下向右(绿色)*移(移动),然后向下(红色)。用 Python 来完成这件事很简单,尤其是在使用清单 2-9 中的列表时。例如,要将一个对象向右移动 dx 的量,只需将 dx 加到 x 坐标上,然后重新绘制它。类似地,对于 y 方向,只需将 dy 添加到 y 坐标,然后重新绘制。在从第 45 行开始的循环中,通过将 x 坐标增加 10 个单位,将蓝色小框*移穿过绘图区域。

A456962_1_En_2_Fig13_HTML.jpg

图 2-13

Examples of translation created by Listing 2-9

1   """
2   2DTRANSLATION
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7
8   x1=-10
9   x2=140
10  y1=90
11  y2=-10
12  plt.axis([x1,x2,y1,y2])
13
14  plt.axis('on')
15  plt.grid(True)
16
17  plt.title('Translation')
18
19  #—————————————————————————————————————————————————triangle
20  x=[20,30,40,20]
21  y=[40,20,40,40]
22  plt.plot(x,y,color='k')
23  plt.plot(x,y,color='k')
24  plt.plot(x,y,color='k')
25
26  #——————————————————————————————————————translate triangle dx=60
27  x=[60,70,80,60]
28  plt.plot(x,y,color='g')
29  plt.plot(x,y,color='g')
30  plt.plot(x,y,color='g')
31
32  #——————————————————————————————————————translate triangle dy=40
33  y=[80,60,80,80]
34  plt.plot(x,y,color='r')
35  plt.plot(x,y,color='r')
36  plt.plot(x,y,color='r')
37
38  #——————————————————————————————————————————————————————box
39  x=[0,0,5,5,0]
40  y=[55,50,50,55,55]
41  plt.plot(x,y,'b')
42
43  #————————————————————————————————————————————translate box
44  y=[55,50,50,55,55]
45  for x in np.arange(0,130,10):

46       x=[x,x,x+5,x+5,x]

47       plt.plot(x,y,'b')

48
49  plt.show()
Listing 2-9Program 2DTRANSLATION

2.9 2D 旋转

到目前为止,在这一章中,你已经看到了如何使用点和线在二维*面上构建图像。在本节中,您将学习如何在二维*面对象自身的*面内旋转该对象。一个你可能想要旋转的 2D 对象,比如一个矩形,或者更复杂的东西,通常由任意数量的点和线组成。当然,线是由它们的端点或一系列点定义的,如果这些点是由点构成的话。如您所见,曲线也可以由线段或点构成。如果您可以确定如何旋转一个点,那么您将能够旋转任何由点定义的*面对象。在第三章,你将把这些概念推广到三维物体绕三个坐标方向的旋转。

图 2-14 显示了三个坐标系:蓝色的 xg,yg 系是全局坐标系。它的数值大小和全局原点的位置(xg=0,yg=0)由 plt.axis([x1,x2,y1,y2])语句中的值定义。这是您在绘图时使用的系统。所有绘图坐标都应与该系统相关。比如写 plt.scatter(xg,yg),xg 和 yg 应该是相对于蓝色的 xg,yg 系统如图所示。

A456962_1_En_2_Fig14_HTML.jpg

图 2-14

2D rotation model

黑色的 x,y 系统是本地系统。局部系统中的一个位置(xp,yp)等价于全局系统中的(xc+xp,yc+yp)。您可以使用本地系统,通过指定组成形状的点的坐标来构造形状。例如,如果要在绘图区域的某个地方绘制一个圆,可以将(xc,yc)放置在圆的中心,参照局部(黑色)系统计算定义圆的点,然后通过将每个点*移 xc 和 yc,将这些点关联回 xg,yg(蓝色)系统进行绘图。

图 2-14 显示了一个点 P,该点通过顺时针角度 Rz 旋转到 P’处的一个新位置。红色坐标系旋转角度 Rz。p 随之旋转。P′在旋转系统(xp,yp)中的坐标与它们在本地系统中的坐标相同。但是,在全球体系中,它们显然是不同的。你现在的目标是确定 P '在局部系统中的坐标,然后在全局系统中,这样你就可以绘制它了。

我使用术语 Rz 来表示角度,因为在 x,y *面中的顺时针旋转实际上是围绕 z 方向的旋转,z 方向指向纸面。第一章对此进行了说明。这将在第三章中详细解释。

图 2-14 显示点 P 处于未旋转位置。它相对于局部 x,y 系统(黑色)的坐标是(xp,yp)。它的位置由向量 P,

$$ \mathbf{P}=xp\widehat{\mathbf{i}}+yp\widehat{\mathbf{j}} $$

(2-17)定义其中 Iˇ和 jˇ是 x 和 y 方向的单位向量。

在 P 旋转通过角度 Rz 之后,它到达相对于 x,y(黑色)系统的坐标(x′,y′)处的新位置 P′(红色)。P′由向量 P′(红色)定义为,

$$ {\mathbf{P}}{\mathbf{\prime}}=x{p}{\mathit{\prime}}\widehat{\mathbf{i}}+y{p}^{\mathit{\prime}}\widehat{\mathbf{j}} $$

(2-18)

P’相对于旋转的 x’,y’系统的坐标是(xp,yp)。因此,P’的位置也由向量

$$ {\mathbf{P}}{\mathbf{\prime}}=xp{\widehat{\mathbf{i}}}{\mathbf{\prime}}+yp{\widehat{\mathbf{j}}}^{\mathbf{\prime}} $$

(2-19)定义,其中 I’和 j’是 x’和 y’方向上的单位向量。

你现在的任务是确定 I’和 j’相对于 I’和 j’的关系,然后将它们代入方程 2-19 。这将给出 P’相对于局部 x,y 系统的坐标。通过简单地将 xc 和 yc 相加,你将得到 P '在全球系统中的坐标,这是你绘图所需要的。

四个单位向量显示在(xc,yc)处。Iˇ和 jˇ指向 x 和 y 方向;I′和 j′指向 x′和 y′方向。通过查看图 2-14 ,可以看到

$$ {\widehat{\mathbf{i}}}^{\mathbf{\prime}}=\underset{X; component}{\underbrace{cos(Rz)}}\kern0.24em \widehat{\mathbf{i}}+\underset{Y; component}{\underbrace{sin(Rz)}}\kern0.24em \widehat{\mathbf{j}} $$

(2-20)

$$ {\widehat{\mathbf{j}}}^{\mathbf{\prime}}=\underset{X; component}{\underbrace{\mathit{\hbox{-}} sin(Rz)}}\kern0.24em \widehat{\mathbf{i}}+\underset{Y; component}{\underbrace{cos(Rz)}}\kern0.24em \widehat{\mathbf{j}} $$

(2-21)

将这些代入方程 2-19,你得到

$$ \mathbf{p}\mathbf{\hbox{'}}=xp\left[ cos(Rz)\widehat{\mathbf{i}}+ sin(Rz)\widehat{\mathbf{j}}\right]+yp\left[- sin(Rz)\widehat{\mathbf{i}}+ cos(Rz)\widehat{\mathbf{j}}\right] $$

(2-22)

这可以分为 x 和 y 分量,

$$ {\mathbf{P}}{\mathbf{\prime}}=x{p}{\mathit{\prime}}\widehat{\mathbf{i}}+y{p}^{\mathit{\prime}}\widehat{\mathbf{j}} $$

(2-23)其中

$$ x{p}^{\mathit{\prime}}=xp\left[ cos(Rz)\right]+yp\left[- sin(Rz)\right] $$

(2-24)

$$ y{p}^{\mathit{\prime}}=xp\left[ sin(Rz)\right]+yp\left[ cos(Rz)\right] $$

(2-25)

这最后两个方程是你将一个点从(xp,yp)通过角度 Rz 旋转到新坐标(XP′,yp′)所需要的全部。注意,两组坐标(xp,yp)和(XP′,yp′),都是相对于局部 x,y 轴的。然后,xc 和 yc 可以轻松地转换它们,以便在全球系统中进行绘图。

在 yp=0 的特殊情况下,即当 P 在旋转前位于 x 轴 x=xp 处时,方程 2-24 和 2-25 退化为

$$ x{p}^{\mathit{\prime}}=xp; cos(Rz) $$

(2-26)

$$ y{p}^{\mathit{\prime}}=xp; sin(Rz) $$

(2-27),这可以从图 2-14 中很容易地得到验证。当然,你关心的是旋转一个一般的点,它最初在 x,y *面的任何地方,所以你需要方程 2-24 和 2-25 中包含的完整公式。这些可以用矩阵形式表示为

$$ \left[\begin{array}{c}\hfill x{p}^{\mathit{\prime}}\hfill \ {}\hfill y{p}^{\mathit{\prime}}\hfill \end{array}\right]=\left[\begin{array}{cc}\hfill cos(Rz)\hfill & \hfill \mathit{\hbox{-}} sin(Rz)\hfill \ {}\hfill sin(Rz)\hfill & \hfill cos(Rz)\hfill \end{array}\right]\left[\begin{array}{c}\hfill xp\hfill \ {}\hfill yp\hfill \end{array}\right] $$

(2-28),可以缩写为

$$ \left[{P}^{\mathit{\prime}}\right]=\left[Rz\right]\left[P\right] $$

(2-29)

[P′]和[P]矩阵通常被称为列向量,因为它们包含向量 P 和 P′的分量。[Rz]是变换矩阵;它将 P 矢量转换成 P’矢量,在这种情况下是通过旋转角度 Rz。这些向量如图 2-15 所示,其中 P 定义了未旋转点 P1(黑色)和旋转点 P′(红色)在 P3 的位置。可以将[Rz]改写为

$$ \left[Rz\right]=\left[\begin{array}{cc}\hfill C\left(1,1\right)\hfill & \hfill C\left(1,2\right)\hfill \ {}\hfill C\left(2,1\right)\hfill & \hfill C\left(2,2\right)\hfill \end{array}\right] $$

(2-30)

$$ C\left(1,1\right)= cos(Rz) $$

(2-31)

$$ C\left(1,2\right)=- sin(Rz) $$

(2-32)

$$ C\left(2,1\right)= sin(Rz) $$

(2-33)

$$ C\left(2,2\right)= cos(Rz) $$

(2-34)

等式 2-31 到 2-34 中的定义将在随后的 Python 程序中使用。它们代表在 x,y *面中顺时针方向的旋转;使用负值的 Rz 以逆时针方向旋转。注意,[Rz]纯粹是旋转角度 Rz 的函数。

要将 XP′和 yp′转换成 xg 和 yg,只需将 xc 加到 XP′上,将 yc 加到 yp′上,如

$$ xg=xc+x{p}^{\mathit{\prime}} $$

(2-35)

$$ yg=yc+y{p}^{\mathit{\prime}} $$

(2-36)

在矩阵形式中,

$$ \left[\begin{array}{c}\hfill xg\hfill \ {}\hfill yg\hfill \end{array}\right]=\left[\begin{array}{c}\hfill xc\hfill \ {}\hfill yc\hfill \end{array}\right]+\left[\begin{array}{cc}\hfill cos(Rz)\hfill & \hfill \mathit{\hbox{-}} sin(Rz)\hfill \ {}\hfill sin(Rz)\hfill & \hfill cos(Rz)\hfill \end{array}\right]\left[\begin{array}{c}\hfill xp\hfill \ {}\hfill yp\hfill \end{array}\right] $$

(2-37),可缩写为

$$ \underset{global}{\underbrace{\left[ Pg\right]}}=\underset{center}{\underbrace{\left[C\right]}}+\left[Rz\right]\underset{local}{\underbrace{\left[P\right]}} $$

(2-38)或在向量形式中,如图 2-15 、

$$ \mathbf{P}g={\mathbf{C}}+{\mathbf{P}}^{\mathbf{\prime}} $$

(2-39)

A456962_1_En_2_Fig15_HTML.jpg

图 2-15

Rotation of a point P1 from Rz=0° (black) to Rz=30° (green), 60° (red), and 90° (grey). Vectors drawn from xg=0, yg=0 to Point 3 at Rz=60° illustrating Equation 2-38. Plotted by Listing 2-10.

作为上述概念的一个例子,列表 2-10 从(xp,yp)=(60,0)的原始未旋转位置以 30 度的增量旋转一个点 P1 大约(xc,yc)。结果如图 2-15 所示。旋转中心的坐标(xc,yc)设置在第 16 行和第 17 行。

清单 2-10 的第 28-37 行定义了一个函数 rotz(xp1,yp1,Rz),它使用方程 2-31 到 2-34 中的变换矩阵【Rz】的元素以及旋转角度 Rz 来计算并返回变换(旋转和*移)后的坐标(xg,yg)。函数 rotz 中的第 35 行和第 36 行将本地坐标与 xg,yg 系统相关联,用于绘图。注意 rotz 旋转每个点,同时在第 35 行和第 36 行*移 xc 和 yc。这将使全球系统中的坐标为绘图做好准备。你旋转这个点四次:Rz=0,30,60,90。使用函数 rotz(xp,yp,Rz)可以避免对每个点的变换进行编码。

第 39 行和第 40 行将 P 的原始坐标设置为(60,0)。需要注意的是,这些坐标是相对于旋转中心(xc,yc)的。第 43 行开始计算第一个点。这是在 Rz=0 时。第 44 行将 Rz 从度转换为弧度。稍后,我将展示如何使用 radians()函数来做到这一点。第 45 行调用函数 rotz(xp,yp,Rz)。xp 和 yp 设置在第 39 和 40 行;Rz 设置在第 43 行。该函数返回第 37 行中旋转点的坐标(xg,yg)。因为 Rz 在第一次变换中为零,所以它们与未旋转点 P1 的坐标相同。

P2 点的绘制从第 50 行开始。您在第 50 行中将旋转角度设置为 30 度。程序和以前一样,P2 被标为灰点。部分 P3 和 P4 增加 Rz 到 60 和 90 度,绘制红色和最后的灰点。

第 74、77、80 和 83 行说明了 Latex 在打印文本时的使用。例如,看第 74 行,

plt.text(28,6,r'$\mathbf{C}$',color='k')

文本从坐标 xg=28,yg=6 开始。正如在第一章中所讨论的,r 告诉 Python 把字符串当作原始的。这使得 Latex 代码所需的反斜杠保留在美元符号之间;在本例中为\(\mathbf{C}\)。\mathbf{}将大括号{}之间的内容加粗。在第 80 行,^{\prime}在 p 旁边放了一个上标撇。如果不包括前缀 r,这将不起作用。

A456962_1_En_2_Fig16_HTML.jpg

图 2-16

Rotation of a rectangle around its center from Listing 2-11

1
2   """
3   2DROT1
4   """
5   import matplotlib.pyplot as plt
6   import numpy as np
7
8   plt.axis([-10,140,90,-10])
9   plt.axis('on')
10  plt.grid(True)
11
12  #————————————————————————–axes
13  plt.arrow(0,0,40,0,head_length=4,head_width=2,color='b')
14  plt.arrow(0,0,0,40,head_length=4,head_width=2,color='b')
15
16  xc=40

17  yc=10

18
19  plt.plot([xc-30,xc+90],[yc,yc],linewidth=1,color='k') #—-X

20  plt.plot([xc,xc],[yc-5,yc+75],linewidth=1,color='k') #—-Y

21
22  plt.text(30,-2,'Xg',color='b')
23  plt.text(-7,33,'Yg',color='b')
24  plt.scatter(xc,yc,s=20,color='k')
25  plt.text(xc+3,yc-2,'(xc,yc)')
26
27  #—————————————————–define rotation matrix rz
28  def rotz(xp,yp,rz): #——–xp,yp=un-rotated coordinates relative to xc,yc
29       c11=np.cos(rz)

30       c12=-np.sin(rz)

31       c21=np.sin(rz)

32       c22=np.cos(rz)

33       xpp=xp*c11+yp*c12 #—-xpp,ypp=rotated coordinates relative to xc,yc
34       ypp=xp*c21+yp*c22

35       xg=xc+xpp #—-xg,yg=rotated coordinates relative to xg,yg
36       yg=yc+ypp

37       return [xg,yg]

38
39  xp=60 #————————————-coordinates of first point P1 relative to xc,yc
40  yp=0

41
42  #——————————————————————————————P1
43  rz=0

44  rz=rz*np.pi/180

45  [xg,yg]=rotz(xp,yp,rz)

46  plt.scatter(xg,yg,s=30,color='k' )
47  plt.text(xg+1,yg+6,'P1',color='k')
48
49  #——————————————————————————————————P2
50  rz=30

51  rz=rz*np.pi/180
52  [xg,yg]=rotz(xp,yp,rz)
53  plt.scatter(xg,yg,s=30,color='grey')
54  plt.text(xg+1,yg+6,'P2',color='grey')
55
56  #——————————————————————————————————P3
57  rz=60
58  rz=rz*np.pi/180
59  [xg,yg]=rotz(xp,yp,rz)
60  plt.scatter(xg,yg,s=30,color='r')
61  plt.text(xg+1,yg+6,'P3',color='r')
62  xpp3=xg #——save for later in line 76
63  ypp3=yg
64
65  #——————————————————————————————————P4
66  rz=90
67  rz=rz*np.pi/180
68  [xg,yg]=rotz(xp1,yp1,rz)
69  plt.scatter(xg,yg,s=30,color='grey')
70  plt.text(xp2+1,yp2+6,'P4',color='grey')
71
72  #————————————————————————————————————————————————plot vectors
73  plt.arrow(0,0,xc-4,yc-1,head_length=4,head_width=2,color='k')
74  plt.text(28,6,r'$\mathbf{C}$',color='k')
75
76  plt.arrow(0,0,xpp3-3,ypp3-3,head_length=4,head_width=2,color='b')
77  plt.text(45,50,r'$\mathbf{Pg}$',color='b')
78
79  plt.arrow(xc,yc,xpp3-2-xc,ypp3-5-yc,head_length=4,head_width=2,color='r')
80  plt.text(61,40,r'$\mathbf{P^{\prime}}$',color='r')
81
82  plt.arrow(xc,yc,xp-4,yp,head_length=4,head_width=2,color='k')
83  plt.text(80,yc-2,r'$\mathbf{P}$',color='k')
84
85  plt.show()
Listing 2-10Program 2DROT1

接下来,你围绕它的中心旋转一个矩形,如图 2-16 所示。旋转中心是(xc,yc)处的点 c。黑色矩形显示的是未旋转方向的矩形。它的角编号为 1-4,如图所示。程序绘制未旋转的矩形,然后围绕 c 点将其旋转到旋转后的位置,并以红色显示。

由于矩形是由其角点定义的,您可以通过围绕 c 旋转角点来旋转矩形。方法详见清单 2-11 。首先,绘制未旋转的矩形(黑色)。它的四个角点的局部坐标是相对于第 42-49 行中的旋转中心 c 指定的。这些点被标记并绘制为第 51-58 行中的点,其中通过将第 55-58 行中的 xc 和 yc 相加将局部坐标转换为全局坐标。

接下来,用线将角连接起来。第 61-68 行通过 xc 和 yc 转换局部角坐标。这些点被标记为 xg 和 yg,以表示它们相对于全局标绘轴。它们在第 70 行和第 71 行中被设置为列表,然后在第 73 行中被绘制,这在连续的 xg、yg 对之间绘制了线。

注意第 70 行和第 71 行中坐标对的顺序。当调用第 73 行时,它将(xg1,yg1)连接到(xg2,yg2),然后(xg2,yg2)连接到(xg3,yg3),依此类推。但是当它到达角 4 时,它必须将角 4 连接回角 1,以便闭合矩形;因此,在 70 和 71 的末尾,( xg4,yg4)连接到(xg1,yg1)。

旋转矩形的绘制从第 76 行开始。Rz 是旋转角度。它在这里被设置为 45 度,然后在第 77 行从度转换为弧度(您可以使用 radians()函数来完成此操作)。

函数 rotz(xp,yp,Rz)在第 29-38 行中定义。等式 2-31 到 2-34 中所示的旋转变换矩阵的元素在第 30-33 行被求值。xp 和 yp 是未旋转点的坐标。使用等式 2-24 和 2-25 在第 34 和 35 行评估旋转系统中的坐标 xpp 和 ypp(XP’和 yp’)。xg 和 yg 是旋转和*移后在全球系统中的坐标,根据等式 2-35 和 2-36 在第 36-37 行中计算。注意,这些线旋转这些点,同时相对于点 c *移它们。转换后的坐标作为列表返回到第 38 行。

第 80-101 行通过调用函数 rotz(xp,yp,Rz)逐个转换每个角坐标。例如,行 80-83 将角 1 从局部的未旋转坐标 xp1,yp1 变换到全局坐标 xg 和 yg。剩下的三个点以同样的方式变换。使用列表在线 104-107 中用红色绘制连接角的线。

1   """
2   2DROTRECTANGLE
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7
8   plt.axis([-10,150,100,-10])
9   plt.axis('on')
10  plt.grid(True)
11
12  #————————————————————————————————————————————————————–axes
13  plt.arrow(0,0,40,0,head_length=4,head_width=2,color='b')
14  plt.arrow(0,0,0,40,head_length=4,head_width=2,color='b')
15  plt.text(30,-3,'Xg',color='b')
16  plt.text(-8,34,'Yg',color='b')
17
18  xc=75 #————————————————–center of rotation
19  yc=50
20  plt.plot([xc-40,xc+60],[yc,yc],linewidth=1,color='grey') #—-X
21  plt.plot([xc,xc],[yc-40,yc+45],linewidth=1,color='grey') #—-Y
22  plt.text(127,48,'X')
23  plt.text(70,90,'Y')
24
25  plt.scatter(xc,yc,s=20,color='k') #—plot center of rotation
26  plt.text(70,49,'c')
27
28  #———————————————————————————————————————————-define function rotz
29  def rotz(xp,yp,rz):

30      c11=np.cos(rz)

31      c12=-np.sin(rz)

32      c21=np.sin(rz)

33      c22=np.cos(rz)

34      xpp=xp*c11+yp*c12 #————-relative to xc,yc
35      ypp=xp*c21+yp*c22

36      xg=xc+xpp #—-relative to xg,yg
37      yg=yc+ypp

38      return [xg,yg]

39
40  #——————————————————————————————————————————–plot unrotated rectangle
41  #—————————————————–rectangle corner coordinates in X,Y system
42  xp1=-20

43  xp2=+20

44  xp3=+20

45  xp4=-20

46  yp1=-5

47  yp2=-5

48  yp3=+5

49  yp4=+5

50
51  plt.text(50,45,'1') #——————-label
52  plt.text(97,45,'2')

53  plt.text(97,57,'3')

54  plt.text(50,57,'4')

55  plt.scatter(xp1+xc,yp1+yc,s=10,color='k')

56  plt.scatter(xp2+xc,yp2+yc,s=10,color='k')

57  plt.scatter(xp3+xc,yp3+yc,s=10,color='k')

58  plt.scatter(xp4+xc,yp4+yc,s=10,color='k')

59
60  #——————————————————————————–plot unrotated rectangle
61  xg1=xc+xp1 #——————–corner coordinates in Xg,Yg system
62  yg1=yc+yp1

63  xg2=xc+xp2

64  yg2=yc+yp2

65  xg3=xc+xp3

66  yg3=yc+yp3

67  xg4=xc+xp4

68  yg4=yc+yp4

69
70  xg=[xg1,xg2,xg3,xg4,xg1]

71  yg=[yg1,yg2,yg3,yg4,yg1]

72
73  plt.plot((xg),(yg),color='k')

74
75  #———————————————————————–rotate rectangle corner coordinates
76  rz=45

77  rz=rz*np.pi/180

78
79  #———————————————————————————————————–point 1
80  xp=xp1

81  yp=yp1

82  [xg,yg]=rotz(xp,yp,rz)

83  [xg1,yg1]=[xg,yg]

84
85  #———————————————————————————————————–point 2
86  xp=xp2

87  yp=yp2

88  [xg,yg]=rotz(xp,yp,rz)

89  [xg2,yg2]=[xg,yg]

90
91  #———————————————————————————————————–point 3
92  xp=xp3

93  yp=yp3

94  [xg,yg]=rotz(xp,yp,rz)

95  [xg3,yg3]=[xg,yg]

96
97  #———————————————————————————————————–point 4
98  xp=xp4

99  yp=yp4

100 [xg,yg]=rotz(xp,yp,rz)

101 [xg4,yg4]=[xg,yg]

102
103 #———————————————————————————————————–plot rotated rectangle
104 xg=[xg1,xg2,xg3,xg4,xg1]

105 yg=[yg1,yg2,yg3,yg4,yg1]

106
107 plt.plot(xg,yg,color='r')

108
109 plt.show()
Listing 2-11Program 2DROTRECTANGLE

总结一下这个过程,首先使用局部 x,y 系统中位于坐标 xp,yp 的点构造一个对象,在这个例子中是一个简单的矩形。这是通过指定相对于旋转中心 c 的坐标来完成的。接下来,指定 Rz,计算变换矩阵的元素,通过 Rz 变换每个坐标,通过 xc,yc *移旋转的点,以将所有内容都放入全局 xg,yg 系统,然后进行绘图。转换由函数 rotz(xp,yp,rz)执行,该函数同时旋转坐标并将坐标转换为 xg,yg 系统以进行绘制。在这种情况下,您首先转换所有的坐标,然后在最后使用列表进行绘制。在一些程序中,你将在变换后立即绘制点或线。

接下来,围绕矩形的左下角旋转矩形。如图 2-17 所示。执行此操作的程序(未列出)与清单 2-11 类似,只是旋转中心改为

$$ xc=55 $$

(2-40)

$$ yc=55 $$

(2-41)并且角坐标改为

$$ xp1=0 $$

(2-42)

$$ xp2=+50 $$

(2-43)

$$ xp3=+50 $$

(2-44)

$$ xp4=0 $$

(2-42)

这些尺寸相对于旋转中心,(xc,yc)。

A456962_1_En_2_Fig17_HTML.jpg

图 2-17

Rotation of a rectangle about a corner

旋转中心 c 不必与物体邻接;你可以把它放在任何地方,只要相对于旋转中心的角坐标被更新。

图 2-18 显示了一个构建和旋转圆形物体的例子。显然,如果没有一些与众不同的特征,你将无法看到一个圆是否被旋转了,所以你将开始圆的上半部分设为绿色,下半部分设为红色。你也可以在直径上加一个横条,两端各有一个点。图 2-19 为清单 2-12 生成图 2-18 所用的模型。

如图 2-19 和 2-18 所示,参照清单 2-12 ,在 41 和 42 行程序中,以 xcc,ycc 为圆心,构造起始圆。它的半径 r=10,设置在第 43 行。在第 45-47 行的步长 dp 中,角度 p 从 P1 = 0°开始,到 p2=2π。请注意,您没有使用角度定义 Rz,因为 p 是关于点 xcc,ycc(圆心)的局部角度,而不是旋转中心 xc,yc。沿着圆的周长的点在局部坐标中的第 55 和 56 行中计算。当 alpha=0 时,这会产生起始圆。

在第 57 行的函数调用中使用 alpha 说明您可以为角度使用任何名称,即使在第 29 行的函数定义中使用了 Rz。您正在将一个数字从函数调用传递给一个函数。它两端的名字并不重要;该函数接收的值将与调用该函数时的值相同。

A456962_1_En_2_Fig19_HTML.jpg

图 2-19

Model used by Listing 2-12

A456962_1_En_2_Fig18_HTML.jpg

图 2-18

Circles rotated about point c from Listing 2-12

当 alpha >0 时,绘制其他四个圆。从第 53 行开始的 alpha 循环围绕旋转中心顺时针移动圆心(xcc,ycc ),步进为 dalpha,这在第 51 行设置。通过调用 rotz 在第 57 行转换局部坐标。在 rotz 函数调用中包含 Alpha 具有使圆绕其自身中心旋转的效果(xcc,ycc)。第 58-61 行确定每个圆周点是否位于 p=0 和 p=π之间。如果是这样,该点将被标绘为红色,否则标绘为绿色。因此,圆圈的上半部分是红色的,下半部分是绿色的。第 62-70 行绘制了直径条和点。

这种方法的一个重要特征是,不仅圆的中心以步长 dα围绕点 c 旋转,而且每个圆本身也围绕其自身的中心旋转,这可以从旋转的圆中的红色和绿色扇区以及直径条的重新定向中看出。在下一个程序中,您将围绕 c 点旋转每个圆的中心,同时保持每个圆不围绕自己的中心旋转。

为什么我在这个演示中使用圆形?主要是因为它说明了如何在相对于旋转中心的任何位置构造圆形并旋转它们。它说明了知道旋转中心位置的重要性;它不一定与圆心相同。

在这种情况下,你在圆的*面内旋转,这显然不是很有启发性。但是稍后,当我展示如何在三维空间中旋转对象(例如一个圆)时,这些概念将变得有用。在圆形的情况下,当旋转出其*面时,它产生一个椭圆形,这对于在三维空间中描绘圆形和球形物体如圆柱体和球体是必不可少的。简单地将一个圆绕坐标方向旋转出*面,就得到一个椭圆。

1   """
2   2DROTCIRCLE1
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7
8   plt.axis([-10,150,100,-10])
9   plt.axis('on')
10  plt.grid(True)
11
12  #————————————————————————————————————————————————————–axes
13  plt.arrow(0,0,40,0,head_length=4,head_width=2,color='b')
14  plt.arrow(0,0,0,40,head_length=4,head_width=2,color='b')
15  plt.text(30,-3,'Xg',color='b')
16  plt.text(-8,34,'Yg',color='b')
17
18  xc=80 #————————————————–center of rotation
19  yc=30
20  plt.plot([xc-50,xc+60],[yc,yc],linewidth=1,color='grey') #—-X
21  plt.plot([xc,xc],[yc-35,yc+60],linewidth=1,color='grey') #—-Y
22  plt.text(xc+50,yc-2,'X')
23  plt.text(xc-5,yc+55,'Y')
24
25  plt.scatter(xc,yc,s=20,color='k') #—plot center of rotation
26  plt.text(xc-5,yc-3,'c')
27
28  #———————————————————————————————————————–define rotation matrix Rz
29  def rotz(xp,yp,rz):
30       c11=np.cos(rz)
31       c12=-np.sin(rz)
32       c21=np.sin(rz)
33       c22=np.cos(rz)
34       xpp=xp*c11+yp*c12 #—-rotated coordinates relative to xc,yc
35       ypp=xp*c21+yp*c22
36       xg=xc+xpp #—-rotated coordinates relative to xg,yg
37       yg=yc+ypp
38       return [xg,yg]
39
40  #——————————————————————————————————————————————————plot circles
41  xcc=25 #–xcc,ycc=center of starting circle in local X,Y system
42  ycc=0

43  r=10 #—radius
44
45  p1=0 #——–p1,p2=angles around circle center
46  p2=2*np.pi

47  dp=(p2-p1)/100

48
49  alpha1=0 #—–angles around xc,yc
50  alpha2=2*np.pi
51  dalpha=(alpha2-alpha1)/5
52
53  for alpha in np.arange(alpha1,alpha2,dalpha):
54        for p in np.arange(p1,p2,dp):
55             xp=xcc+r*np.cos(p) #——xp,yp=coordinates relative to local X,Y system
56             yp=ycc+r*np.sin(p)

57             [xg,yg]=rotz(xp,yp,alpha)

58             if p < np.pi:

59                  plt.scatter(xg,yg,s=1,color='r') #——plot lower half red
60             else:

61                  plt.scatter(xg,yg,s=1,color='g') #——plot upper half green
62             xp1=xcc+r #——plot diameter bars and bar end points
63             yp1=0

64             [xg1,yg1]=rotz(xp1,yp1,alpha)

65             xp2=xcc-r

66             yp2=0

67             [xg2,yg2]=rotz(xp2,yp2,alpha)

68             plt.plot([xg1,xg2],[yg1,yg2],color='b')

69             plt.scatter(xg1,yg1,s=10,color='b')

70             plt.scatter(xg2,yg2,s=10,color='b')

71
72  plt.text(xc+31,yc-13,'starting circle')
73  plt.arrow(xc+31,yc-13,-3,2,head_length=2,head_width=1)
74
75  plt.show()
Listing 2-12Program 2DROTCIRCLE1

如图 2-20 所示,列表 2-13 以角度 dα为增量旋转起始圆,同时保持每个圆的方向不变。该程序与前一个程序类似,只是每个圆的局部中心绕 c 点旋转,而由起始圆定义的圆周点保持不旋转。程序应该是不言自明的。

请注意清单 2-12 和 2-13 之间的区别。在清单 2-12 中,旋转发生在第 53-70 行。在每个角度α处,围绕圆周的每个点的坐标在线 55 和 56 中确定。然后在第 57 行使用函数 rotz(xp,yp,alpha)对这些进行转换。也就是说,圆周上的每个点都旋转了角度α。这具有旋转整个圆的效果,如图 2-18 所示。然而,在清单 2-13 中,绘图是在第 41-68 行完成的。在这里,只有圆心在第 50 行和第 51 行旋转。在第 55 行,rotz(xp,yp,0)在其参数中使用角度 p=0。这具有不旋转圆本身的效果,仅旋转其中心,如图 2-20 所示。

A456962_1_En_2_Fig20_HTML.jpg

图 2-20

Circles with centers rotated about point c from Listing 2-13

您应该使用哪种旋转方法:图 2-18 或 2-20 所示的方法?这取决于你的应用。在一种情况下,您可能希望整个对象(包括组成它的点)围绕一个中心旋转,而在另一种情况下,您可能只希望对象的中心旋转,而对象保持其原始方向。见图 2-21 。

A456962_1_En_2_Fig21_HTML.jpg

图 2-21

Model used by Listing 2-13

1   """
2   2DROTCIRCLE2
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7
8   plt.axis([-10,150,100,-10])
9   plt.axis('on')
10  plt.grid(True)
11
12  #————————————————————————————————————————————————————–axes
13  plt.arrow(0,0,40,0,head_length=4,head_width=2,color='b')
14  plt.arrow(0,0,0,40,head_length=4,head_width=2,color='b')
15  plt.text(30,-3,'Xg',color='b')
16  plt.text(-8,34,'Yg',color='b')
17
18  xc=80 #—————————————————center of rotation
19  yc=30
20  plt.plot([xc-50,xc+60],[yc,yc],linewidth=1,color='grey') #—-X
21  plt.plot([xc,xc],[yc-35,yc+60],linewidth=1,color='grey') #—-Y
22  plt.text(xc+50,yc-2,'X')
23  plt.text(xc-5,yc+55,'Y')
24
25  plt.scatter(xc,yc,s=20,color='k') #—plot center of rotation
26  plt.text(xc-5,yc-3,'c')
27
28  #————————————————————————————————————————define rotation matrix Rz
29  def rotz(xp,yp,rz):
30       c11=np.cos(rz)
31       c12=-np.sin(rz)
32       c21=np.sin(rz)
33       c22=np.cos(rz)
34       xpp=xp*c11+yp*c12 #—-relative to xc,yc
35       ypp=xp*c21+yp*c22
36       xg=xc+xpp #—-relative to xg,yg
37       yg=yc+ypp
38       return [xg,yg]
39
40  #————————————————————————————————————————————plot circles
41  p1=0
42  p2=2*np.pi
43  dp=(p2-p1)/100
44
45  alpha1=0
46  alpha2=2*np.pi
47  dalpha=(alpha2-alpha1)/5
48
49  for alpha in np.arange(alpha1,alpha2,dalpha):
50        xcc=25*np.cos(alpha)

51        ycc=25*np.sin(alpha)

52        for p in np.arange(p1,p2,dp):
53             xp=xcc+r*np.cos(p)
54             yp=ycc+r*np.sin(p)
55             [xg,yg]=rotz(xp,yp,0)

56             if p < np.pi:
57                  plt.scatter(xg,yg,s=1,color='r')
58             else:
59                  plt.scatter(xg,yg,s=1,color='g')
60             xp1=xcc+r
61             yp1=ycc+0
62             [xg1,yg1]=rotz(xp1,yp1,0)
63             xp2=xcc-r
64             yp2=ycc+0
65             [xg2,yg2]=rotz(xp2,yp2,0)
66             plt.plot([xg1,xg2],[yg1,yg2],color='b')
67             plt.scatter(xg1,yg1,s=10,color='b')
68             plt.scatter(xg2,yg2,s=10,color='b')
69
70  plt.text(xc+34,yc-10,'starting circle')
71  plt.arrow(xc+34,yc-10,-2,2,head_length=1,head_ width=1)
72
73  plt.show()
Listing 2-13Program 2DROTCIRCLE2

2.10 摘要

在这一章中,你看到了如何使用点和线来构建二维图形。您学习了相对坐标的概念,特别是本地系统,它用于构建具有相对于中心的坐标值的图像,在旋转的情况下,它可以用作旋转的中心,以及用于绘图的全球系统。您已经看到了如何将局部坐标转换到全局坐标系中进行绘图,全局坐标系的原点是通过 plt.axes()函数定义的。你看到了如何从点构造线;将彩色圆点排列成艺术图案;并使用点和线段绘制圆弧、圆盘、圆和椭圆。然后你学习了*移(容易)和旋转(不那么容易)的概念。您将所有这些应用于点、矩形和圆形。在下一章,你将把这些想法扩展到三个维度。

三、三维图形

在本章中,您将学习如何在三维空间中创建、*移和旋转三维对象。您还将学习如何将它们投影并显示在计算机屏幕的二维表面上。物体的一般运动意味着*移和旋转。在前一章中,我从两个方面讨论了这一点。你看到了二维的*移是微不足道的。只需在 x 坐标上加上或减去一个量,就可以在 x 方向上*移,对于 y 方向也是如此。在三维空间,这仍然是微不足道的,尽管你现在能够在第三维空间,z 方向,简单地通过增加或减少一个物体的 z 坐标。然而,旋转是另一回事。该分析遵循您在二维空间中使用的方法,但由于您现在能够围绕三个坐标方向旋转对象,因此该分析变得复杂。在这一章中,我将不再进一步讨论 3D *移,而是集中讨论 3D 旋转。

3.1 三维坐标系

在前面关于二维旋转的讨论中,您在二维 x,y *面中旋转了二维对象。你现在通过引入第三个轴,z 轴,将这些概念扩展到三维,如图 3-1 所示。请注意,z 轴指向屏幕内部,而不是外部。这不是一个任意的选择。我们遵循右手定则惯例,通过 x 轴和 y 轴之间的较小角度,将 x 轴向 y 轴旋转,找到 z 轴的正方向。当以这种方式转动时,z 轴的正方向将指向右旋螺钉所遵循的方向。在这种情况下,螺钉会进入筛网;这就是 z 轴的正方向。我们可以基于一个左手螺旋构建一个完整的数学理论,但是最普遍使用的惯例是右手系统。有些书和论文把坐标轴标注为 x1,x2,x3。按照右手定则,如上所述,通过将 x1 旋转到 x2,可以找到 x3 的方向。在这项工作中,我们将继续使用 x,y,z 符号来表示方向。

A456962_1_En_3_Fig1_HTML.jpg

图 3-1

Three-dimensional coordinate axes with point P at coordinates (x,y,z)

现在应该很清楚为什么我在前面的二维旋转的讨论中使用术语 Rz;它是指围绕 z 轴的旋转。当 x 向右,y 向下时,这表现为 x-y *面中的顺时针旋转。如果 x 向右,y 向上,z 将指向屏幕外,绕 z 轴的正向旋转将表现为逆时针旋转。

按照二维旋转分析中使用的方法,在本章的剩余部分,我将讨论绕 x、y 和 z 轴的独立旋转,然后讨论绕所有三个轴的组合旋转。顺便说一下,当我说“绕 x 轴旋转”时,例如,我在暗示这等同于“绕 x 方向旋转”,反之亦然。虽然绕*行于 x 轴的假想轴的旋转与绕 x 轴的旋转并不完全相同,但区别只是*移的问题。我将交替使用这两个术语,除非会引起混淆。

图 3-2 显示了右侧的 x、y、z 系统。想象你站在原点,向 x 轴方向看去。如果你顺时针转动一个右旋的螺丝,它会沿着 x 轴的正方向前进。双向箭头是指示右旋方向 Rx 的常规方式;Ry 和 Rz 也是如此。

为什么我选择如图 3-2 所示的坐标系定向?标准 matplotlib 使用不同的方向,例如在 https://matplotlin.org/mpl_toolkits/mplot3d/tutorial.html#scatter-plots 中所示。

A456962_1_En_3_Fig2_HTML.jpg

图 3-2

Three-dimensional coordinate axes showing right-hand rotation around each coordinate direction

如前所述,图 3-2 中的方向更加直观。正在构建的对象位于由 x、y、z 轴定义的空间内。在这种情况下,观察者在空间之外向内看。该物体可以任意*移和旋转以给出任何期望的视图。你可以直视一个物体,也可以从上方或下方观看,就像第十章中土星的照片所示。另一方面,matplotlib 方向是数据绘图常用的方向,也是你在第九章中要用到的方向;查看图 9-1 至 9-5 。如果你更喜欢标准的 matplotlib 系统,很容易改变到那个方向;就像在第九章中所做的那样,将轴旋转到任何你想要的方向,其中,为了使 z 指向上,你围绕全局 x 方向旋转-100 度(稍微向前倾斜 z),全局 y 轴旋转-135 度,全局 z 方向旋转+8 度(参见清单 9-1 中的第 191-193 行)。您可以通过围绕全局轴的小旋转来微调方向。在你完成这一章之后,如果你愿意,你会发现给背景*面着色很容易,如 matplotlib 所示。只要遵循右手定则,您可以任意确定轴的方向。

3.2 坐标*面上的投影

我们如何在二维电脑显示器上显示三维物体?我们通过将物体投影到三个二维坐标*面(x,y;x,z;和 y,z ),然后在监视器上绘制这些图像中的任何一个。图 3-3 显示了一条从 A 到 b 的三维线(黑色)。从绘图空间上方向下看 x,z *面,您会看到它是一条红线,它是黑线在 x,z *面上的投影。类似地,绿线显示了它在 y,z *面上的投影;蓝线是它在 x,y *面上的投影。我将只使用这些投影中的一个进行可视化,通常是 x,y 投影。

A456962_1_En_3_Fig3_HTML.jpg

图 3-3

Projection of a three-dimensional line (black) onto the three coordinate planes: red=x,z projection, green=y,z projection, and blue=x,y projection

x,y 投影是通过在 x,y *面上绘制一个点的 x 和 y 坐标获得的;对于一条直线,在直线端点的 x 和 y 坐标之间绘制一条直线。在从空间坐标(xA,yA,zA)到(xB,yB,zB)的黑线的情况下,在 xA,yA 和 xB,yB 之间绘制一条线:

plt.plot([xA,xB],[yA,yB],color='b')

这给你一条蓝线,它是在 x,y *面上的投影,如图 3-4 所示。如果你想得到顶视图,你画出黑线的 z,x 坐标。如果使用普通坐标轴绘图,x 轴从左向右,y 轴从左向下,则 y 轴将替换 z 轴。这相当于绕 x 轴旋转-90 度。然后,在直线的 z 和 x 坐标之间绘制

plt.plot([zA,zB],[xA,xB],color='r')

去拿红线。要获得绿色的 y,z 投影,可以使用命令绘制 z 和 y 坐标

A456962_1_En_3_Fig4_HTML.jpg

图 3-4

Projection of a three-dimensional line onto the x, y plane

plt.plot([zA,zB],[yA,yB],color='g')

在这种情况下,您必须重新定向屏幕坐标轴,使+z 从左向右穿过屏幕顶部,y 轴从右侧向下。这将给出从 x,y,z 坐标系之外的 z,y 视图。

注意,在投影到 x,y *面的情况下,不使用对象的 z 坐标。但是你仍然需要它们来执行旋转。类似地,对于其他投影,投影不需要一个坐标,但旋转需要一个坐标,因此它必须包含在分析中。

为了简化一切,您将在接下来的大部分工作中使用 x,y 投影。正如您将看到的,围绕三个坐标方向旋转一个对象并将该对象的(x,y)坐标投影到 x,y *面上将产生一个三维视图。

三维物体在二维坐标*面上的投影称为等距投影。它们通常用于工程和制图。这些图像看起来不像它们在人眼或照片中的样子,因为缺少艺术家所说的透视缩小,更普遍的说法是透视。举一个透视缩小的例子,如果你沿着铁路轨道向下看一排向远处延伸的电线杆,离你最*的电线杆看起来会比远处的电线杆高,当它们接*地*线时,铁轨看起来会合并。什么导致透视缩短?这仅仅是因为眼睛在远处比在*处能看到更多的区域。以电线杆为例,这是因为远处有更多的垂直空间,所以固定高度的电线杆所占的比例较小;对于铁轨来说,这是不断扩大的水*空间。等轴测投影不考虑透视缩小,但我会在第四章讨论透视变换时考虑。

A456962_1_En_3_Fig5_HTML.jpg

图 3-5

Isometric vs. perspective views

虽然您已经看到了如何将一条简单的三维线及其端点投影到三个坐标*面上,但是您可能已经处理过由许多点和线组成的更复杂的对象。正如你所看到的,甚至一个圆也可以仅仅由点(点)或线构成,并且有任何程度的细化。

虽然是一个简单的例子,但三维线说明了您将在以下工作中使用的方法:根据具有坐标(x,y,z)的点和线在三维 x,y,z 空间内定义形状;通过旋转和*移对它们进行操作;将它们投影到 x,y *面上。然后用它们的 x 和 y 坐标把它们画出来。因此,您可以将 3D 对象投影到计算机显示器的屏幕上。

在三维空间中旋转一个点意味着围绕 x、y 和 z 方向旋转它。在前一章中,你看到了如何围绕 z 方向,Rz,进行二维旋转。在这里,您将获得绕 y、x 和 z 方向旋转的三维变换。

3.3 绕 y 方向旋转

图 3-6 显示了围绕 y 方向 ry 旋转的单位矢量几何图形。这是俯视 x,y,z 系统顶部时看到的视图。y 轴伸入纸的*面。

A456962_1_En_3_Fig6_HTML.jpg

图 3-6

Unit vectors for rotation about the y direction. This is a view looking down on the plotting space. The y axis runs into the plane of the paper.

按照第二章中使用的方法,一个位置最初由矢量 P 定义的点被旋转到 P。定义 P 和 P’在 x、y、z(未旋转)和 x’、y’、z’(旋转)系统中的位置的矢量是

$$ \mathbf{P}= xp\widehat{\mathbf{i}}+ yp\widehat{\mathbf{j}}+ zp\widehat{\mathbf{k}} $$

【3-1】

$$ {\mathbf{P}}^{\prime }=x{p}{\prime}\widehat{\mathbf{i}}+y{p}{\prime}\widehat{\mathbf{j}}+z{p}^{\prime}\widehat{\mathbf{k}} $$

【3-2】

$$ {\mathbf{P}}^{\prime }= xp{\widehat{\mathbf{i}}}^{\prime }+ yp{\widehat{\mathbf{j}}}^{\prime }+ zp{\widehat{\mathbf{k}}}^{\prime } $$

【3-3】其中 i\、j\和 k\是在 x、y 和 z 方向上的单位矢量,i\\\\\\\\'和 k\\\\\\\从图 3-6 可以看出

$$ {\widehat{\mathbf{i}}}^{\prime }=\mathit{\cos}(Ry)\kern0.1em \widehat{\mathbf{i}}+(0)\widehat{\mathbf{j}}-\mathit{\sin}(Ry)\kern0.1em \widehat{\mathbf{k}} $$

(3-4)

$$ {\widehat{\mathbf{j}}}^{\prime }=(0)\kern0.1em \widehat{\mathbf{i}}+(1)\widehat{\mathbf{j}}-(0)\kern0.1em \widehat{\mathbf{k}} $$

(3-5)

$$ {\widehat{\mathbf{k}}}^{\prime }=\mathit{\sin}(Ry)\kern0.1em \widehat{\mathbf{i}}+(0)\widehat{\mathbf{j}}-\mathit{\cos}(Ry)\kern0.1em \widehat{\mathbf{k}} $$

(3-6)

将它们代入方程 3-3 得到

$$ {\mathbf{P}}^{\prime }= xp\left[\mathit{\cos}(Ry)\widehat{\mathbf{i}}-\mathit{\sin}(Ry)\widehat{\mathbf{k}}\right]+ yp\widehat{\mathbf{j}}+ zp\left[\mathit{\sin}(Ry)\widehat{\mathbf{i}}+\mathit{\cos}(Ry)\widehat{\mathbf{k}}\right] $$

(3-7)

分离成ˇI、ˇj、kˇ分量,得到

$$ {\mathbf{P}}^{\prime }=\underset{x{p}^{\prime }}{\underbrace{\left[ xpcos(Ry)+ zp\kern0.1em \mathit{\sin}(Ry)\right]}}\widehat{\mathbf{i}}+\underset{y{p}^{\prime }}{\underbrace{\left[ yp\right]}}\widehat{\mathbf{j}}+\underset{z{p}^{\prime }}{\underbrace{\left[- xp\kern0.1em \mathit{\sin}(Ry)+ zp\kern0.1em \mathit{\cos}(Ry)\right]}}\widehat{\mathbf{k}} $$

(3-8))

用方程式 3-2 、

$$ x{p}^{\prime }= xp\kern0.1em \mathit{\cos}(Ry)+ zp\kern0.1em \mathit{\sin}(Ry) $$

、【3-10】、

、【3-11】

等式 3-9 到 3-11 给出了旋转点在本地 x、y、z 系统中的坐标。当然,方程 3-10 中的 yp′= yp,因为 y 坐标不会随着绕 y 轴的旋转而改变。

方程 3-9 、 3-10 、 3-11 可以用矩阵形式表示,如方程 3-12 :

$$ \left[\begin{array}{c}x{p}^{\prime}\ {}y{p}^{\prime}\ {}z{p}^{\prime}\end{array}\right]=\left[\begin{array}{ccc}\mathit{\cos}(Ry)& 0& \mathit{\sin}(Ry)\ {}0& 1& 0\ {}-\mathit{\sin}(Ry)& 0& \mathit{\cos}(Ry)\end{array}\right]\kern0.1em \left[\begin{array}{c} xp\ {} yp\ {} zp\end{array}\right] $$

(3-12)所示

它可以缩写为

$$ \left[{P}^{\hbox{'}}\right]=\left[ Ry\right]\kern0.1em \left[P\right] $$

(3-13)

【Ry】,y 轴旋转的变换矩阵,是

$$ \left[ Ry\right]=\left[\begin{array}{ccc} Cy\left(1,1\right)& Cy\left(1,2\right)& Cy\left(1,3\right)\ {} Cy\left(2,1\right)& Cy\left(2,2\right)& Cy\left(2,3\right)\ {} Cy\left(3,1\right)& Cy\left(3,2\right)& Cy\left(3,3\right)\end{array}\right] $$

(3-14)

$$ Cy\left(1,1\right)=\mathit{\cos}(Ry) $$

(3-15)

$$ Cy\left(1,2\right)=0 $$

(3-16)

$$ Cy\left(1,3\right)=\mathit{\sin}(Ry) $$

(3-17)

$$ Cy\left(2,1\right)=0 $$

(3-18)

$$ Cy\left(2,2\right)=0 $$

(3-19)

$$ Cy\left(2,3\right)=0 $$

(3-20)

$$ Cy\left(3,1\right)=-\mathit{\sin}(Ry) $$

(3-21)

这些元素将在随后的程序中使用。

3.4 绕 x 方向旋转

图 3-7 显示了绕 x 方向旋转的单位矢量几何图形。

A456962_1_En_3_Fig7_HTML.jpg

图 3-7

Unit vectors for rotation around the x direction. The x axis runs into the plane of the paper.

你看那个

$$ {\widehat{\mathbf{i}}}^{\prime }=(1)\kern0.1em \widehat{\mathbf{i}}+(0)\widehat{\mathbf{j}}+(0)\kern0.1em \widehat{\mathbf{k}} $$

(3-24)

$$ {\widehat{\mathbf{j}}}^{\prime }=(0)\kern0.1em \widehat{\mathbf{i}}+\mathit{\cos}(Rx)\kern0.1em \widehat{\mathbf{j}}+\mathit{\sin}(Rx)\kern0.1em \widehat{\mathbf{k}} $$

(3-25)

$$ {\widehat{\mathbf{k}}}^{\prime }=(0)\kern0.1em \widehat{\mathbf{i}}-\mathit{\sin}(Rx)\kern0.1em \widehat{\mathbf{j}}+\mathit{\cos}(Rx)\kern0.1em \widehat{\mathbf{k}} $$

(3-26)

按照上一节的方法,

$$ \mathbf{P}= xp\widehat{\mathbf{i}}+ yp\widehat{\mathbf{j}}+ zp\widehat{\mathbf{k}} $$

(3-27)

$$ {\mathbf{P}}^{\prime }= xp\widehat{\mathbf{i}}+ yp\left[\mathit{\cos}(Rx)\widehat{\mathbf{j}}+\mathit{\sin}(Rx)\widehat{\mathbf{k}}\right]+ zp\left[-\mathit{\sin}(Rx)\widehat{\mathbf{j}}+\mathit{\cos}(Rx)\widehat{\mathbf{k}}\right] $$

(3-28)

$$ =\underset{x{p}^{\prime }}{\underbrace{xp}}\widehat{\mathbf{i}}+\underset{y{p}^{\prime }}{\underbrace{\left[ yp\kern0.1em \mathit{\cos}(Rx)- zp\kern0.1em \mathit{\sin}(Rx)\right]}}\widehat{\mathbf{j}}+\underset{z{p}^{\prime }}{\underbrace{\left[ yp\kern0.1em \mathit{\sin}(Rx)+ zp\kern0.1em \mathit{\cos}(Rx)\right]}}\widehat{\mathbf{k}} $$

(3-29)

在矩阵形式中是

$$ \left[\begin{array}{c}x{p}^{\prime}\ {}y{p}^{\prime}\ {}z{p}^{\prime}\end{array}\right]=\left[\begin{array}{ccc}1& 0& 0\ {}0& \mathit{\cos}(Rx)& -\mathit{\sin}(Rx)\ {}0& \mathit{\sin}(Rx)& \mathit{\cos}(Rx)\end{array}\right]\kern0.1em \left[\begin{array}{c} xp\ {} yp\ {} zp\end{array}\right] $$

(3-30),可以缩写为

$$ \left[{P}^{\hbox{'}}\right]=\left[ Rx\right]\kern0.1em \left[P\right] $$

(3-31)

这就引出了用于 x 方向旋转的变换矩阵

$$ \left[ Rx\right]=\left[\begin{array}{ccc}1& 0& 0\ {}0& \mathit{\cos}(Rx)& -\mathit{\sin}(Rx)\ {}0& \mathit{\sin}(Rx)& \mathit{\cos}(Rx)\end{array}\right] $$

(3-32)

$$ \left[ Rx\right]=\left[\begin{array}{ccc} Cx\left(1,1\right)& Cx\left(1,2\right)& Cx\left(1,3\right)\ {} Cx\left(2,1\right)& Cx\left(2,2\right)& Cx\left(2,3\right)\ {} Cx\left(3,1\right)& Cx\left(3,2\right)& Cx\left(3,3\right)\end{array}\right] $$

(3-33)

$$ Cx\left(1,1\right)=1 $$

(3-34)

$$ Cx\left(1,2\right)=0 $$

(3-35)

$$ Cx\left(1,3\right)=0 $$

(3-36)

$$ Cx\left(2,1\right)=0 $$

(3-37)

$$ Cx\left(2,2\right)=\mathit{\cos}(Rx) $$

(3-38)

$$ Cx\left(2,3\right)=-\mathit{\sin}(Rx) $$

3.5 绕 z 方向旋转

在第二章中,你推导出了绕 z 方向二维旋转的变换矩阵。你现在将在三维空间中做这件事。重复第 2 :

$$ \left[\begin{array}{c}x{p}^{\prime}\ {}y{p}^{\prime}\end{array}\right]=\left[\begin{array}{cc}\mathit{\cos}(Rz)& -\mathit{\sin}(Rz)\ {}\mathit{\sin}(Rz)& \mathit{\cos}(Rz)\end{array}\right]\kern0.1em \left[\begin{array}{c} xp\ {} yp\end{array}\right] $$

(3-43)章中的二维 Rz 矩阵(方程 3-43

在三个维度中,你有以下几种:

$$ \left[\begin{array}{c}x{p}^{\prime}\ {}y{p}^{\prime}\ {}z{p}^{\prime}\end{array}\right]=\left[\begin{array}{ccc}\mathit{\cos}(Rz)& -\mathit{\sin}(Rz)& 0\ {}\mathit{\sin}(Rz)& \mathit{\cos}(Rz)& 0\ {}0& 0& 1\end{array}\right]\kern0.1em \left[\begin{array}{c} xp\ {} yp\ {} zp\end{array}\right] $$

(3-44)

$$ \left[ Rz\right]=\left[\begin{array}{ccc} Cz\left(1,1\right)& Cz\left(1,2\right)& Cz\left(1,3\right)\ {} Cz\left(2,1\right)& Cz\left(2,2\right)& Cz\left(2,3\right)\ {} Cz\left(3,1\right)& Cz\left(3,2\right)& Cz\left(3,3\right)\end{array}\right] $$

(3-45)

$$ Cz\left(1,1\right)=\mathit{\cos}(Rz) $$

(3-46)

$$ Cz\left(1,2\right)=-\mathit{\sin}(Rz) $$

(3-47)

$$ Cz\left(1,3\right)=0 $$

(3-48)

$$ Cz\left(2,1\right)=\mathit{\sin}(Rz) $$

(3-49)

$$ Cz\left(2,2\right)=\mathit{\cos}(Rz) $$

(3-50)

$$ Cz\left(2,3\right)=0 $$

(3-55)

通过简单地观察第一行中的 XP’不依赖于 zp,因此 C(1,3)=0,可以将二维矩阵方程扩展到方程 3-44 中的三维矩阵方程;在第二行中,yp’也不依赖于 zp,因此 c(2,3)= 0;在第三行中,ZP’不依赖于 XP’或 yp’,因此 C(3,1)和 C(3,2)都等于 0。C(3,3)=1,因为在绕 z 轴旋转后,z 坐标保持不变。

这三个转变概括起来就是:

$$ \left[ Rx\right]=\left[\begin{array}{ccc}1& 0& 0\ {}0& \mathit{\cos}(Rx)& -\mathit{\sin}(Rx)\ {}0& \mathit{\sin}(Rx)& \mathit{\cos}(Rx)\end{array}\right] $$

(3-55)

$$ \left[ Ry\right]=\left[\begin{array}{ccc}\mathit{\cos}(Ry)& 0& \mathit{\sin}(Ry)\ {}0& 1& 0\ {}-\mathit{\sin}(Ry)& 0& \mathit{\cos}(Ry)\end{array}\right] $$

(3-56)

$$ \left[ Rz\right]=\left[\begin{array}{ccc}\mathit{\cos}(Rz)& -\mathit{\sin}(Rz)& 0\ {}\mathit{\sin}(Rz)& \mathit{\cos}(Rz)& 0\ {}0& 0& 1\end{array}\right] $$

(3-57)

3.6 绕坐标方向的单独旋转

图 3-8 显示了盒子(a)绕 x、y 和 z 方向的单独旋转。该图形是使用清单 3-1 创建的。轮换是分开的,不是连续的。即,盒子(b)是旋转了 Rx 的盒子(a );盒子(c)被(a)旋转了 Ry;并且框(d)被(a)旋转了 Rz。旋转不是相加的,这意味着 Ry 没有加到 Rx 的结果上,Rz 没有加到 Rx 和 Ry 的结果上;它们分别是原始盒子(a)的独立旋转。旋转是围绕盒子的中心进行的。

A456962_1_En_3_Fig8_HTML.jpg

图 3-8

Output from Listing 3-1. Projection (a) of an unrotated box on the x,y plane, (b) rotated around the x direction by Rx=45°, (c) around the y direction by Ry=30°, and (d) around the z direction by Rz=30°. Double-headed red arrows show the direction of rotation using the right-hand rule convention. Heavy lines indicate the top and bottom. The boxes are rotated about their center, which is indicated by a black dot.

清单 3-1 利用了函数和列表。如果没有它们,程序的规模将会翻倍。使用它们可以大大减小程序的大小。它可以通过使用数组来进一步缩短,但是节省将是最小的,并且倾向于模糊该方法。

图 3-9 显示了清单 3-1 使用的角编号方案。角落的数字是蓝色的。它们是 Python 列表编号,从 0 开始。通常我们从 1 到 8 给角编号。然而,在 Python 中,列表中的第一个元素总是 0。在八角盒的情况下,最后一个,即第八个,是列表中的元素 7。比如第一个点的 x 坐标是 x[0],第二个是 x[1],依此类推。这就像把梯子的第一级编号为第零级。迷惑?是的。要怪就怪 C 编程语言吧,这个陷阱是从 C 语言遗留下来的。也许避免问题的最好方法是养成从 0 而不是从 1 开始编号的习惯,这就是我在图 3-9 中所做的。我可以在图 3-9 中使用不同的编号安排,但是从左上角开始并顺时针进行似乎是合理的(例如,我可以从右上角而不是左上角开始编号)。只要选择的方案和程序一致就没关系。

A456962_1_En_3_Fig9_HTML.jpg

图 3-9

Numbering scheme for the box’s corners in Lsiting 3-1. Lists at the upper right contain the coordinate values. They are the same as the lists in Listing 3-1, lines 14, 15, and 16. The center coordinates xc,yc,zc are not the same as used in Listing 3-1. The z axis is not shown.

图中所示的列表定义了角坐标。每个列表中有八个元素,因为盒子中有八个角。角落 2 是列表中的第三个元素,其坐标为 x[3]=10,y[3]=-10,z[3]=3。这些是局部坐标;换句话说,它们相对于盒子的中心,也就是旋转的中心。

清单 3-1 从在第 14-16 行定义列表[x]、[y]和[z]开始。这些线保存了盒子的角相对于其中心的坐标。第 18-20 行中的[xg]、[yg]和[zg]将在转换完成后保存全局绘图坐标。因为盒子有八个角,所以每个列表中保留八个空间。

接下来是旋转函数 rotx、roty 和 rotz 的定义。它们分别围绕 x、y 和 z 方向旋转点的坐标 xp、yp 和 zp。每个函数都返回一组新的坐标:xg、yg 和 zg,它们是旋转点的全局坐标。这些坐标将用于绘图。

查看 rotx 的定义,从第 23 行开始,当调用 rotx 进行关于 x 方向的变换时,rotx 接收盒子的中心坐标 xc,yc,zc,在这种情况下是旋转的中心,加上点的未旋转坐标 xp,yp,zp 和关于 x 方向的旋转角度 Rx。第 24 行的列表 a=[xp,yp,zp]包含未旋转点的坐标。这实际上是指向 xp,yp,zp 的向量。在第 25 行,b=[1,0,0]是等式 3-55 所示的 Rx 变换矩阵的第一行列表。第 26 行 xpp=np.inner(a,b)构成了这些列表的点或标量积。还有一个可以使用的 np.dot(a,b)函数。对于简单的非复数向量,np.inner(a,b)和 np.dot(a,b)给出相同的结果。但是对于更高维的数组,结果可能不同。

为了说明绕 x 方向旋转的 ypp 的计算,你已经看到向量 p通过

$$ \left[\begin{array}{c} xp p\ {} yp p\ {} zp p\end{array}\right]=\left[\begin{array}{ccc}1& 0& 0\ {}0& \mathit{\cos}(Rx)& -\mathit{\sin}(Rx)\ {}0& \mathit{\sin}(Rx)& \mathit{\cos}(Rx)\end{array}\right]\kern0.1em \left[\begin{array}{c} xp\ {} yp\ {} zp\end{array}\right] $$

(3-58)与 p 相关,其中 ypp(即 yp′)是旋转点的 y 坐标。程序中的第 27 行是等式 3-57 的第二行。a 和 b 的标量积在第 28 行形成,产生 ypp(yp’)。即

$$ a=\left[ xp, yp, zp\right] $$

(3-59)

$$ b=\left[0,\mathit{\cos}(Rx),-\mathit{\sin}(Rx)\right] $$

(3-60)

$$ ypp=\mathrm{np}.\mathrm{inner}\left(\mathrm{a},\mathrm{b}\right) $$

(3-61)

$$ = xp(0)+ yp\left(\mathit{\cos}(Rx)\right)+ zp\left(-\mathit{\sin}(Rx)\right) $$

(3-62)

$$ = ypcos(Rx)- zpsin(Rx) $$

(3-63)也就是第 28 行。第 29 行和第 30 行使用等式 3-57 的第三行重复该过程,产生 zpp(ZP’)。第 31 行将盒子中心的坐标 xc,yc,zc 与 xpp,ypp,zpp 相加,从而相对于全局坐标系的原点*移旋转的点,产生全局绘图坐标[xg,yg,zg]。roty 和 rotz 遵循相同的结构,在它们的 b 表中使用[Ry]和[Rz]行。

接下来是第 56 行的函数绘图框。这将使用长方体的全局角坐标 xg、yg 和 zg 来绘制长方体。从第 57 行开始的循环通过用线连接前三个角来绘制顶部。线 60 通过在角 3 和 0 之间画一条线来封闭顶部。这还没有包括在循环中,循环是为了绘制一个角和下一个角。当你试图将角 3 与 0 连接时,问题就来了;循环中的算法不起作用。可以修改它来处理它,但是只添加第 60 行比使循环复杂化更容易。直到第 68 行的 plotbox 的其余部分完成了该框。第 70 行在其中心画了一个点。

第 72 行启动函数 plotboxx。这将转换角坐标,使其为 plotbox 的绘图做好准备。从第 73 行到第 74 行的循环通过调用 rotx 围绕 x 方向旋转所有八个角。第 76 行调用函数 plotbox,它进行绘图。对于围绕 y 和 z 方向的旋转,plotboxy 和 plotboxz 也是如此。

到目前为止,您一直在定义函数。你在这个程序中使用函数,因为许多操作是重复的。如果你试着用单个语句写这个程序,它至少会有两倍长。

程序的控制权在第 91 行和第 116 行之间。第 91-95 行绘制了第一个框(a)。因为第一个方框(a)是不旋转的,所以在第 91 行指定 Rx=0。使用带有 Rx=0 参数的函数 plotboxx 进行绘图。您可以对 plotboxy 使用 Ry=0,或对 plotboxz 使用 Rz=0。这无关紧要,因为旋转角度为 0。第 92-94 行指定了盒子的中心坐标。第 95 行调用 plotboxx。结果如图 3-8 所示为(a)。第 98-116 行产生旋转的盒子(b)、(c)和(d)。

以方框(b)为例总结该过程,旋转角度设置在第 98 行;盒子的中心坐标在第 99-101 行。然后,在第 102 行,调用函数 plotboxx。中心坐标和角度 Rx 作为参数传递。plotboxx 从第 72 行开始,通过调用 rotx 旋转八个角。plotboxx 不使用 xc、yc 和 zc,但是它将它们传递给需要它们的 rotx。rotx 旋转和*移坐标,产生 xg,yg,zg。第 76 行调用函数 plotbox,它进行绘图。

在第 91、98、105 和 112 行中,您使用了函数 radians(),它是从第 7 行的数学库中导入的。(注意,您可以为此使用 numpy)。它将角度参数转换为弧度参数,这是 sin()和 cos()所需要的。在早期的程序中,您使用 np.pi/180.进行了转换

  1   """
  2   4BOXES
  3   """
  4
  5   import numpy as np
  6   import matplotlib.pyplot as plt
  7   from math import sin, cos, radians #–or use numpy
  8
  9   plt.axis([0,150,100,0])
 10   plt.axis('on')
 11   plt.grid(True)
 12
 13   #————————————————————————-lists
 14   x=[-10,-10,10,10,-10,-10,10,10] #–un-rotated corner coordinates
 15   y=[-10,-10,-10,-10,10,10,10,10] #–relative to box's center
 16   z=[ -3, 3, 3, -3,-3, 3, 3,-3]
 17
 18   xg=[0,1,2,3,4,5,6,7] #–define global coordinates
 19   yg=[0,1,2,3,4,5,6,7]
 20   zg=[0,1,2,3,4,5,6,7]
 21
 22   #———————————————————–function definitions
 23   def rotx(xc,yc,zc,xp,yp,zp,Rx):
 24        a=[xp,yp,zp]
 25        b=[1,0,0] #———————————-[cx11,cx12,cx13]
 26        xpp=np.inner(a,b) #—–scalar product of a,b=xp*cx11+yp*cx12+ zp*cx13
 27        b=[0,cos(Rx),-sin(Rx)] #—————[cx21,cx22,cx23]
 28        ypp=np.inner(a,b)
 29        b=[0,sin(Rx),cos(Rx)] #—————[cx31,cx32,cx33]
 30        zpp=np.inner(a,b)
 31        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 32        return[xg,yg,zg]
 33
 34   def roty(xc,yc,zc,xp,yp,zp,Ry):
 35        a=[xp,yp,zp]
 36        b=[cos(Ry),0,sin(Ry)] #——————–[cx11,cx12,cx13]
 37        xpp=np.inner(a,  b)
 38        b=[0,1,0] #—————[cx21,cx22,cx23]
 39        ypp=np.inner(a,b) #——————–scalar product of a,b
 40        b=[-sin(Ry),0,cos(Ry)] #—————[cx31,cx32,cx33]
 41        zpp=np.inner(a,b)
 42        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 43        return[xg,yg,zg]
 44
 45   def rotz(xc,yc,zc,xp,yp,zp,Rz):
 46        a=[xp,yp,zp]
 47        b=[cos(Rz),-sin(Rz),0] #——————-[cx11,cx12,cx13]
 48        xpp=np.inner(a, b)
 49        b=[sin(Rz),cos(Rz),0] #—————[cx21,cx22,cx23]
 50        ypp=np.inner(a,b)
 51        b=[0,0,1] #—————[cx31,cx32,cx33]
 52        zpp=np.inner(a,b) #———————scalar product of a,b
 53        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 54        return[xg,yg,zg]
 55
 56   def plotbox(xg,yg,zg): # –plots the box using its rotated coordinates xg,yg,zg
 57        for i in (0,1,2): #———————————————-plot top
 58              plt.plot([xg[i],xg[i+1]],[yg[i],yg[i+1]],linewidth=3,color='k')
 59
 60        plt.plot([xg[3],xg[0]],[yg[3],yg[0]],linewidth=3,color='k') #-close top
 61
 62        for i in (4,5,6): #——————————————-plot bottom
 63              plt.plot([xg[i],xg[i+1]],[yg[i],yg[i+1]],linewidth=3,color='k')
 64
 65        plt.plot([xg[7],xg[4]],[yg[7],yg[4]],linewidth=3,color='k') #–close bottom
 66
 67        for i in (0,1,2,3): #——————————————plot sides
 68             plt.plot([xg[i],xg[i-4]],[yg[i],yg[i-4]],linewidth=1,color='k')
 69
 70        plt.scatter(xc,yc,s=5) #–plot a dot at the center
 71
 72   def plotboxx(xc,yc,zc,Rx):
 73        for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
 74              [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
 75
 76        plotbox(xg,yg,zg)
 77
 78   def plotboxy(xc,yc,zc,Ry):
 79        for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
 80             [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
 81
 82        plotbox(xg,yg,zg)
 83
 84   def plotboxz(xc,yc,zc,Rz):
 85        for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
 86              [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
 87
 88        plotbox(xg,yg,zg)
 89
 90   #——————————————————————–R=0 box(a)
 91   Rx=radians(0)
 92   xc=25 #—————box (a) center coordinates
 93   yc=40
 94   zc=20
 95   plotboxx(xc,yc,zc,Rx) #–since Rx=0 we could use plotboxy or plotboxz
 96
 97   #———————————————————————Rx box(b)
 98   Rx=radians(45)
 99   xc=55
100   yc=40
101   zc=20
102   plotboxx(xc,yc,zc,Rx)
103
104   #——————————————————————–Ry box (c)
105   Ry=radians(30)
106   xc=85
107   yc=40
108   zc=20
109   plotboxy(xc,yc,zc,Ry)
110
111   #——————————————————————–Rz box (d)
112   Rz=radians(30)
113   xc=115
114   yc=40
115   zc=20
116   plotboxz(xc,yc,zc,Rz)
117
118   #————————————————————————-notes
119   plt.text(23,63,'(a)')
120   plt.text(53,63,'(b)')
121   plt.text(83,63,'(c)')
122   plt.text(112,63,'(d)')
123   plt.text(21,73,'R=0')
124   plt.text(47,73,'Rx=45°')
125   plt.text(77,73,'Ry=30°')
126   plt.text(107,73,'Rz=30°')
127   plt.arrow(42,40,25,0,head_width=2,head_length=3,color='r') #–red arrows
128   plt.arrow(42,40,28,0,head_width=2,head_length=3,color='r')
129   plt.arrow(85,25,0,27,head_width=2,head_length=2,color='r')
130   plt.arrow(85,25,0,29,head_width=2,head_length=2,color='r')
131   plt.plot([8,130],[8,8],color='k') #–axes
132   plt.plot([8,8],[8,85],color='k')
133   plt.text(120,6,'X')
134   plt.text(3,80,'Y')
135   plt.scatter(115,40,s=30,color='r') #———–red dot center of box (d)
136
137   plt.show()
Listing 3-1Program 4BOXES

3.7 围绕坐标方向的连续旋转

在清单 3-1 中,您操作了由第 14、15 和 16 行中的列表定义的盒子的初始角坐标。该程序产生了围绕 x、y 和 z 坐标方向的单独旋转。在本节中,您从相同的一组角坐标开始,但您按顺序旋转。即,在绕 x 方向(b)旋转 Rx 之后,旋转 Ry 被加到 Rx (c)的结果上。然后将 Rz 加到 Ry (d)的结果上。因此,旋转不像以前那样是独立的,而是相加的。这可以通过在每次旋转后用一组新的坐标替换第 14、15 和 16 行中的 x、y 和 z 定义来实现。也就是说,长方体的角坐标在每次旋转后都会更新,以便下一次旋转从更新后的坐标开始。这可以通过简单地修改清单 3-1 中第 72-88 行之间的函数 plotboxx、plotboxy 和 plotboxz 来实现。在清单 3-2 中,添加了 74b、80b 和 86b 行。在每次旋转后,它们通过用变换后的坐标 xg,yg,zg 替换初始的角坐标 x,y,z 来进行更新。该代码替换了清单 3-1 中的第 72-88 行。

A456962_1_En_3_Fig10_HTML.jpg

图 3-10

Sequential rotations of a box. Box (a) is rotated by Rx=30° to (b), then by an additional rotation of Ry=30° to (c), and then by an additional rotation of Rz=15° to (d). x and y axes show direction only. Coordinate values are indicated by the grid.

71
72   def plotboxx(xc,yc,zc,Rx):
73         for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
74                [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
74b               [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
75
76         plotbox(xg,yg,zg)
77
78   def plotboxy(xc,yc,zc,Ry):
79         for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
80                 [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
80b                [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
81
82         plotbox(xg,yg,zg)
83
84   def plotboxz(xc,yc,zc,Rz):
85         for i in (0,1,2,3,4,5,6,7): #————————–rotate eight corners
86                [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
86b               [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
87
88         plotbox(xg,yg,zg)
89
Listing 3-2Program 4BOXESUPDATE

变换参数在第 91-116 行由旋转 Rx、Ry 和 Rz 的值以及盒子中心坐标 xc、yc、zc 设置。

该程序中的旋转顺序是硬连线的,以产生图 3-10 ,首先是(a),然后是(b)、(c)和(d)。在一般程序中,旋转和中心坐标的顺序和值可以通过移动代码段或通过键盘输入顺序来设置。你将很快做到这两点。但首先,你要做圆周的连续旋转。

A456962_1_En_3_Fig11_HTML.jpg

图 3-11

Sequential rotations of a circle created by Listing 3-3. Circle (a) is rotated by Rx=45° to (b), then by an additional rotation of Ry=70° to (c), and then by an additional rotation of Rz=90° to (d). Red indicates the upper half of circle. x and y axes show direction only, not coordinate values, which are indicated by the grid.

清单 3-3 类似于清单 3-1 和 3-2 的前一个修改版本,其中您对一个盒子进行了顺序旋转。在那个程序中,盒子有八个角,每次旋转都要变换和更新。这里你有一个圆,它有更多的点要变换和更新。

在第 23-38 行中,您用围绕圆周的点的局部和全局坐标的起始值填充第 33 和 38 行之间的列表。它们间隔 dphi=5,如线 25 所示。如第 27 行所示,圆的半径为 10。空列表已经在第 14-20 行定义过了。随着从第 29 行开始的循环以角度φ围绕圆前进,第 30 至 32 行计算每个点的局部坐标。第 33-38 行使用 append()函数将坐标添加到列表中,该函数将元素添加到列表中。例如,通过循环线 33 的每个循环,将当前角度φ处的 xp 的局部值附加(添加)到 x 列表。因为此时您只是在填充列表,所以您可以使用 xp、yp、zp 来填充第 36-38 行的 xg、yg 和 zg 列表。注意,在这个圆的初始定义中,zp=0(程序行 32)。也就是说,圆在 x,y *面上开始是*的。随后的旋转将围绕该初始方向。

第 41-72 行像以前一样定义了转换函数。圆形绘图功能从第 75-86 行开始延伸。线条用于绘制圆。绘图循环从 78-82 运行。第 86 行在中心画了一个点。

使用 range(len(x))函数给出列表中元素的数量,而不是计算圆周围的点数。你可以用 x 的长度作为度量,因为所有的列表都有相同的长度。第 79-82 行绘制了上半部分的红色和下半部分的绿色。第 83-84 行更新了最后一个 xg any yg 全局坐标,以便像以前一样绘制线条时使用。由于在绘图时只使用了 xg 和 yg,所以这里不需要包含 zg。第 89-108 行像清单 3-1 和 3-2 中那样转换坐标。不同之处在于,这里你必须处理 len(x)长的列表,而以前你只有八个角。

  1   """
  2   SEQUENTIALCIRCLES
  3   """
  4
  5   import numpy as np
  6   import matplotlib.pyplot as plt
  7   from math import sin, cos, radians
  8
  9   plt.axis([0,150,100,0])
 10   plt.axis('on')
 11   plt.grid(True)
 12
 13   #——————————————————————define lists
 14   x=[]
 15   y=[]
 16   z=[]
 17
 18   xg=[]
 19   yg=[]
 20   zg=[]
 21
 22   #——————————————fill lists with starting coordinates
 23   phi1=radians(0)
 24   phi2=radians(360)
 25   dphi=radians(5) #–circumferential points spaced 5 degrees
 26
 27   r=10 #–circle's radius
 28
 29   for phi in np.arange(phi1,phi2+dphi,dphi): #–establish coordinates of circumferential points
 30         xp=r*cos(phi)
 31         yp=r*sin(phi)
 32   zp=0
 33   x.append(xp)   #–fill lists
 34   y.append(yp)
 35   z.append(zp)
 36   xg.append(xp)
 37   yg.append(yp)
 38   zg.append(zp)
 39
 40   #—————————————————–define rotation functions
 41   def rotx(xc,yc,zc,xp,yp,zp,Rx):
 42        a=[xp,yp,zp]
 43        b=[1,0,0] #———————————-[cx11,cx12,cx13]
 44        xpp=np.inner(a,b) #—–scalar product of a,b=xp*cx11+yp*cx12+ zp*cx13
 45        b=[0,cos(Rx),-sin(Rx)] #—————[cx21,cx22,cx23]
 46        ypp=np.inner(a,b)
 47        b=[0,sin(Rx),cos(Rx)] #—————[cx31,cx32,cx33]
 48        zpp=np.inner(a,b)
 49        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 50        return[xg,yg,zg]
 51
 52   def roty(xc,yc,zc,xp,yp,zp,Ry):
 53        a=[xp,yp,zp]
 54        b=[cos(Ry),0,sin(Ry)] #——————–[cx11,cx12,cx13]
 55        xpp=np.inner(a, b)
 56        b=[0,1,0] #—————[cx21,cx22,cx23]
 57        ypp=np.inner(a,b) #——————–scalar product of a,b
 58        b=[-sin(Ry),0,cos(Ry)] #—————[cx31,cx32,cx33]
 59        zpp=np.inner(a,b)
 60        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 61        return[xg,yg,zg]
 62
 63   def rotz(xc,yc,zc,xp,yp,zp,Rz):
 64        a=[xp,yp,zp]
 65        b=[cos(Rz),-sin(Rz),0] #——————-[cx11,cx12,cx13]
 66        xpp=np.inner(a, b)
 67        b=[sin(Rz),cos(Rz),0] #—————[cx21,cx22,cx23]
 68        ypp=np.inner(a,b)
 69        b=[0,0,1] #—————[cx31,cx32,cx33]
 70        zpp=np.inner(a,b) #———————scalar product of a,b
 71        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 72        return[xg,yg,zg]
 73
 74   #——————————————————define circle plotting function
 75   def plotcircle(xg,yg,zg):
 76        lastxg=xg[0]
 77        lastyg=yg[0]
 78        for i in range(len(x)): #—–len(x)=length of all lists
 79              if i < len(x)/2: #—–half green
 80                     plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1,color='g')
 81              else:
 82                     plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1,color='r')
 83        lastxg=xg[i]
 84        lastyg=yg[i]
 85
 86        plt.scatter(xc,yc,s=5) #–plot a dot at the center
 87
 88   #———————————————–transform coordinates and plot
 89   def plotcirclex(xc,yc,zc,Rx): #—————-transform & plot Rx circle
 90       for i in range(len(x)): #–for i in range(len(x)): ok too
 91              [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
 92              [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
 93
 94       plotcircle(xg,yg,zg) #—————plot
 95
 96   def plotcircley(xc,yc,zc,Ry):
 97        for i in range(len(x)): #—————–transform & plot Ry circle
 98              [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
 99              [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
100
101        plotcircle(xg,yg,zg)
102
103   def plotcirclez(xc,yc,zc,Rz):
104        for i in range(len(x)): #—————–transform &  plot Rz circle
105              [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
106              [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
107
108        plotcircle(xg,yg,zg)
109
110   #——————————————————————plot circles
111   Rx=radians(0)
112   xc=25 #—————circle (a) center coordinates
113   yc=40
114   zc=20
115   plotcirclex(xc,yc,zc,Rx) #–since R=0 we could use plotcircley or plotcirclez
116
117   #—————————————————————–Rx circle (b)
118   Rx=radians(45)
119   xc=55
120   yc=40
121   zc=20
122   plotcirclex(xc,yc,zc,Rx)
123
124   #—————————————————————–Ry circle (c)
125   Ry=radians(70)
126   xc=85
127   yc=40
128   zc=20
129   plotcircley(xc,yc,zc,Ry)
130
131   #—————————————————————–Rz circle (d)
132   Rz=radians(90)
133   xc=115
134   yc=40
135   zc=20
136   plotcirclez(xc,yc,zc,Rz)
137
138   #——————————————————————-notes
139   plt.text(23,63,'(a)')
140   plt.text(53,63,'(b)')
141   plt.text(83,63,'(c)')
142   plt.text(112,63,'(d)')
143   plt.text(21,73,'R=0')
144   plt.text(47,73,'Rx=45
°
')
145   plt.text(77,73,'Ry=70
°
')
146   plt.text(107,73,'Rz=90
°
')
147   plt.arrow(42,40,25,0,head_width=2,head_length=3,color='r') #–red arrows
148   plt.arrow(42,40,28,0,head_width=2,head_length=3,color='r')
149   plt.arrow(85,25,0,27,head_width=2,head_length=2,color='r')
150   plt.arrow(85,25,0,29,head_width=2,head_length=2,color='r')
151   plt.plot([8,130],[8,8],color='k') #–axes
152   plt.plot([8,8],[8,85],color='k')
153   plt.text(120,6,'X')
154   plt.text(3,80,'Y')
155   plt.scatter(115,40,s=30,color='r') #———–red dot center of box (d)
156
157   plt.show()
Listing 3-3Program SEQUENTIALCIRCLES

3.8 矩阵串联

将图 3-12 与图 3-11 进行比较,您可以看到,虽然 Rx、Ry 和 Rz 在两个图中具有相同的值,但是(c)和(d)中的圆的方向是不同的。这是因为图 3-11 中的旋转顺序是 Rx、Ry、Rz,而图 3-12 中的旋转顺序是 Rx、Rz、Ry。显然,轮换的顺序很重要。

A456962_1_En_3_Fig12_HTML.jpg

图 3-12

Circle (a) is rotated sequentially by Rx=45° to (b), then by an additional rotation of Rz=90° to (c), followed by an additional rotation of Ry=70 to (d). Red indicates the lower half of circle. x and y axes show direction only, not coordinate values, which are indicated by the grid.

你可以自己证明这一点。拿一本书,正面朝上放在桌子边上,顶部朝右。想象桌子的边缘是从左到右的 x 方向。接下来,绕 x 方向旋转 90 度,再绕 z 方向旋转 90 度。这是 RxRz。这本书会倒过来,正面对着你。然后颠倒顺序,先绕 z 方向旋转,再绕 x 方向旋转。这是 RzRx。正如你所看到的,在这两种情况下,你会得到这本书不同的最终方向。

虽然您已经通过在程序代码中排序和更新旋转坐标来执行顺序旋转,但从数学上讲,这相当于矩阵乘法。例如,下面的等式产生矢量[P]的旋转 Rx,随后是旋转 Rz。两次旋转产生矢量[P′]。

$$ \left[{P}^{\prime}\right]=\left[ Rz\right]\kern0.1em \left[ Rx\right]\kern0.1em \left[P\right] $$

(3-64)

[Rx]对向量[P]进行运算,[Rz]然后对[Rx][P]的结果进行运算。按 Rz 依次旋转 Rx、

$$ \left[{P}^{\prime}\right]=\left[ Rx\right]\kern0.1em \left[ Rz\right]\kern0.1em \left[P\right] $$

(3-65)

一般来说,

$$ \left[ Rx\right]\kern0.1em \left[ Rz\right]\ne \left[ Rz\right]\kern0.1em \left[ Rx\right] $$

(3-66)

您可以通过一个使用二维矩阵的简单示例来说明这一点。考虑 A 和 B 两个矩阵,其中

$$ \left[A\right]=\left[\begin{array}{cc}a& b\ {}c& d\end{array}\right] $$

(3-67)

$$ \left[B\right]=\left[\begin{array}{cc}e& f\ {}g& h\end{array}\right] $$

(3-68)

$$ AB=\left[\begin{array}{cc}a& b\ {}c& d\end{array}\right]\kern0.1em \left[\begin{array}{cc}e& f\ {}g& h\end{array}\right]=\left[\begin{array}{cc} ae+ bg& af+ bh\ {} ce+ dg& cf+ dh\end{array}\right] $$

(3-69)

$$ BA=\left[\begin{array}{cc}e& f\ {}g& h\end{array}\right]\kern0.1em \left[\begin{array}{cc}a& b\ {}c& d\end{array}\right]=\left[\begin{array}{cc} ae+ cf& be+ df\ {} ag+ ch& bg+ dh\end{array}\right] $$

(3-70)

$$ \therefore AB\ne BA $$

(3-71)

对于仅围绕三个不同坐标方向的三次旋转,有六种可能的变换序列组合:

$$ RxRyRz $$

【3-72】

$$ RxRzRy $$

【3-73】

$$ RyRxRz $$

【3-74】

$$ RyRzRx $$

【3-75】

$$ RzRxRy $$

【3-76】

$$ RzRyRx $$

【3-77】

这些组合中的每一个都包括三次单独的旋转。您可以将方程 3-55 、 3-56 和 3-57 中所示的三个变换矩阵相乘,以获得每种组合的单个变换矩阵。然后你可以写一个程序来执行这些组合:选择一个组合,输入三个角度,然后得到最终的旋转。但是如果你想要三次以上的旋转,比如 RyRzRxRyRz 呢?那将需要大量的矩阵乘法!很明显,通过将序列编码到 Python 程序中并在每次转换后更新坐标,可以更容易地合并序列,正如您在这里所学的那样。

为了生成图 3-12 ,清单 3-3 的第 110-136 行被清单 3-4 中的代码替换。

109
110   #——————————————————————plot circles
111   Rx=radians(0)
112   xc=25 #—————circle (a) center coordinates
113   yc=40
114   zc=20
115   plotcirclex(xc,yc,zc,Rx) #–since R=0 we could use plotcircley or plotcirclez
116
117   #—————————————————————–Rx circle (b)
118   Rx=radians(45)
119   xc=55
120   yc=40
121   zc=20
122   plotcirclex(xc,yc,zc,Rx)
123
124   #—————————————————————–Rz circle (d)
125   Rz=radians(90)
126   xc=85
127   yc=40
128   zc=20
129   plotcirclez(xc,yc,zc,Rz)
130
131   #—————————————————————–Ry circle (c)
132   Ry=radians(70)
133   xc=115
134   yc=40
135   zc=20
136   plotcircley(xc,yc,zc,Ry)
137
Listing 3-4Program SEQUENTIALCIRCLESUPDATE

这里,您执行了操作 RxRzRy,颠倒了最后两个转换的顺序。圆圈(a)如前所述绘制,Rx=0,在第 111 行。同样如前所述,接下来绘制圆(b ), Rx = 45 度,在线 118 中。不同之处在于第 124-136 行,其中旋转 Ry 和 Rz 颠倒,Rz 在 Ry 之前绘制。角度值与之前相同。重新排列绘图顺序很容易;只需剪切和粘贴部分代码。但是一定要更新中心坐标 xc、yc 和 zc。您可以通过引入 input()函数使程序更加用户友好,这将使您能够通过键盘输入转换的顺序。然后,您可以输入旋转 Rx、Ry 或 Rz,以及任意顺序的数量和中心坐标。接下来你会这么做。

3.9 具有功能程序结构的键盘数据输入

正如你在矩阵连接的讨论中看到的,重新安排程序中的循环顺序是一个有用的选择。然而,正如您将在本节中看到的,通过键盘输入数据更令人满意。您还将使用函数式编程结构,其中几行代码控制执行各种操作的各种预定义函数。这将使你在控制程序时有很大的灵活性。

列表 3-5 产生如图 3-13 至 3-16 所示的结果。第一张图显示了绕 x 方向旋转 0°的圆;第二个绕 y 方向 60°;第三个绕 x 方向 45°;第四个绕 z 方向旋转 90°。所有旋转都将添加到圆的先前方向。旋转轴和数量通过键盘输入。旋转方向的顺序无关紧要,旋转的次数也无关紧要。

参考清单 3-5 ,第 111-113 行指定了圆的中心坐标。所有的圆都有相同的中心坐标。第 115 行中的 while True:语句保持数据输入循环运行,因此您可以进行无限次数的顺序循环。第 116 行要求您在 Spyder 输出窗格中指定旋转轴。用小写字母输入 x、y 或 z。要退出循环,按回车键。(重要提示:如果您使用的是 Spyder 控制台,请确保在输入任何内容之前,在“输出”窗格中用光标单击鼠标。如果你忘记并把它留在程序窗格中,你可能会在程序的某个地方得到一个不需要的 x、y 或 z。如果发生这种情况,请转到屏幕顶部,打开一个新的控制台。这实质上是重新开始程序。).如果输入 x(小写),第 118 行要求输入旋转角度 r x。以正角或负角的形式输入。input()函数返回一个字符串。float 命令将其转换为浮点数。第 119 行然后调用函数 plotcirclex(),该函数绘制旋转的圆。Ry 和 Rz 旋转以类似的方式执行。请注意,旋转的顺序或次数没有限制。第 126 行检查您是否为 axis 输入了一个空白,在这种情况下,第 127 行退出程序。所有的圆都围绕同一个中心 xc,yc,zc 旋转。如果希望能够移动每个圆的中心,只需在第 115 行和第 116 行之间添加输入()行作为中心坐标。

第 89-108 行旋转并更新圆周点的坐标,如清单 3-3 所示。在函数 plotcircle()中,第 71-86 行进行绘图。每次调用该函数时,轴和网格都会被重新绘制。第 86 行显示了最新的情节。

这个程序是程序控制的一个重要例证。仅仅 115 和 127 之间的几行就控制了程序的整个操作,并在控制操作顺序和使用的数据方面提供了很大的灵活性。在其他编程语言中,如 Basic 和 Fortran,这被称为自顶向下编程。在这些语言中,相当于 Python 函数的子例程通常放在底部,而控制代码放在顶部。在 Python 中,通常将函数放在顶部,控件放在底部,这种风格称为自底向上编程。无论控制是在顶部还是底部,这种程序结构都被称为函数式编程,因为控制代码使用函数来执行各种操作。由于控制数据是通过键盘输入的,它提供了相当大的灵活性。

A456962_1_En_3_Fig16_HTML.jpg

图 3-16

The previous circle is rotated around the z axis by 90°

A456962_1_En_3_Fig15_HTML.jpg

图 3-15

The previous circle is rotated around the y axis by 45°

A456962_1_En_3_Fig14_HTML.jpg

图 3-14

The previous circle is rotated around the y axis by 60°

A456962_1_En_3_Fig13_HTML.jpg

图 3-13

The circle is rotated around the x axis by 0°

  1   """
  2   KEYBOARDDATAENTRY
  3   """
  4
  5   import numpy as np
  6   import matplotlib.pyplot as plt
  7   from math import sin, cos, radians
  8
  9   #——————————————————————-define  lists
 10   x=[]
 11   y=[]
 12   z=[]
 13
 14   xg=[]
 15   yg=[]
 16   zg=[]
 17
 18   #——————————————fill lists with starting coordinates
 19   phi1=radians(0)
 20   phi2=radians(360)
 21   dphi=radians(5) #–circumferential points spaced 5 degrees
 22
 23   radius=15 #–circle's radius
 24
 25   for phi in np.arange(phi1,phi2+dphi,dphi): #–establish coordinates of circumferential points
 26         xp=radius*cos(phi)
 27         yp=radius*sin(phi)
 28         zp=0
 29         x.append(xp) #–fill lists
 30         y.append(yp)
 31         z.append(zp)
 32         xg.append(xp)
 33         yg.append(yp)
 34         zg.append(zp)
 35
 36   #—————————————————–define rotation functions
 37   def rotx(xc,yc,zc,xp,yp,zp,Rx):
 38        a=[xp,yp,zp]
 39        b=[1,0,0] #———————————-[cx11,cx12,cx13]
 40        xpp=np.inner(a,b) #—–scalar product of a,b=xp*cx11+yp*cx12+ zp*cx13
 41        b=[0,cos(Rx),-sin(Rx)] #—————[cx21,cx22,cx23]
 42        ypp=np.inner(a,b)
 43        b=[0,sin(Rx),cos(Rx)] #—————[cx31,cx32,cx33]
 44        zpp=np.inner(a,b)
 45        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 46        return[xg,yg,zg]
 47
 48   def roty(xc,yc,zc,xp,yp,zp,Ry):
 49        a=[xp,yp,zp]
 50        b=[cos(Ry),0,sin(Ry)] #——————–[cx11,cx12,cx13]
 51        xpp=np.inner(a, b)
 52        b=[0,1,0] #—————[cx21,cx22,cx23]
 53        ypp=np.inner(a,b) #——————–scalar product of a,b
 54        b=[-sin(Ry),0,cos(Ry)] #—————[cx31,cx32,cx33]
 55        zpp=np.inner(a,b)
 56        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 57        return[xg,yg,zg]
 58
 59   def rotz(xc,yc,zc,xp,yp,zp,Rz):
 60        a=[xp,yp,zp]
 61        b=[cos(Rz),-sin(Rz),0] #——————-[cx11,cx12,cx13]
 62        xpp=np.inner(a, b)
 63        b=[sin(Rz),cos(Rz),0] #—————[cx21,cx22,cx23]
 64        ypp=np.inner(a,b)
 65        b=[0,0,1] #—————[cx31,cx32,cx33]
 66        zpp=np.inner(a,b) #———————scalar product of a,b
 67        [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
 68        return[xg,yg,zg]
 69
 70   #———————————————–define circle plotting function
 71   def plotcircle(xg,yg,zg):
 72        lastxg=xg[0]
 73        lastyg=yg[0]
 74        for i in range(len(x)): #–for i in range(len(x)): ok too
 75               if i < len(x)/2: #—–half green
 76                      plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1  ,color='g')
 77               else:
 78                     plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1  ,color='r')
 79        lastxg=xg[i]
 80        lastyg=yg[i]
 81
 82        plt.scatter(xc,yc,s=5,color='k') #–plot a dot at the center
 83        plt.axis([0,150,100,0]) #–replot axes and grid
 84        plt.axis('on')
 85        plt.grid(True)
 86        plt.show() #–plot latest rotation
 87
 88   #————————————————transform coordinates and plot
 89   def plotcirclex(xc,yc,zc,Rx): #————-transform and plot Rx circle
 90        for i in range(len(x)):
 91               [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
 92               [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
 93
 94        plotcircle(xg,yg,zg) #—————plot
 95
 96   def plotcircley(xc,yc,zc,Ry):
 97         for i in range(len(x)): #—————–transform and plot Ry circle
 98                [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
 99                [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
100
101        plotcircle(xg,yg,zg)
102
103   def plotcirclez(xc,yc,zc,Rz):
104        for i in range(len(x)): #—————–transform and plot Rz circle
105               [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
106               [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
107
108        plotcircle(xg,yg,zg)
109
110   #——————————————————————plot circles
111   xc=75 #–center coordinates
112   yc=50
113   zc=50
114
115   while True:
116        axis=input('x, y or z?: ') #–input axis of rotation (lower case)
117        if axis == 'x': #–if x axis
118               Rx=radians(float(input('Rx degrees?: ')))
119               plotcirclex(xc,yc,zc,Rx) #–call function plotcirclex
120        if axis == 'y':
121              Ry=radians(float(input('Ry degrees?: ')))
122              plotcircley(xc,yc,zc,Ry)
123        if axis == 'z':
124               Rz=radians(float(input('Rz degrees?: ')))
125               plotcirclez(xc,yc,zc,Rz)
126        if axis == ":
127               break
Listing 3-5Program KEYBOARDDATAENTRY

3.10 摘要

在本章中,您学习了如何构建三维坐标轴和三维形状,并围绕三个坐标方向旋转它们。这涉及到围绕三个坐标方向的旋转变换的推导。您看到了将对象从其原始方向旋转一次与按顺序旋转之间的区别,在顺序旋转中,每次后续旋转都使用对象在前一次旋转中的坐标作为起点。您探讨了轮换顺序很重要的观点;Rx,Ry,Rz 不会产生与 Rx,Rz,Ry 相同的结果。这是通过矩阵串联显示的。最后,您开发了一个程序,可以通过键盘输入顺序旋转,而不是在程序中指定它们。所有这些工作都涉及到列表的使用。

四、视角

我在前一章讨论了等距视图和透视视图。现在,您将开发一个能够自动生成透视图的转换。它的操作很像一台照相机,光线从组成一个物体的各个点被追踪到一个*面上,你可能会认为这是一个电影*面。图 4-1 显示了几何形状。它是 x,y,z 空间中的一个三维盒子。x,y *面代表胶片*面。还有一个焦点,在 x,y,z 空间之外,在 x,y *面的前面。光线从盒子的角落追踪到焦点。通过连接光线到达 x,y *面的点,可以构建长方体的透视图。

A456962_1_En_4_Fig1_HTML.jpg

图 4-1

Geometry used to project a perspective image of an object on the x,y plane

如图 4-2 所示,在不透明的薄片上打一个小孔,就可以构造出一个原始的相机。从一个物体发出的光线穿过这个孔,会在“胶片*面”上产生一个类似照片的透视图像。除了在计算机屏幕上描绘图像之外,你将在这一章中产生的透视变换将以有点类似的方式操作。除了针孔几何形状产生反转图像之外,该几何形状在几何形状上是相似的。如果焦点在-z 方向上向后移动很远,来自物体的光线变得几乎*行,透视效果就丧失了;图像变得扁*。摄影师用长焦距镜头拍摄时,这种现象是众所周知的。

A456962_1_En_4_Fig2_HTML.jpg

图 4-2

Pinhole camera vs. computer projection geometry

图 4-3 和 4-4 显示了您将用于构建转换的几何图形。图 4-3 显示了 x、y、z 空间内的三维物体。焦点在全局坐标(xfp,yfp,zfp)的空间之外。它可以在 x,y *面(-z 方向)前面的任何地方。不同的位置会产生物体的不同视图,就像照相机从不同的位置拍摄物体时会产生不同的图像一样。

从盒子四角发出的假想射线穿过 x,y *面,你可以想象这是你的电脑屏幕。由于 x,y *面位于 z=0 处,因此每条射线在到达焦点 zh=0 的途中都会在撞击点(xh,yh,zh=0)处撞击 x,y *面。连接由来自组成物体的点的射线产生的击中点将产生透视图像。

对象上的典型点位于(x,y,z)处。点和焦点之间的距离是 q,Qh 是焦点到命中点的距离。|zfp|+z 是从焦点到物点的水*距离。|z|是从焦点到击中点的水*距离。③是从焦点指向物点的单位矢量。利用这个几何图形,可以推导出以下关系:

$$ a=x- xfp $$

(4-1)

$$ b=y- yfp $$

(4-2)

$$ c=z+\left| zfp\right| $$

(4-3)

因为,在方程 4-3 中 zfp 是负的(它位于 x,y *面的前面),你使用它的绝对值|zfp|因为它加到 z 上给出焦点和物点之间的总 z 方向距离。当然,你可以将方程 4-3 写成 c=z-zfp,这是等价的,但是绝对值|zfp|的使用使得下面的分析更容易理解。同样,如果你忘记并为 zfp 输入一个正的 z 值也没关系。

$$ Q=\sqrt{a²+{b}²+{c}²} $$

【4-4】

$$ ux=a/Q $$

【4-5】

$$ uy=b/Q $$

【4-6】

$$ uz=c/Q $$

【4-7】

$$ \widehat{\mathbf{u}}= ux\widehat{\mathbf{i}}+ uy\widehat{\mathbf{j}}+ uz\widehat{\mathbf{k}} $$

【4-8】

$$ Qh=\frac{Q;\left|zfp\right|}{z+\left|zfp\right|} $$

【4-9】

$$ xh=ux;Qh+xfp $$

【4-10】

$$ yh=uy;Qh+yfp $$

【4-11】

$$ zh=0 $$

您可以通过以下方式显示 zh=0(即命中点位于 x,y *面上,正如它应该的那样):

$$ \left|zh=uz;Qh-\left|zfp\right|\right. $$

(4-13)

$$ =\frac{c}{Q};Qh-\left|zfp\right| $$

(4-14)

$$ =\left(z+\left|zfp\right|\right);\frac{Qh}{Q}-\left|zfp\right| $$

(4-15)

$$ =\frac{\left(z+\left|zfp\right|\right)}{Q};\frac{Q\left|zfp\right|}{\left(z+\left|zfp\right|\right)}-\left|zfp\right| $$

(4-16)

$$ =\left| zfp\right|-\left| zfp\right| $$

(4-17)

$$ =0 $$

(4-18)

等式 4-13 中的负号是因为|zfp|总是正的,而你知道焦点总是在-z 位置。

A456962_1_En_4_Fig4_HTML.jpg

图 4-4

Perspective image projection geometry side view

A456962_1_En_4_Fig3_HTML.jpg

图 4-3

Perspective image projection geometry

清单 4-1 展示了上述模型的使用。它使您能够构建一个对象,旋转它,然后以透视的方式查看它。第 14-29 行定义了对象,在这个例子中是一所房子。第 14-16 行建立了局部坐标中的角坐标 x,y,z;也就是说,相对于在第 18-20 行中设置的点 xc,yc,zc。这是房子的中心,也是旋转的中心。第 22-29 行通过向第 22-24 行中的空列表集合添加元素,将 x,y,z 转换为全局坐标 xg,yg,zg。第 31-47 行通过用线连接角点来绘制房子。

第 50-63 行定义了一个关于 xc,yc,zc 旋转本地坐标的函数,将结果保存为 xg,yg,zg。它使用函数 roty,该函数在第 54-63 行中定义。这个函数在以前的程序中使用过。它是这个程序中唯一的旋转函数,也就是说你只能绕着 y 方向旋转。接下来是视角转换视角(xfp,yfp,zfp);它实现了上面开发的等式 4-1 到 4-12 。从第 67 行开始的循环计算从对象的每个角点到焦点的光线的击中点的坐标。用全局坐标表示的命中点保存在第 79-81 行。

程序的控制发生在第 83-95 行。第 83-85 行定义了焦点的位置;第 87-89 行房子的中心点。第 91 行的 Ry 指定了围绕 y 方向的旋转角度。第 93 行然后调用函数 plothouse(xc,yc,zc,Ry),它旋转房子。第 94 行调用 perspective(xfp,yfp,zfp),它执行透视转换。第 95 行绘制了房子。这可以包含在 function 透视图中,但是放在这里是为了说明操作的顺序。

1   """
2   PERSPECTIVE
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sin, cos, radians
8
9   plt.axis([0,150,100,0])
10
11  plt.axis('on')
12  plt.grid(True)
13
14  x=[-20,-20,20,20,-20,-20,20,20,-20,20] #——–object local corner coordinates
15  y=[-10,-10,-10,-10,10,10,10,10,-20,-20]
16  z=[5,-5,-5,5,5,-5,-5,5,0,0]
17
18  xc=30 #———————————————object center coordinates
19  yc=50
20  zc=10
21
22  xg=[ ] #———————————————object global coordinates
23  yg=[ ]
24  zg=[ ]
25
26  for i in np.arange(len(x)):
27        xg.append(x[i]+xc)
28        yg.append(y[i]+yc)
29        zg.append(z[i]+zc)
30
31  #————————————–plot object
32  def plothouse(xg,yg,zg):
33       plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k')
34       plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k')
35       plt.plot([xg[4],xg[7]],[yg[4],yg[7]],color='k')
36       plt.plot([xg[5],xg[6]],[yg[5],yg[6]],color='k')
37       plt.plot([xg[8],xg[9]],[yg[8],yg[9]],color='k')
38       plt.plot([xg[4],xg[0]],[yg[4],yg[0]],color='k')
39       plt.plot([xg[5],xg[1]],[yg[5],yg[1]],color='k')
40       plt.plot([xg[6],xg[2]],[yg[6],yg[2]],color='r')
41       plt.plot([xg[7],xg[3]],[yg[7],yg[3]],color='r')
42       plt.plot([xg[0],xg[8]],[yg[0],yg[8]],color='k')
43       plt.plot([xg[1],xg[8]],[yg[1],yg[8]],color='k')
44       plt.plot([xg[2],xg[9]],[yg[2],yg[9]],color='r')
45       plt.plot([xg[3],xg[9]],[yg[3],yg[9]],color='r')
46       plt.plot([xg[4],xg[5]],[yg[4],yg[5]],color='k')
47       plt.plot([xg[6],xg[7]],[yg[6],yg[7]],color='r')
48
49  #——————————————rotate object about the Y direction
40  def plothousey(xc,yc,zc,Ry):
51       for i in range(len(x)): #—————rotate 10 corners
52             [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
53
54  def roty(xc,yc,zc,x,y,z,Ry):
55       a=[x,y,z]
56       b=[cos(Ry),0,sin(Ry)]
57       xpp=np.inner(a,b)
58       b=[0,1,0]
59       ypp=np.inner(a,b)
60       b=[-sin(Ry),0,cos(Ry)]
61       zpp=np.inner(a,b)
62       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
63       return [xg,yg,zg]
64
65  #—————————————————————————————————————perspective transformation
66  def perspective(xfp,yfp,zfp):
67       for i in range(len(x)):
68             a=xg[i]-xfp
69             b=yg[i]-yfp
70             c=zg[i]+abs(zfp)
71             q=np.sqrt(a*a+b*b+c*c)
72             ux=a/q
73             uy=b/q
74             uz=c/q
75             qh=q*abs(zfp)/(zg[i]+abs(zfp))
76             xh=ux*qh+xfp
77             yh=uy*qh+yfp
78             zh=0
79             xg[i]=xh
80             yg[i]=yh
81             zg[i]=zh
82
83  xfp=80 #—————————————————————————focal point coordinates
84  yfp=50
85  zfp=-100
86
87  xc=80 #——————————————redefine center coordinates
88  yc=50
89  zc=50
90
91  Ry=radians(45)  #—————————————————————angle of rotation
92
93  plothousey(xc,yc,zc,Ry)               #—-rotate
94  perspective(xfp,yfp,zfp)              #—-transform
95  plothouse(xg,yg,zg)                   #—-rotate
96
97  plt.show()
Listing 4-1Program PERSPECTIVE

图 4-5 至 4-8 显示了列表 4-1 的输出。图 4-5 显示了未旋转(Ry = 0°)方向的房屋。右边是红色的。焦点在 xc=80,yc=50,-100。这与房子的中心成一直线,但是在 x,y *面的前面 100 度。图 4-6 显示房屋绕 y 方向旋转 45 度。透视效果很明显。图 4-7 显示了具有相同设置的房屋,但是焦点从 zfp=-100 移回到 zfp=-600。你可以看到图像是如何变*的,透视效果大部分丢失。图 4-8 显示了一些随机设置的房屋。按照清单 4-1 中的步骤,你应该能够很容易地创建一个更复杂的场景。

A456962_1_En_4_Fig8_HTML.jpg

图 4-8

Perspective image with Ry=-60, zfp=-100, xc=40, yc=70, xfp=100, zfp=-80

A456962_1_En_4_Fig7_HTML.jpg

图 4-7

Perspective image with Ry=45, zfp=-600

A456962_1_En_4_Fig6_HTML.jpg

图 4-6

Perspective image with Ry=45, zfp=-100

A456962_1_En_4_Fig5_HTML.jpg

图 4-5

Perspective image with Ry=0, zfp=-100

问题是,焦点放在哪里。如果你把图像投影到 x,y *面上,显然它应该在那个*面的前面(即-z 方向)。但是焦点的 x,y 坐标呢?最好的结果,最像人眼看到的,是把它放在与房子中心相同的 x,y 坐标上。当然,如果模型中有很多物体,比如更多的房屋和树木,那么在哪里放置焦点就不明显了。将它放置在 x,y *面的前面,在对应于模型*似中心的坐标处,将获得最佳结果。这类似于将相机对准要拍摄的场景的中心。画家维米尔在他的许多画作中选择了这种结构。事实上,在他的一些画布上,艺术史学家在消失点发现了一个钉孔,所有*行线如房间角落和地砖都在这里交汇。钉孔大约在场景的中心。人们相信他在钉子上绑了一根线,用它来追踪聚合线,就像你在算法中使用线一样。你可以在弗米尔的许多室内画中看到这种结构。

4.1 总结

在本章中,您学习了如何构建透视图。几何体基于简单的盒式摄像机。你把透视图像投影到 x,y *面上。你可以使用任何其他坐标*面,例如 x,z *面;几何形状是相似的。你探讨了焦点放在哪里的问题,焦点对应于观察者或相机的观察点。答案是,除非你在寻找一个不寻常的图像,在模型的大致中心。这是弗米尔在他的许多画中使用的结构。

五、相交

在这一章中,你将开发算法,告诉你线和*面与各种物体相交的位置。当您删除隐藏线和跟踪对象投射的阴影时,您开发的技术将会很有用。您还将学习如何用球体显示直线和*面的交点。正如您将看到的,没有一种神奇的算法可以满足所有情况;每一种都需要自己的方法。虽然您可能永远不需要这些算法中的一些,例如与扇形相交的直线,但是这些依赖于基于向量的几何的过程非常有趣,应该会为您提供在遇到不同情况时所需的工具。

除了使用向量,这些解中的许多可以通过分析得出。例如,与球体相交的直线的解可以通过将直线的方程与球体的方程结合起来得到。结果是一个二次方程,当求解时,产生入口和出口点。如果您处理的对象可以用简单的方程来表示,那么这种方法既快速又简单。然而,基于向量的过程虽然看起来更复杂,但实际上非常简单和直观。他们也可以更灵活,更能适应不同寻常的情况。您将在这里使用它们。

5.1 与矩形*面相交的直线

图 5-1 显示了与矩形*面相交的直线。你将开发算法和程序来寻找交点,称为命中点。这里你规定*面是有限的,但它不一定是有限的。通过分析,你会发现这里没有任何东西要求*面是有限的。你也可以从假设*面是矩形开始。它不一定是矩形的,但是现在,保持有限的矩形更容易。

A456962_1_En_5_Fig1_HTML.jpg

图 5-1

Geometry of a line intersecting a rectangular plane

*面在 0、1、2 和 3 处有角。这些相对于(xc,yc,zc)处的旋转中心具有(x0,y0,z0) - (x3,y3,z3)的局部坐标。该线从 x[4],y[4],z[4]开始,到 x[5],y[5],z[5]结束。它在命中点与*面相交。

角 0 处有三个单位向量;③、$$ \widehat{\mathbf{v}} $$$$ \widehat{\mathbf{n}} $$。单位矢量$$ \widehat{\mathbf{v}} $$从 0 角指向 1 角;③从 0 到 3。$$ \widehat{\mathbf{n}} $$是*面法线。$$ \widehat{\mathbf{l}} $$是指从 4 到 5 的直线上的单位向量。Q 45 是 4 到 5 的距离。Q h 是 4 到命中点的距离。Q n 是从 4 到*面的垂直距离。你的任务是确定击中点的位置(xh,yh,zh)。使用向量几何,您可以写出以下关系:

距离 4→5:$$ a=x\left[5\right]-x\left[4\right] $$(5-1)

$$ b=y\left[5\right]-y\left[4\right] $$

(5-2)

(5-3)

(5-4)

单位向量 4→5:

$$ lx=\frac{a}{Q_{45}} $$

(5-5)

$$ ly=\frac{b}{Q_{45}} $$

(5-6)

$$ lz=\frac{c}{Q_{45}} $$

(5-7)

$$ \widehat{\mathbf{I}}= lx\widehat{\mathbf{i}}+ ly\widehat{\mathbf{j}}+ lz\widehat{\mathbf{k}} $$

(5-8)

距离 0→3:$$ a=x\left[3\right]-x\left[0\right] $$(5-9)

$$ b=y\left[3\right]-y\left[0\right] $$

(5-10)

(5-11)

(5-12)

单位向量 0→3:

$$ ux=\frac{a}{Q_{03}} $$

(5-13)

$$ uy=\frac{b}{Q_{03}} $$

(5-14)

$$ uz=\frac{c}{Q_{03}} $$

(5-15)

$$ \widehat{\mathbf{u}}= ux\widehat{\mathbf{i}}+ uy\widehat{\mathbf{j}}+ uz\widehat{\mathbf{k}} $$

(5-16)

距离 0→1:$$ a=x\left[1\right]-x\left[0\right] $$(5-17)

$$ b=y\left[1\right]-y\left[0\right] $$

(5-18)

(5-19)

(5-20)

单位向量 0→1:

$$ vx=\frac{a}{Q_{01}} $$

(5-21)

$$ vy=\frac{b}{Q_{01}} $$

(5-22)

$$ vz=\frac{c}{Q_{01}} $$

(5-23)

$$ \widehat{\mathbf{v}}= vx\widehat{\mathbf{i}}+ vy\widehat{\mathbf{j}}+ vz\widehat{\mathbf{k}} $$

(5-24)

单位矢量$$ \widehat{\mathbf{n}} $$:

$$ \widehat{\mathbf{n}}=\widehat{\mathbf{u}}\times \widehat{\mathbf{v}} $$

(5-25)

$$ =\left[\begin{array}{ccc}\widehat{\mathbf{i}}& \widehat{\mathbf{j}}& \widehat{\mathbf{k}}\ {} ux& uy& uz\ {} vx& vy& vz\end{array}\right] $$

(5-26)

$$ \widehat{\mathrm{n}}=\widehat{\mathbf{i}}\underset{nx}{\underbrace{\left( uy\cdotp vz- uz\cdotp vy\right)}}+\widehat{\mathbf{j}}\underset{ny}{\underbrace{\left( uz\cdotp vx- ux\cdotp vz\right)}}+\widehat{\mathbf{k}}\underset{nz}{\underbrace{\left( ux\cdotp vy- uy\cdotp vx\right)}} $$

(5-27)

$$ \widehat{\mathbf{n}}= nx\widehat{\mathbf{i}}+ ny\widehat{\mathbf{j}}+ nz\widehat{\mathbf{k}} $$

(5-28)

$$ nx= uy\cdotp vz- uz\cdotp vy $$

(5-29)

$$ ny= uz\cdotp vx- ux\cdotp vz $$

(5-30)

$$ nz= ux\cdotp vy- uy\cdotp vx $$

(5-31)

向量 0→4:

$$ {\mathbf{V}}_{04}=v{x}_{04}\widehat{\mathbf{i}}+v{y}_{04}\widehat{\mathbf{j}}+v{z}_{04}\widehat{\mathbf{k}} $$

(5-32)

$$ v{x}_{04}=x\left[4\right]-x\left[0\right] $$

(5-33)

$$ v{y}_{04}=y\left[4\right]-y\left[0\right] $$

(5-34)

$$ v{z}_{04}=z\left[4\right]-z\left[0\right] $$

(5-35)

与*面垂直距离 4:

$$ {Q}_n=\left|{\mathbf{V}}_{04}\cdotp \widehat{\mathbf{n}}\right| $$

(5-36)

命中点:

$$ {Q}_n={Q}_h\kern0.1em \mathit{\cos}(p) $$

(5-37)

$$ {Q}_h=\frac{Q_n}{\mathit{\cos}(p)} $$

(5-38)

$$ \mathit{\cos}(p)=\widehat{\mathbf{l}}\cdotp \widehat{\mathbf{n}} $$

(5-39)

$$ = lx\cdotp nx+ ly\cdotp ny+ lz\cdotp nz $$

(5-40)

$$ xh=x\left[4\right]+{Q}_h lx $$

(5-41)

$$ yh=y\left[4\right]+{Q}_h ly $$

(5-42)

$$ zh=z\left[4\right]+{Q}_h lz $$

(5-43)

您可以通过测试来查看命中点是否位于*面的边界内。图 5-2 显示了几何形状。向量 V0h 从 0 角运行到命中点 h,up 和 vp 分别是 V0h 在 03 和 01 方向上的投影。为了测试入界或出界命中,

如果 up < 0 or up > Q03 命中出界

如果 vp < 0 or vp > Q01 命中出界

以 xh、yh、zh 为命中点 h 的坐标,可以计算出 up 和 vp 如下:

$$ a= xh-x\left[0\right] $$

(5-44)

$$ b= yh-y\left[0\right] $$

(5-45)

$$ c= zh-z\left[0\right] $$

(5-46)

$$ \mathbf{V}\mathbf{0}\mathbf{h}=a\widehat{\mathbf{i}}+b\widehat{\mathbf{j}}+c\widehat{\mathbf{k}} $$

(5-47)

A456962_1_En_5_Fig2_HTML.jpg

图 5-2

Out-of-bounds geometry

为了找到答案,你把 V0h 投影到 03 方向。要做到这一点,你需要 V0h 与:

$$ up=a\cdotp ux+b\cdotp uy+c\cdotp uz $$

(5-48)的点积

求 vp,你取 V0h 与$$ \widehat{\mathbf{v}} $$ :

$$ vp=a\cdotp vx+b\cdotp vy+c\cdotp vz $$

(5-49)的点积

如果你认为从 4 到 5 的线是有限的,你可以测试它是否足够长到*面。从图 5-1 、

$$ a= xh-x\left[4\right] $$

$$ b= yh-y\left[4\right] $$

、【5-51】、$$ c= zh-z\left[4\right] $$、、【5-52】、

、【5-53】如果 Q45 < Qh 线太短,没有命中

所有这些都被合并到清单 5-1 中,该清单与第三章中的清单 3-5 具有相同的结构,尽管一些功能和操作被改变了。在该程序中,旋转方向和旋转量通过键盘输入。轮换是相加的;例如,如果系统首先旋转 Rx=40 度,然后旋转 Rx=10 度,则总角度为 50 度。Ry 和 Rz 的操作类似。

清单 5-1 中的一些数据是硬连接的,例如矩形*面和与之相交的直线的定义。它们显示在第 18–20 行的列表中。每个列表中有六个元素,编号为[0]-[5]: [0]-[3]是*面的四个角,而[4]和[5]是直线的起点和终点。它们与图 5-1 和 5-2 中的图相配合。要修改*面和直线,只需在列表中输入新的数字。例如,项目[5]是该行的结尾。要在+y 方向下拉,增加 y[5]。列表中的数字是相对于旋转中心(xc,yc,zc)的局部坐标,旋转中心位于*面的中心。第 14-16 行显示了这些值。

定义一个*面只需要三个点。这里你有一个四角矩形*面。如果您改变*面的角坐标,请确保它们位于同一*面。最简单的方法是从位于或*行于其中一个坐标*面的*面开始。稍后可以将其旋转出该坐标*面。在第 19 行,y 列表的前四个元素都是零。它描述了一个在 y=0 时*行于 x,z *面的*面。此外,如果改变[x]或[z]列表,请确保*面保持矩形,因为在此分析中,命中点的计算假设是这样的。

围绕坐标方向旋转坐标的旋转函数 rotx、roty 和 rotz 包含在第 28-35 行中。它们与先前程序中使用的相同,因此没有列出。

线 45 在线与*面相交的命中点(xhg,yhg)处画出一个点。如果命中点位于*面的边界内,则圆点的颜色为红色;如果在外面,它就是蓝色的。如果从[4]到[5]的线太短,并且从未到达*面,则颜色变为绿色,并且在线的末端[5]处放置一个点。这如图 5-5 所示。命中点的计算由函数 hitpoint(x,y,z)执行,该函数从第 53 行开始。该程序遵循上述方程 5-1 至 5-49 中的分析,应该是不言自明的。

数据输入发生在行 154-166。这类似于清单 3-5 。输出样本如图 5-3 、 5-4 和 5-5 所示。参数包含在标题中。

A456962_1_En_5_Fig5_HTML.jpg

图 5-5

Example of a line too short, in which case a green dot appears at coordinate [5]: x[4]=-40, y[4]=-20, z[4]=15, x[5]=-20, y[5]=-10, z[5]=0, Rx=30°, Ry=45°, Rz°=20

A456962_1_En_5_Fig4_HTML.jpg

图 5-4

Line intersecting the plane defined by a rectangle. The hit point lies outside the rectangle’s boundaries: y[5]=-5, Rx=45°, Ry=45°, Rz°=20.

A456962_1_En_5_Fig3_HTML.jpg

图 5-3

Line intersecting the plane defined by a rectangle. The hit point lies within the plane’s boundaries: y[5]=+5, Rx=45°, Ry=45°, Rz°=20

1   """
2   LRP
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians,sqrt
8
9   #——————————————fill lists with starting coordinates
10  xg=[ ]
11  yg=[ ]
12  zg=[ ]
13
14  xc=80 #————————center coordinates
15  yc=40
16  zc=40
17
18  x=[-40,-40,40,40,-40,50] #—system (plane and line geometry)
19  y=[0,0,0,0,-20,3]
20  z=[-10,10,10,-10,15,-10]
21
22  for i in range(len(x)):
23        xg.append(x[i]+xc)
24        yg.append(y[i]+yc)
25        zg.append(z[i]+zc)
26
27  #——————————————————define  rotation  functions
28  def rotx(xc,yc,zc,xp,yp,zp,Rx):
29       (same as in prior programs)
30
31  def  roty(xc,yc,zc,xp,yp,zp,Ry):
32        (same as in prior programs)
33
34  def rotz(xc,yc,zc,xp,yp,zp,Rz):
35       (same as in prior programs)
36
37  #———————————————-plot  plane, line and hit point
38  def plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor):
39       plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k')  #—————plot plane
40       plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k')
41       plt.plot([xg[2],xg[3]],[yg[2],yg[3]],color='k')
42       plt.plot([xg[3],xg[0]],[yg[3],yg[0]],color='k')
43       plt.plot([xg[4],xg[5]],[yg[4],yg[5]],color='b') #———plot line
44
45       if hitcolor="g": #——————plot hit point at [5]
46             plot.scatter(xg[5],yg[5],s=20,color=hitcolor)
47       else: #——————plot hit point at h
48             plt.scatter(xhg,yhg,s=20,color=hitcolor)
49
50       plt.axis([0,150,100,0]) #———replot axes and grid
51       plt.axis('on')
52       plt.grid(False)
53       plt.show() #———plot latest rotation
54
55  #—————————————find hit point coordinates and color
56  def hitpoint(x,y,z):
57       a=x[5]-x[4]
58       b=y[5]-y[4]
59       c=z[5]-z[4]
60       Q45=sqrt(a*a+b*b+c*c)  #———distance  point  4  to  5
61
62       lx=a/Q45 #———unit vector components point 4 to 5
63       ly=b/Q45
64       lz=c/Q45
65
66       a=x[3]-x[0]
67       b=y[3]-y[0]
68       c=z[3]-z[0]
69       Q03=sqrt(a*a+b*b+c*c) #———distance 0 to 3
70
71       ux=a/Q03 #———unit vector 0 to 3
72       uy=b/Q03
73       uz=c/Q03
74
75       a=x[1]-x[0]
76       b=y[1]-y[0]
77       c=z[1]-z[0]
78       Q01=sqrt(a*a+b*b+c*c) #———distance 0 to 1
79
80       vx=a/Q01 #———unit vector 0 to 1
81       vy=b/Q01
82       vz=c/Q01
83
84       nx=uy*vz-uz*vy #———normal unit vector
85       ny=uz*vx-ux*vz
86       nz=ux*vy-uy*vx
87
88       vx1b=x[4]-x[0] #———vector components 0 to 4
89       vy1b=y[4]-y[0]
90       vz1b=z[4]-z[0]
91
92       Qn=(vx1b*nx+vy1b*ny+vz1b*nz) #———perpendicular distance 4 to plane
93
94       cosp=lx*nx+ly*ny+lz*nz #——cos of angle p
95       Qh=abs(Qn/cosp) #———distance 4 to hit point
96
97       xh=x[4]+Qh*lx  #———hit  point  coordinates
98       yh=y[4]+Qh*ly
99       zh=z[4]+Qh*lz
100
101      xhg=xh+xc #———global hit point coordinates
102      yhg=yh+yc
103      zhg=zh+zc
104
105  #————————————————————out of bounds check
106      a=xh-x[0] #——components of vector V0h
107      b=yh-y[0]
108      c=zh-z[0]
109
110      up=a*ux+b*uy+c*uz #———dot products
111      vp=a*vx+b*vy+c*vz
112
113      hitcolor='r' #———if inbounds plot red hit point
114      if up<0: #———change color to blue if hit point out of bounds
115           hitcolor='b'
116
117      if up>Q03:
118           hitcolor='b'
119
120      if vp<0:
121           hitcolor='b'
122
123      if vp>Q01:
124           hitcolor='b'
125
126      a=x[5]-x[4]
127      b=y[5]-y[4]
128      c=z[5]-z[4]
129      Q45=sqrt(a*a+b*b+c*c)
130
131      if Q45 < Qh:
132           hitcolor='g'
133
134      return xh,yh,xhg,yhg,hitcolor
135
136 #————————————————transform  coordinates  and  plot
137 def  plotx(xc,yc,zc,Rx):   #———transform  &  plot  Rx  system
138       for i in range(len(x)):
139             [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
140             [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
141
142       xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z) #———returns xh,yh,xhg,yhg
143
144       plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor) #———plot
145
146 def ploty(xc,yc,zc,Ry):  #———transform & plot Ry system
147      for i in range(len(x)):
148            [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
149            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
150
151      xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z)
152
153      plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor)
154
155 def plotz(xc,yc,zc,Rz):   #———transform  &  plot  Rz  system
156      for i in range(len(x)):
157            [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
158            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
159
160      xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z)
161
162      plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor)
163
164 #—————————————————-input data and plot system
165 while True:
166      axis=input('x, y or z?: ') #———input axis of rotation (lower case)
167      if axis == 'x': #—if x axis
168            Rx=radians(float(input('Rx Degrees?: '))) #———input degrees
169            plotx(xc,yc,zc,Rx) #–call function plotx
170      if axis == 'y':
171            Ry=radians(float(input('Ry Degrees?: '))) #———input degrees
172            ploty(xc,yc,zc,Ry)
173      if axis == 'z':
174            Rz=radians(float(input('Rz Degrees?: '))) #———input degrees
175            plotz(xc,yc,zc,Rz)
176      if axis == ":
177            break #—quit the program
Listing 5-1Program LRP

5.2 与三角形*面相交的直线

几乎任何*面都可以由一组三角形*面构成,而曲面可以用三角形来*似,因此我们对三角形*面感兴趣。

图 5-6 显示了一条线与一个三角形*面相交的几何图形。清单 5-3 中使用的算法与清单 5-1 中使用的算法基本相同。一个区别是列表的长度当然更短,因为三角形少了一个角。另一个原因是对命中点是在三角形内还是在三角形外的检查是不同的。

在继续列出 5-3 之前,你将开发一个简单的方法来确定一个命中点是在三角形内还是在三角形外。图 5-7 显示了用于越界计算的几何图形。列表 5-2 产生如图 5-8 所示的输出,并修改定义点 3 坐标的列表,如图 5-9 所示。在图 5-8 中,击球出界;在图 5-9 中,它在三角形内。

A456962_1_En_5_Fig6_HTML.jpg

图 5-6

Geometry of a line intersecting a triangular plane

图 5-7 显示了三个三角形:由点 0、1 和 2 定义的黑色三角形是基础三角形,也就是你所关心的三角形。它的面积为 a。由点 0、1 和 3(命中点)定义的三角形的面积为 A1。点 0、3 和 2 之间的第三个三角形的面积为 A2。很容易看出,如果 A1+A2 > A,命中点出界;如果 A1+A2 < A,则在界内。如果你能计算出三个三角形的面积,你将有一个简单的方法来确定命中点是在基本三角形之内还是之外。为此,你依靠一个简单的表达式来确定三角形的面积:

$$ s=\left(a+b+c\right)/2 $$

【5-54】

$$ A=\sqrt{s\left(s-a\right)\kern0.1em \left(s-b\right)\kern0.1em \left(s-c\right)} $$

【5-55】其中 A、b 和 c 是三角形三条边的长度,A 是它的面积。这就是众所周知的赫伦公式,以亚历山大的英雄命名,他是一位大约公元 10-70 年的希腊工程师和数学家。

这种关系在清单 5-2 和随后的清单 5-3 中使用。在清单 5-2 中,程序的大部分都与评估图 5-7 中所示线条的长度有关。然后使用 Heron 公式计算三个区域:A、A1 和 A2。清单 5-2 的第 114-117 行决定命中点是在基本三角形的内部还是外部。它产生图 5-8 。程序 THT2(未显示)与 THT1(列表 5-2 )相同,但调整了列表,将命中点放在三角形内。产生图 5-9 。调整后的列表包括

x=[40,30,80,55]

y=[60,10,60,45]

z=[0,0,0,0]

从这些列表中可以看到,生命值被移到了(55,45,0)。

A456962_1_En_5_Fig9_HTML.jpg

图 5-9

In bounds, hit produced by modified Listing 5-2

A456962_1_En_5_Fig8_HTML.jpg

图 5-8

Out of bounds, no hit produced by Listing 5-2

A456962_1_En_5_Fig7_HTML.jpg

图 5-7

Model for out-of-bounds test

1   """
2   THT1
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sin, cos, radians, sqrt
8
9   plt.axis([0,150,100,0])
10
11  plt.axis('on')
12  plt.grid(True)
13
14  x=[40,30,80,75] #———plane
15  y=[60,10,60,40]
16  z=[0,0,0,0]
17
18  plt.plot([x[0],x[1]],[y[0],y[1]],color='k') #——plot plane A
19  plt.plot([x[1],x[2]],[y[1],y[2]],color='k')
20  plt.plot([x[2],x[0]],[y[2],y[0]],color='k')
21  plt.scatter(x[3],y[3],s=20,color='r')
22
23  plt.plot([x[0],x[3]],[y[0],y[3]],linestyle=':',color='r') #plot planes
24  plt.plot([x[1],x[3]],[y[1],y[3]],linestyle=':',color='r')
25  plt.plot([x[2],x[3]],[y[2],y[3]],linestyle=':',color='r')
26
27  plt.text(35,63,'0') #——label corners
28  plt.text(25,10,'1')
29  plt.text(83,63,'2')
30  plt.text(x[3]+2,y[3],'3')
31
32  a=x[1]-x[0] #——calculate dimensions
33  b=y[1]-y[0]
34  c=z[1]-z[0]
35  Q01=sqrt(a*a+b*b+c*c)
36
37  a=x[2]-x[1]
38  b=y[2]-y[1]
39  c=z[2]-z[1]
40  Q12=sqrt(a*a+b*b+c*c)
41
42  a=x[2]-x[0]
43  b=y[2]-y[0]
44  c=z[2]-z[0]
45  Q02=sqrt(a*a+b*b+c*c)
46
47  a=x[1]-x[3]
48  b=y[1]-y[3]
49  c=z[1]=z[3]
50  Q13=sqrt(a*a+b*b+c*c)
51
52  a=x[2]-x[3]
53  b=y[2]-y[3]
54  c=z[2]-z[3]
55  Q23=sqrt(a*a+b*b+c*c)
56
57  a=x[0]-x[3]
58  b=y[0]-y[3]
59  c=z[0]-z[3]
60  Q03=sqrt(a*a+b*b+c*c)
61
62  s=(Q01+Q12+Q02)/2 #——calculate areas A, A1 and A2
63  A=sqrt(s*(s-Q01)*(s-Q12)*(s-Q02))
64
65  s1=(Q01+Q03+Q13)/2
66  A1=sqrt(s1*(s1-Q01)*(s1-Q03)*(s1-Q13))
67
68  s2=(Q02+Q23+Q03)/2
69  A2=sqrt(s2*(s2-Q02 )*(s2-Q23)*(s2-Q03))
70
71  plt.arrow(70,55,10,15,linewidth=.5,color='grey') #——label area A
72  plt.text(82,73,'A',color='k')
73
74  plt.text(100,40,'A=') #——plot output
75  dle='%7.0f'%  (A)
76  dls=str(dle)
77  plt.text(105,40,dls)
78
79  plt.text(100,45,'A1=',color='r')
80  dle='%7.0f'% (A1)
81  dls=str(dle)

82  plt.text(105,45,dls)
83
84  plt.text(100,50,'A2=',color='r')
85  dle='%7.0f'% (A2)
86  dls=str(dle)
87  plt.text(105,50,dls)
88
89  plt.text(91,55,'A1+A2=',color='r')
90  dle='%7.0f'%  (A1+A2)
91  dls=str(dle)
92  plt.text(106,55,dls)
93
94  plt.text(100,40,'A=')
95  dle='%7.0f'%  (A)
96  dls=str(dle)
97  plt.text(105,40,dls)
98
99  plt.text(100,45,'A1=',color='r')
100 dle='%7.0f'% (A1)
101 dls=str(dle)
102 plt.text(105,45,dls)
103
104 plt.text(100,50,'A2=',color='r')
105 dle='%7.0f'% (A2)
106 dls=str(dle)
107 plt.text(105,50,dls)
108
109 plt.text(91,55,'A1+A2=',color='r')
110 dle="%7.0f'% (A1+A2)
111 dls=str(dle)
112 plt.text(106,55,dls)
113
114 if A1+A2 > A:
115      plt.text(100,63,'OUT, NO HIT')
116 else:
117      plt.text(100,63,'IN, HIT')
118
119 plt.show()

Listing 5-2Program THT1

清单 5-3 描绘了一条线和一个三角形之间的点击点。它与清单 5-1 相似,除了它使用上面开发的内部或外部测试。输出示例如图 5-10 、 5-11 和 5-12 所示。值得注意的一个区别是单位矢量$$ \widehat{\mathbf{n}} $$的计算,它垂直于三角形的*面。在清单 5-1 中,这是通过取\与 v\的叉积找到的。由于和$$ \widehat{\mathbf{v}} $$之间的角度为 90°,这产生了一个垂直于两者的单位矢量,这意味着垂直于*面,并且大小为 1。这是因为$ \left|\widehat{\mathbf{u}}\times \widehat{\mathbf{v}}\right|=\left|\widehat{\mathbf{u}}\right|\kern0.1em \left|\widehat{\mathbf{v}}\right|\kern0.1em \sin \left(\alpha \right)  \widehat{\mathbf{v}}  \left|\widehat{\mathbf{u}}\times \widehat{\mathbf{v}}\right|=(1)(1)(1)=1 $

A456962_1_En_5_Fig12_HTML.jpg

图 5-12

Line too short, no hit. x[3]=-40, x[4]=-10, y[3]=-20, y[4]=-5, z[3]=15, z[4]=0, Rx=0, Ry=0, Rz=0 (produced by Listing 5-3)

A456962_1_En_5_Fig11_HTML.jpg

图 5-11

Out-of-bounds hit. x[3]=-60, x[4]=40, y[3]=-20, y[4]=5, z[3]=15, z[4]=0, Rx=-90, Ry=45, Rz=20 (produced by Listing 5-3)

A456962_1_En_5_Fig10_HTML.jpg

图 5-10

In-bounds hit. x[3]=-60, x[4]=70, y[3]=-20, y[4]=20, z[3]=15, z[4]=0, Rx=-90, Ry=45, Rz=20 (produced by Listing 5-3)

然而,对于一般的非直角三角形,角度不是 90°,因此由叉积产生的向量虽然垂直于*面,但不具有值 1;换句话说,它不是一个单位向量。第 88 行和第 91 行之间的算法通过规范化$ \widehat{\mathbf{n}}  \widehat{\mathbf{n}}  \widehat{\mathbf{n}}  \widehat{\mathbf{n}}  \widehat{\mathbf{n}} $成为一个单位向量。

1   """
2   LTP
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians,sqrt
8
9   #——————————————fill lists with starting coordinates
10  xg=[ ]
11  yg=[ ]
12  zg=[ ]
13
14  xc=80 #————————center coordinates
15  yc=40
16  zc=40
17
18  x=[-10,-30,20,-40,-10]
19  y=[0,0,0,-20,-5]
20  z=[0,30,0,15,0]
21
22  for i in range(len(x)):
23        xg.append(x[i]+xc)
24        yg.append(y[i]+yc)
25        zg.append(z[i]+zc)
26
27  #—————————————————–define  rotation  functions
28  def rotx(xc,yc,zc,xp,yp,zp,Rx):
29       (same as in prior programs)
30
31  def  roty(xc,yc,zc,xp,yp,zp,Ry):
32       (same as in prior programs)
33
34  def rotz(xc,yc,zc,xp,yp,zp,Rz):
35       (same as in prior programs)
36
37  #———————————————-define  system  plotting  functions
38  def plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor):
39       plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k')  #—————plot plane
40       plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k')
41       plt.plot([xg[2],xg[0]],[yg[2],yg[0]],color='k')
42       plt.plot([xg[3],xg[4]],[yg[3],yg[4]],color='g') #———plot line
43       plt.scatter(xc,yc,s=10,color='k') #———plot center of rotation
44
45       if hitcolor=='g':
46            plt.scatter(xg[4],yg[4],s=20,color=hitcolor)
47       else:
48            plt.scatter(xhg,yhg,s=20,color=hitcolor) #——————plot hit point
49
50       plt.axis([0,150,100,0]) #———replot axes and grid
51       plt.axis('on')
52       plt.grid(True)
53       plt.show() #———plot latest rotation
54
55  #————————————-calculate hit point coordinates and color
56  def hitpoint(x,y,z):
57       a=x[4]-x[3]
58       b=y[4]-y[3]
59       c=z[4]-z[3]
60       Q34=sqrt(a*a+b*b+c*c)  #———distance point 3 to 4
61
62       lx=a/Q34 #———unit vector components point 3 to 4
63       ly=b/Q34
64       lz=c/Q34
65
66       a=x[2]-x[0]
67       b=y[2]-y[0]
68       c=z[2]-z[0]
69       Q02=sqrt(a*a+b*b+c*c) #———distance 0 to 3
70
71       ux=a/Q02 #———unit vector 0 to 3
72       uy=b/Q02
73       uz=c/Q02
74
75       a=x[1]-x[0]
76       b=y[1]-y[0]
77       c=z[1]-z[0]
78       Q01=sqrt(a*a+b*b+c*c) #———distance 0 to 1
79
80       vx=a/Q01 #———unit vector 0 to 1
81       vy=b/Q01
82       vz=c/Q01
83
84       nx=uy*vz-uz*vy #———normal unit vector
85       ny=uz*vx-ux*vz
86       nz=ux*vy-uy*vx
87  #——————————–correct magnitude of unit vector ^n
88       magn=sqrt(nx*nx+ny*ny+nz*nz)
89       nx=nx/magn
90       ny=ny/magn
91       nz=nz/magn
92  #——————————————————————
93       a=x[3]-x[0] #———vector components 0 to 3
94       b=y[3]-y[0]
95       c=z[3]-z[0]
96
97       Qn=(a*nx+b*ny+c*nz) #———perpendicular distance 3 to plane
98
99       cosp=lx*nx+ly*ny+lz*nz #———cos of angle p
100      Qh=abs(Qn/cosp) #———distance 4 to hit point
101
102      xh=x[3]+Qh*lx #———hit point coordinates
103      yh=y[3]+Qh*ly
104      zh=z[3]+Qh*lz
105
106      xhg=xh+xc #———global hit point coordinates
107      yhg=yh+yc
108      zhg=zh+zc
109
110 #————————————————————out of bounds check
111      a=x[1]-x[2]
112      b=y[1]-y[2]
113      c=z[1]-z[2]
114      Q12=sqrt(a*a+b*b+c*c)
115
116      a=x[1]-xh
117      b=y[1]-yh
118      c=z[1]-zh
119      Q1h=sqrt(a*a+b*b+c*c)
120
121      a=x[2]-xh
122      b=y[2]-yh
123      c=z[2]-zh
124      Q2h=sqrt(a*a+b*b+c*c)
125
126      a=x[0]-xh
127      b=y[0]-yh
128      c=z[0]-zh
129      Q0h=sqrt(a*a+b*b+c*c)
130
131      s=(Q01+Q12+Q02)/2 #—area A
132      A=sqrt(s*(s-Q01)*(s-Q12)*(s-Q02))
133
134      s1=(Q01+Q0h+Q1h)/2 #———area A1
135      A1=sqrt(s1*(s1-Q01)*(s1-Q0h)*(s1-Q1h))
136
137      s2=(Q02+Q2h+Q0h)/2 #—area A2
138      A2=sqrt(s2*(s2-Q02)*(s2-Q2h)*(s2-Q0h))
139
140      hitcolor='r' #———if within bounds plot red hit point
141
142      if A1+A2 > A: #———if out of bounds plot blue hit point
143           hitcolor='b'
144
145      a=x[4]-x[3]
146      b=y[4]-y[3]
147      c=z[4]-z[3]
148      Q34=sqrt(a*a+b*b+c*c)
149
150      if Q34 < Qh: #———if line too short plot green at end of line
151           hitcolor='g'
152
153      return xh,yh,xhg,yhg,hitcolor
154
155 #————————————————transform coordinates and plot
156 def plotx(xc,yc,zc,Rx):   #———transform & plot Rx system
157      for i in range(len(x)):
158            [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)   
159            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
160
161      xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z) #———returns xh,yh,xhg,yhg
162
163      plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor) #———plot plane, line, hit point
164
165      def ploty(xc,yc,zc,Ry):  #———transform & plot Ry system
166      for i in range(len(x)):
167           [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
168           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
169
170      xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z)
171
172      plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor)
173
174 def plotz(xc,yc,zc,Rz):   #———transform  &  plot  Rz  system
175      for i in range(len(x)):
176            [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
177            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
178
179      xh,yh,xhg,yhg,hitcolor=hitpoint(x,y,z)
180
181      plotsystem(xg,yg,zg,xh,yh,xhg,yhg,hitcolor)
182
183 #——————————————————input data and plot system
184      while True:
185           axis=input('x, y or z?: ') #———input axis of rotation (lower case)
186           if axis == 'x': #—if x axis
187                     Rx=radians(float(input('Rx Degrees?: '))) #———input degrees of rotation
188                     plotx(xc,yc,zc,Rx) #–call function plotx
189           if axis == 'y':
190                     Ry=radians(float(input('Ry Degrees?: '))) #———input degrees of rotation
191                     ploty(xc,yc,zc,Ry)
192           if axis == 'z':
193                     Rz=radians(float(input('Rz Degrees?: '))) #———input degrees of rotation
194                     plotz(xc,yc,zc,Rz)
195           if axis == ":
196                     break #———quit the program
Listing 5-3Program LTP

5.3 与圆相交的线

确定与圆*面相交的直线的命中点是否在圆内是微不足道的。如图 5-13 所示,如果圆心到命中点的距离大于圆的半径,则位于圆外:

如果 rh > r 未命中

A456962_1_En_5_Fig13_HTML.jpg

图 5-13

Model for out-of-bounds test for a circle

我们不会写一个单独的程序来演示这一点。您应该能够通过修改清单 5-1 或清单 5-3 来自己完成这项工作。只需用定义圆的周长和线坐标的点填充 x[ ]、y[ ]、z[ ],并修改函数 plotsystem 和 hitpoint。

5.4 与扇形相交的直线

在本节中,您将开发一个程序来确定与圆的扇形*面相交的直线的命中点是在扇形内部还是外部。图 5-14 所示为扇形。它的圆心在 0 点,半径为 r,命中点在 3 点。rh 是从 0 到击中点的距离。你的目标是确定命中点是在扇区内还是扇区外。(我们不会在这里开发一个完整的三维程序;您将看到内部或外部算法是如何工作的。)它可以很容易地合并到前面的任何程序中,比如清单 5-3 。

A456962_1_En_5_Fig14_HTML.jpg

图 5-14

Model for determining whether a line intersecting a circular sector is in or out of bounds. 3=hit point.

点 0 处有五个单位向量:③从 0 到 2 的点;$$ \widehat{\mathbf{v}} $$从 0 到 1 分;和ĥ从 0 到 3 的生命值。$$ \widehat{\mathbf{n}} $$是垂直于扇形*面的单位矢量。因为它向上指向*面外,所以没有显示。$$ \widehat{\mathbf{u}}\times \widehat{\mathbf{n}} $$是③与$$ \widehat{\mathbf{n}} $$叉积的结果;$$ \widehat{\mathbf{n}}\times \widehat{\mathbf{v}} $$来自$$ \widehat{\mathbf{n}} $$$$ \widehat{\mathbf{v}} $$的叉积。

您的策略是首先确定 Rh>r,在这种情况下,命中点在径向方向的扇区之外。然后你拿ĥ和$$ \widehat{\mathbf{u}}\times \widehat{\mathbf{n}} $$的点积。如果结果是肯定的,则命中点在 0-2 侧的扇区之外。然后你拿ĥ和$$ \widehat{\mathbf{n}}\times \widehat{\mathbf{v}} $$的点积。如果为正,则命中点在 0-1 侧出界。

在清单 5-4 中,第 14-16 行的列表中定义了局部坐标(相对于点 0)。列表中的最后一个元素定义了命中点(点 3)的坐标。第 18-20 行中的 xc、yc 和 zc 是点 0 的全局坐标。命中测试算法从第 23 行开始。基于前面的讨论,大部分应该是不言自明的。请注意第 52-58 行。这就是法向矢量$$ \widehat{\mathbf{n}} $$的计算方法,它是通过取与$$ \widehat{\mathbf{v}} $$的叉积。如前所述,只有当和$$ \widehat{\mathbf{v}} $$相互垂直时,才会产生一个单位矢量(大小为 1)。由于在一般扇区中它们之间的角度不一定是 90 度,所以矢量必须被归一化。发生在第 55-58 行。与ĥ的点积发生在第 64 行,与ĥ的点积发生在第 70 行。第 72 行假设命中颜色是红色,这意味着命中在扇区内。如果 A 为正,则它位于扇区之外,在这种情况下,第 74 行中的命中颜色变为蓝色。第 76 行和第 77 行对扇区的另一侧执行相同的测试。第 79 行和第 80 行检查在径向方向上位于扇区外部的命中点。图 5-15 和 5-16 显示了两次样品运行。你可以通过改变第 14-15 行列表中第 3 点的坐标来移动生命值。您只需更改击中点的 x 和 y 坐标,因为它被假定位于 z=0 *面上,就像扇形一样。

A456962_1_En_5_Fig16_HTML.jpg

图 5-16

In-bounds or out-of-bounds test produced by Listing 5-4: red=in, blue=out

A456962_1_En_5_Fig15_HTML.jpg

图 5-15

In-bounds or out-of-bounds test produced by Listing 5-4: red=in, blue=out

1   """
2    LCSTEST
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sin, cos, radians, degrees, sqrt, acos
8
9   plt.axis([0,150,100,0])
10
11  plt.axis('on')
12  plt.grid(True)
13
14  x=[0,20,40,5]
15  y=[0,-35,0,-25]
16  z=[0,0,0,0]
17
18  xc=40
19  yc=60
20  zc=0
21
22  #——————————————hit test
23  a=x[3]-x[0]
24  b=y[3]-y[0]
25  c=z[3]-z[0]
26  rh=sqrt(a*a+b*b+c*c)
27
28  a=x[3]-x[0]
29  b=y[3]-y[0]
30  c=z[3]-z[0]
31  Q0h=sqrt(a*a+b*b+c*c)
32  hx=a/Q0h #———unit vector 0 to hit point
33  hy=b/Q0h
34  hz=c/Q0h
35
36  a=x[2]-x[0]
37  b=y[2]-y[0]
38  c=z[2]-z[0]
39  Q02=sqrt(a*a+b*b+c*c)
40  ux=a/Q02 #———unit vector 0 to 3
41  uy=b/Q02
42  uz=c/Q02
43
44  a=x[1]-x[0]
45  b=y[1]-y[0]
46  c=z[1]-z[0]
47  Q01=sqrt(a*a+b*b+c*c)
48  vx=a/Q01 #———unit vector 0 to 1
49  vy=b/Q01
50  vz=c/Q01
51
52  a=uy*vz-uz*vy #———vector uˆxvˆ normal to plane
53  b=uz*vx-ux*vz
54  c=ux*vy-uy*vx
55  Quxv=sqrt(a*a*b*b+c*c) #———normalize uˆxvˆ
56  nx=a/Quxv
57  ny=b/Quxv
58  nz=c/Quxv
59
60  uxnx=uy*nz-uz*ny #———unit vector uˆxvˆ
61  uxny=uz*nx-ux*nz
62  uxnz=ux*ny-uy*nx
63
64  A=uxnx*hx+uxny*hy+uxnz*hz #———dot product uˆxvˆ with hˆ
65
66  nxvx=ny*vz-nz*vy #———unit vector uˆxvˆ
67  nxvy=nz*vx-nx*vz
68  nxvz=nx*vy-ny*vx
69
70  B=nxvx*hx+nxvy*hy+nxvz*hz #———dot product uˆxvˆ with hˆ
71
72  hitcolor='r'
73  if A>0:  #—out
74       hitcolor='b'
75
76  if B>0: #—out
77       hitcolor='b'
78
79  if rh>r: #—out
80      hitcolor='b'
81
82  plt.scatter(x[3]+xc,y[3]+yc,s=20,color=hitcolor)
83
84  #————————————-plot   arc
85  r=40
86  phi1=0
87  phi2=-radians(60)
88  dphi=(phi2-phi1)/180
89  xlast=xc+r
90  ylast=yc+0
91  for phi in np.arange(phi1,phi2,dphi):
92      x=xc+r*cos(phi)
93      y=yc+r*sin(phi)
94      plt.plot([xlast,x],[ylast,y],color='k')
95      xlast=x
96      ylast=y
97
98
99  #————————————-labels
100 print('rh=',rh)
101 print('r=',r)
102 plt.arrow(xc,yc,40,0)
103 plt.arrow(xc,yc,20,-35,linewidth=.5,color='k')
103 plt.text(33,61,'0')
104 plt.text(52,27,'1')
105 plt.text(82,65,'2')
106
107  plt.show()
Listing 5-4Program LCSTEST

5.5 与球体相交的线

清单 5-5 的输出图 5-17 ,显示了一条与球体相交的线。入口点和出口点显示为红色。图 5-18 为清单 5-5 使用的型号。这条线从 B 开始,到 e 结束。

A456962_1_En_5_Fig18_HTML.jpg

图 5-18

Model for a line intersecting a sphere

A456962_1_En_5_Fig17_HTML.jpg

图 5-17

Line intersecting a sphere, produced by Listing 5-5

要找到入口撞击点,从 B 开始,沿着直线向 e 逐渐移动点 p。在每一步,计算 Qpc,即 p 和 c 之间的距离。如果 Qpc 小于或等于球体的半径 rs,则表示您已经接触到球体,并绘制出红点。你继续沿着球内的线移动 p,不画任何东西(你可以画一条虚线),一边移动一边计算 Qpc,直到 Qpc 等于或大于 rs。在这一点上,p 离开球体,并绘制另一个红点。p 继续沿着 E 线移动,一路上画出黑点。您可以像以前的程序那样使用短线段,而不是用点画线。

要沿线移动 p,您需要使用参数 t,它是 B 到 p 的距离。要获得 p 的坐标,您需要构造单位向量,它沿线指向

$$ a= xe- xb $$

【5-56】

$$ b= ye- yb $$

【5-57】

$$ c= ze- zb $$

【5-58】

$$ Qbe=\sqrt{a²+{b}²+{c}²} $$

【5-59】

$$ ux=a/ Qbe $$

【5-60】

$$ uy=b/ Qbe $$

【5-66】p 的坐标是这样的

$$ xp= xb+ uxt $$

(5-63)

$$ yp= yb+ uyt $$

(5-64)

$$ zp= zb+ uzt $$

(5-65)

Qpc 很容易确定:

$$ a= xc- xp $$

(5-66)

$$ b= yc- yp $$

(5-67)

$$ c= zc- zp $$

(5-68)

$$ Qpc=\sqrt{a²+{b}²+{c}²} $$

(5-69)

在清单 5-5 中,球体的中心坐标设置在第 18-20 行。球体由经度(垂直)线和纬度(水*)线组成。第 10-16 行中的列表包含经度的局部和全局坐标。这些列表的初始填充发生在第 25-38 行,它们在 z=0 *面上创建了一个半圆。如图 5-19 所示,点 p 位于圆周上坐标 xp,yp,zp 处,其中

$$ xp= rs\kern0.1em \mathit{\cos}\left(\phi \right) $$

(5-70)

$$ yp= rs\kern0.1em \mathit{\sin}\left(\phi \right) $$

(5-71)

$$ zp=0 $$

(5-72)

它们被设置在第 30-32 行。ϕ是围绕 z 方向的角度。从-90 到+90 运行。你不需要经度的后半部分,所以它们不会被标绘。这个半圆将围绕 y 方向旋转,以创建椭圆形经度。它们相隔 10 个如 74 行中设定的 ??。因为它们只绕 y 方向旋转,所以程序只包含旋转函数 roty:在这个模型中不需要 rotx 和 rotz。经度的绘制发生在第 72-77 行。

纬度绘制在 80-97 行。图 5-21 显示了球体在 x,y *面内的前视图。每个纬度本质上是一个半径为 R1 的圆,其中

$$ xl= rs\kern0.1em \mathit{\cos}\left(\phi \right) $$

(5-73)

这是在程序的第 89 行计算的。从前面看,纬度显示为一条直线,因为在这个程序中您没有旋转球体。

从第 88 行开始的ϕ循环以 10 的增量将ϕ从-90 调整到+ 90 。每增加一次,就绘制一个新的纬度。其半径由上面的公式 5-73 给出。从第 92 行开始的α循环以 10 的增量从α=0 到 180 扫过圆形纬度的正面。图 5-22 对此进行了说明,该图显示了从 x、z *面向下看的俯视图。

A456962_1_En_5_Fig22_HTML.jpg

图 5-22

Sphere latitude - x,z view

A456962_1_En_5_Fig21_HTML.jpg

图 5-21

Sphere latitude - x,y view

A456962_1_En_5_Fig20_HTML.jpg

图 5-20

x,y view of sphere longitude rotated by Ry=60°

A456962_1_En_5_Fig19_HTML.jpg

图 5-19

x,y view of sphere longitude shown at starting position Ry=0. Rotation around the y direction in 10° increments will produce longitudes.

1   """
2   LS
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians, sqrt
8
9   #—————————————————————————lists
10  x=[ ]
11  y=[ ]
12  z=[ ]
13
14  xg=[ ]
15  yg=[ ]
16  zg=[ ]
17
18  xc=80 #——sphere center
19  yc=50
20  zc=0
21
22  rs=40 #———sphere radius
23
24  #————————————————————fill longitude lists
25  phi1=radians(-90)
26  phi2=radians(90)
27  dphi=radians(10)
28
29  for phi in np.arange(phi1,phi2,dphi):
30       xp=rs*cos(phi)
31       yp=rs*sin(phi)
32       zp=0
33       x.append(xp)
34       y.append(yp)
35       z.append(zp)
36       xg.append(xp)
37       yg.append(yp)
38       zg.append(zp)
39
40  #==============================================define rotation function
41  def roty(xc,yc,zc,xp,yp,zp,Ry):
42       a=[xp,yp,zp]
43       b=[cos(Ry),0,sin(Ry)] #———————[cx11,cx12,cx13]
44       xpp=np.inner(a, b)
45       b=[0,1,0] #—————[cx21,cx22,cx23]
46       ypp=np.inner(a,b) #——————–scalar product of a,b
47       b=[-sin(Ry),0,cos(Ry)] #—————[cx31,cx32,cx33]
48       zpp=np.inner(a,b)
49       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
50       return[xg,yg,zg]
51
52  #=========================================================
53  def plotsphere(xg,yg,zg):
54       lastxg=xg[0]
55       lastyg=yg[0]
56       for i in range(len(x)):
57             if i < len(x)/2:
58                   plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1,color='k')
59             else:
60                   plt.plot([lastxg,xg[i]],[lastyg,yg[i]],linewidth=1,color='k')
61       lastxg=xg[i]
62       lastyg=yg[i]
63
64  #================================================transform coordinates
65  def plotspherey(xc,yc,zc,Ry):
66       for i in range(len(x)): #—————transform and plot Ry sphere
67             [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
68
69  plotsphere(xg,yg,zg)  #———plot rotated coordinates
70
71  #—————————————————plot longitudes
72  Ry1=radians(0)
73  Ry2=radians(180)
74  dRy=radians(10)
75
76  for Ry in np.arange(Ry1,Ry2,dRy):
77       plotspherey(xc,yc,zc,Ry)
78
79  #————————————————–plot latitudes
80  alpha1=radians(0)
81  alpha2=radians(180)
82  dalpha=radians(10)
83
84  phi1=radians(-90)
85  phi2=radians(90)
86  dphi=radians(10)
87
88  for phi in np.arange(phi1,phi2,dphi):
89       r=rs*cos(phi) #————————latitude radius
90       xplast=xc+r
91       yplast=yc+rs*sin(phi)
92       for  alpha  in  np.arange(alpha1,alpha2,dalpha):
93            xp=xc+r*cos(alpha)
94            yp=yplast
95            plt.plot([xplast,xp],[yplast,yp],color='k')
96            xplast=xp
97            yplast=yp
98
99  #—————————————————line and hit points
100 xb=-60 #—line beginning
101 yb=-30
102 zb=-20
103
104 xe=60 #——line end
105 ye=30
106 ze=-40
107
108 a=xe-xb
109 b=ye-yb
110 c=ze-zb
111 Qbe=sqrt(a*a+b*b+c*c)  #———line length
112 ux=a/Qbe #—unit vector uˆ
113 uy=b/Qbe
114 uz=c/Qbe
115
116 dt=1
117 for t in np.arange(0,Qbe,dt):
118       xp=xb+ux*t
119       yp=yb+uy*t
120       zp=zb+uz*t
121       Qpc=sqrt(xp*xp+yp*yp+zp*zp)
122       if Qpc > rs:
123            plt.scatter(xp+xc,yp+yc,s=5,color='k')
124       if Qpc <= rs:
125            plt.scatter(xp+xc,yp+yc,s=80,color='r')
126       tlast=t
127       break
128
129 for t in np.arange(tlast,Qbe,dt):
130       xp=xb+ux*t
131       yp=yb+uy*t
132       zp=zb+uz*t
133       Qpc=sqrt(xp*xp+yp*yp+zp*zp)
134       if Qpc >= rs:
135            plt.scatter(xp+xc,yp+yc,s=80,color='r')
136       tlast=t
137       break
138
139 for t in np.arange(tlast,Qbe,dt):
140       xp=xb+ux*t
141       yp=yb+uy*t
142       zp=zb+uz*t
143       Qpc=sqrt(xp*xp+yp*yp+zp*zp)
144       if Qpc >= rs:
145            plt.scatter(xp+xc,yp+yc,s=5,color='k')
146
147 plt.axis([0,150,100,0]) #–plot axes and grid
148 plt.axis('off')
149 plt.grid(False)
150
151  plt.show()
Listing 5-5Program LS

5.6 与球体相交的*面

在本节中,您将学习一种绘制与球体相交的*面矩形的技术。图 5-23 显示了清单 5-6 的输出;图 5-24 显示了该清单使用的模型。

这里的策略是使用上一节中开发的算法,将与球体相交的直线作为基本元素。通过将*面表示为一系列*行线,可以很容易地找到*面与球体的交点。图 5-23 显示了拐角 1 处的单位矢量。和以前一样,这从第一行的开始指向结束。在角 1 也有单位矢量$$ \widehat{\mathbf{v}} $$。这指向角 3。通过沿着从 1 到 3 的线一小步一小步地前进,你可以构建与从 1 到 2 的第一条线*行的线。沿着每一条线,以 t 为增量,你可以找到*面上各点的坐标。为了在$$ \widehat{\mathbf{v}} $$方向前进,您引入参数 s,它是从角 1 到新线起点的距离。要得到那条线的终点的坐标,使用 vˇ和 s 从点 2 开始执行相同的操作,如在

$$ xe=x2+ vx\cdotp s $$

【5-74】

$$ ye= yr+ vy\cdotp s $$

【5-75】

$$ ze=z2+ vz\cdotp s $$

【5-76】中,xe、ye 和 ze 是线的终点的坐标;x2、y2 和 z2 是点 2 的坐标;而 vx,vy,vz 是单位矢量$$ \widehat{\mathbf{v}} $$的分量。

使用参数 t 和 s 在*面上向下和横向递增,允许您扫过*面的表面。在每个点 p,你计算从 p 到球心的距离。如果它等于或小于球体的半径,你就击中了。

我不会列出产生图 5-23 的整个程序,因为它与清单 5-5 非常相似,除了增加了一个在$$ \widehat{\mathbf{v}} $$方向扫过的 s 循环。程序的控制从第 27 行开始。第 27-37 行定义了*面拐角 1、2 和 3 的坐标。在第 39-53 行建立单位向量\u 和$$ \widehat{\mathbf{v}} $$。第 55 和 56 行设置 dt 和 ds 中的扫描增量。循环 57-64 在$$ \widehat{\mathbf{v}} $$方向扫描,建立每条线的起点和终点坐标。从第 1 行开始的函数*面确定每一行和球体是否有命中。对于每个 s,从第 3 行开始的循环在{\ F2 }方向沿着该行前进,计算沿着该行的每个点 p 的坐标 xp、yp、zp。第 10 行计算 p 到球体中心的距离。第 11 行说,如果距离大于球体的半径,画一个黑点。如果它小于或等于半径,第 18 行绘制一个无色点。直到第 24 行的其余逻辑确定该线是否从球体中出现,在这种情况下,黑点的绘制继续。结果如图 5-23 所示。

A456962_1_En_5_Fig24_HTML.jpg

图 5-24

Model for Listing 5-6

A456962_1_En_5_Fig23_HTML.jpg

图 5-23

Plane intersecting a sphere produced by Listing 5-6

"""
PS
"""
import numpy as np
import matplotlib.pyplot as plt
from math import sin, cos, radians, sqrt
.

(similar to Program LS)
.

    #===================================================plane
1   def plane(xb,yb,zb,xe,ye,ze,Q12,dt):
2        hit='off'
3        for t in np.arange(0,Q12,dt): #———B to hit
4             xp=xb+ux*t
5             yp=yb+uy*t
6             zp=zb+uz*t
7             xpg=xc+xp
8             ypg=yc+yp
9             zpg=zc+zp
10            Qpc=sqrt(xp*xp+yp*yp+zp*zp)
11            if Qpc>=rs:
12                 plt.scatter(xpg,ypg,s=.5,color='k')
13            if Qpc<=rs:
14                 if hit=='off':
15                       hit='on'
16            if Qpc<rs:
17                 if hit=='on':
18                       plt.scatter(xpg,ypg,s=10,color=")
19            if Qpc>=rs:
20                 if hit=='on':
21                      hit='off'
22            if Qpc>rs:
23                 if hit=='off':
24                      plt.scatter(xpg,ypg,s=.5,color='k')
25
26  #———————————————————scan  across  plane
27  x1=-40
28  y1=-30
29  z1=-20
30
31  x2=60
32  y2=25
33  z2=-35
34
35  x3=-65
36  y3=-20
37  z3=-50
38
39  a=x2-x1
40  b=y2-y1
41  c=z2-z1
42  Q12=sqrt(a*a+b*b+c*c)
43  ux=a/Q12
44  uy=b/Q12
45  uz=c/Q12
46
47  a=x3-x1
48  b=y3-y1
49  c=z3-z1
50  Q13=sqrt(a*a+b*b+c*c)
51  vx=a/Q13
52  vy=b/Q13
53  vz=c/Q13
54
55  dt=.7 #————————————scan increment
56  ds=.7
57  for s in np.arange(0,Q13,ds):
58       sbx=x1+s*vx
59       sby=y1+s*vy
60       sbz=z1+s*vz
61       sex=x2+s*vx
62       sey=y2+s*vy
63       sez=z2+s*vz
64       plane(sbx,sby,sbz,sex,sey,sez,Q12,dt)
65
66  plt.axis([0,150,100,0]) #–replot axes and grid
67  plt.axis('off')
68  plt.grid(False)
69
70  plt.show() #–plot latest rotation

Listing 5-6Program PS

5.7 总结

在本章中,您学习了如何预测三维直线或*面是否会与三维曲面或立体相交。为什么要为此烦恼呢?因为这是消除隐藏线的基础,你会在第六章看到。在绘制可能位于另一个曲面或对象 B 后面的曲面 A 时,可以一小步一小步地进行,在每一步绘制一个分散点(或一小段线段)。如果 A 上的点被 B 隐藏,则不要绘制它。为了确定它是否被观察者隐藏,你画一条从 A 点到观察者的假想线(即在-z 方向)。如果你能确定从 A 开始的那条线是否与它前面的一个表面或物体 B 相交,那么你就知道它是否被隐藏了。虽然您不能为每一种可能的情况开发隐藏线算法(您在这里开发了矩形*面、三角形*面、圆形扇形、圆形和球形),但是通过了解如何为这些对象开发隐藏线算法,您应该能够利用一点创造力为其他表面和对象开发自己的隐藏线算法。也许线三角形*面是最有用的,因为复杂的表面和物体通常可以用三角形的集合来*似。你会在第六章中看到更多。

六、隐藏线移除

前几章中使用的大多数模型本质上都是由点和线构成的简笔画。当在三维空间中观察这种物体时,可以看到背面的线条,就好像物体是透明的一样。这一章是关于从物体上去除通常隐藏的线条,使它们看起来是实心的。

本章将涵盖两种情况。第一种称为对象内隐藏线去除。这是指从单个对象中删除隐藏线。我们假设大多数物体都是由*面构成的;这些示例包括长方体、棱锥体和由*面*似的球面。您将使用的技术依赖于确定特定*面是面向观察者(在这种情况下,它是可见的并被打印)还是远离观察者(在这种情况下,它是不可见的且不被打印)。

另一方面,对象间隐藏线去除指的是多于一个对象的系统,例如两个*面,一个在另一个后面。这里的一般方法是使用一些在前一章中开发的光线追踪技术来寻找线和表面之间的交点。首先,使用点或短线段绘制背面对象。在每一点上,你构建一条朝向观察者的线(光线),观察者在-z 方向,并观察它是否与前面的物体相交。如果是,则隐藏背面对象上的该点,并且不打印。

6.1 方框

作为对象内隐藏线去除的一个例子,让我们从一个简单的盒子开始,如图 6-1 和 6-2 所示。他们是通过列举 6-1 得出的。图 6-3 、 6-4 和 6-5 显示了程序使用的模型。

在图 6-3 中,你看到盒子有八个角,编号为 0 到 7。在角 0 处,有两个向量:从角 0 到 1 的 V01 和从 0 到 3 的 V03。首先查看 0,1,2,3 面,当旋转盒子时,策略是确定它是朝向还是远离-z 方向上的观察者倾斜。如果它面向观察者,则绘制该面的边。如果它背对着观察者,则不会绘制它们。如何确定人脸是否正对观察者?叉(向量)积 V03×V01 给出一个向量 N,垂直于 0,1,2,3 面,所以

$$ \mathbf{V}\mathbf{03}=V03x\widehat{\mathbf{i}}+V03y\widehat{\mathbf{j}}+V03z\widehat{\mathbf{k}} $$

【6-1】

$$ \mathbf{V}\mathbf{01}=V01x\widehat{\mathbf{i}}+V01y\widehat{\mathbf{j}}+V01z\widehat{\mathbf{k}} $$

(6-2)

$$ V03x=x\left[3\right]-x\left[0\right] $$

【6-3】

$$ V03y=y\left[3\right]-y\left[0\right] $$

【6-4】

$$ V03z=z\left[3\right]-z\left[0\right] $$

【6-5】

$$ V01x=x\left[1\right]-x\left[0\right] $$

(6-6)

$$ V01y=y\left[1\right]-y\left[0\right] $$

您可以通过 Nz,N 的 z 分量的值来确定*面是面向还是背离观察者。图 6-4 和 6-5 显示了相对于观察者的*面(蓝色)。这是图 6-3 中所示盒子的一个面的侧视图。观察者位于坐标系的右侧,朝+z 方向看。参考图 6-4 ,如果方程 6-11 中 N,Nz 的 z 分量为< 0(即指向-z 方向),则*面面向观察者,观察者可见,并标绘。如果 Nz 为正(即指向+z 方向),如图 6-5 所示,该面倾斜远离观察者,在这种情况下,观察者看不到该面,也不会绘制该面。注意,你可以使用全矢量 V,而不是单位矢量,因为你只关心 V 的符号。

其他的脸呢?4,5,6,7 面*行于 0,1,2,3 面,因此其指向外的法向量与 0,1,2,3 面的法向量相反。您可以进行类似的检查,确定其法向量是指向+z(不绘制)还是=z(绘制)方向。

其余的面以类似的方式处理。1,2,6,5 的法线与 0,3,7,4 的法线相反;3,2,6,7 的法线与 0,1,5,4 的法线相反。

A456962_1_En_6_Fig1_HTML.jpg

图 6-1

Box with hidden lines removed: Rx=45°, Ry=45°, Rz=30° (produced by Listing 6-1)

列表 6-1 产生数字 6-1 和 6-2 。第 9、10 和 11 行中的列表定义了未旋转的盒子相对于其中心的坐标,该坐标在第 124-126 行中设置。第 13-15 行用零填充全局坐标列表。这些列表与列表 x 的长度相同(也列出了 y 和 z ),由 len(x)函数设置。

第 124-140 行像前面的程序一样接受键盘输入。作为操作序列的一个例子,假设您在第 129 行输入 x,后跟一个角度(度)。第 132 行调用从第 102 行开始的函数 plotboxx。第 103-105 行旋转角点并更新局部和全局坐标列表。第 107 行调用从第 40 行开始的函数 plotbox。该函数使用列表 xg、yg 和 zg 绘制新旋转方向的长方体。从 0,1,2,3 面开始,第 41-47 行使用上述分析计算 Nz,即第 47 行中法向量 N 的 z 分量。如果 Nz <=0, the 0,1,2,3 face is plotted in lines 49-52. If it is not visible (i.e. Nz> 0),那么你知道对面的面 4,5,6,7 一定是可见的,并且它被绘制在第 54-57 行中。其他面以类似的方式处理。

A456962_1_En_6_Fig5_HTML.jpg

图 6-5

Model for hidden line removal of a box used by Listing 6-1

A456962_1_En_6_Fig4_HTML.jpg

图 6-4

Model for hidden line removal of a box used by Listing 6-1

A456962_1_En_6_Fig3_HTML.jpg

图 6-3

Model for hidden line removal of a box used by Listing 6-1. N not to scale.

A456962_1_En_6_Fig2_HTML.jpg

图 6-2

Box with hidden lines removed: Rx=30°, Ry=-60°, Rz=30° (produced by Listing 6-1)

1   """
2   HLBOX
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians
8   #————————————————————define  lists
9   x=[-20,20,20,-20,-20,20,20,-20]
10  y=[-10,-10,-10,-10,10,10,10,10]
11  z=[5,5,-5,-5,5,5,-5,-5]
12
13  xg=[0]*len(x) #—fill xg,yg,zg lists with len(x) zeros
14  yg=[0]*len(x)
15  zg=[0]*len(x)
16
17  #===================================================rotation functions
18  def rotx(xc,yc,zc,xp,yp,zp,Rx):
19       xpp=xp
20       ypp=yp*cos(Rx)-zp*sin(Rx)
21       zpp=yp*sin(Rx)+zp*cos(Rx)
22       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
23       return[xg,yg,zg]
24
25  def  roty(xc,yc,zc,xp,yp,zp,Ry):
26       xpp=xp*cos(Ry)+zp*sin(Ry)
27       ypp=yp
28       zpp=-xp*sin(Ry)+zp*cos(Ry)
29       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
30       return[xg,yg,zg]
31
32  def rotz(xc,yc,zc,xp,yp,zp,Rz):
33       xpp=xp*cos(Rz)-yp*sin(Rz)
34       ypp=xp*sin(Rz)+yp*cos(Rz)
35       zpp=zp
36       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
37       return[xg,yg,zg]
38
39  #================================================box plotting function
40  def plotbox(xg,yg,zg):
41       v01x=x[1]-x[0] #———0,1,2,3 face
42       v01y=y[1]-y[0]
43       v01z=z[1]-z[0]
44       v03x=x[3]-x[0]
45       v03y=y[3]-y[0]
46       v03z=z[3]-z[0]
47       nz=v03x*v01y-v03y*v01x
48       if nz<=0 :
49            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linewidth=2)
50            plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k',linewidth=2)
51            plt.plot([xg[2],xg[3]],[yg[2],yg[3]],color='k',linewidth=2)
52            plt.plot([xg[3],xg[0]],[yg[3],yg[0]],color='k',linewidth=2)
53       else: #—-plot the other side
54            plt.plot([xg[4],xg[5]],[yg[4],yg[5]],color='k',linewidth=2)
55            plt.plot([xg[5],xg[6]],[yg[5],yg[6]],color='k',linewidth=2)
56            plt.plot([xg[6],xg[7]],[yg[6],yg[7]],color='k',linewidth=2)
57            plt.plot([xg[7],xg[4]],[yg[7],yg[4]],color='k',linewidth=2)
58
59       v04x=x[4]-x[0] #———0,3,7,4 face
60       v04y=y[4]-y[0]
61       v04z=z[4]-z[0]
62       v03x=x[3]-x[0]
63       v03y=y[3]-y[0]
64       v03z=z[3]-z[0]
65       nz=v04x*v03y-v04y*v03x
66       if nz<=0 :
67            plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k',linewidth=2)
68            plt.plot([xg[3],xg[7]],[yg[3],yg[7]],color='k',linewidth=2)
69            plt.plot([xg[7],xg[4]],[yg[7],yg[4]],color='k',linewidth=2)
70            plt.plot([xg[4],xg[0]],[yg[4],yg[0]],color='k',linewidth=2)
71       else: #———plot the other side
72            plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k',linewidth=2)
73            plt.plot([xg[2],xg[6]],[yg[2],yg[6]],color='k',linewidth=2)
74            plt.plot([xg[6],xg[5]],[yg[6],yg[5]],color='k',linewidth=2)
75            plt.plot([xg[5],xg[1]],[yg[5],yg[1]],color='k',linewidth=2)
76
77       v01x=x[1]-x[0] #—0,1,5,4 face
78       v01y=y[1]-y[0]
79       v01z=z[1]-z[0]
80       v04x=x[4]-x[0]
81       v04y=y[4]-y[0]
82       v04z=z[4]-z[0]
83       nz=v01x*v04y-v01y*v04x
84       if nz<=0 :
85            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linewidth=2)
86            plt.plot([xg[1],xg[5]],[yg[1],yg[5]],color='k',linewidth=2)
87            plt.plot([xg[5],xg[4]],[yg[5],yg[4]],color='k',linewidth=2)
88            plt.plot([xg[4],xg[0]],[yg[4],yg[0]],color='k',linewidth=2)
89       else: #———plot the other side
90            plt.plot([xg[3],xg[2]],[yg[3],yg[2]],color='k',linewidth=2)
91            plt.plot([xg[2],xg[6]],[yg[2],yg[6]],color='k',linewidth=2)
92            plt.plot([xg[6],xg[7]],[yg[6],yg[7]],color='k',linewidth=2)
93            plt.plot([xg[7],xg[3]],[yg[7],yg[3]],color='k',linewidth=2)
94
95       plt.scatter(xc,yc,s=5,color='k') #–plot a dot at the center
96       plt.axis([0,150,100,0]) #–replot axes and grid
97       plt.axis('on')
98       plt.grid(True)
99       plt.show() #–plot latest rotation
100
101 #==============================transform coordinates and plot functions
102 def plotboxx(xc,yc,zc,Rx): #——————transform & plot Rx box
103     for i in range(len(x)):
104           [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
105           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]  
106
107     plotbox(xg,yg,zg) #—————plot
108
109 def plotboxy(xc,yc,zc,Ry):
110      for i in range(len(x)): #——————transform & plot Ry box
111           [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
112           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
113
114     plotbox(xg,yg,zg)
115
116 def plotboxz(xc,yc,zc,Rz):
117      for i in range(len(x)): #——————transform  &  plot  Rz  box
118           [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
119           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
120
121      plotbox(xg,yg,zg)
122
123 #——————————————————————plot  box
124 xc=75 #–center coordinates
125 yc=50
126 zc=50
127
128 while True:
129      axis=input('x, y or z?: ') #———input axis of rotation (lower case)
130      if axis == 'x': #–if x axis
131             Rx=radians(float(input('Rx Degrees?: '))) #———input degrees of rotation
132             plotboxx(xc,yc,zc,Rx) #———call function plotboxx
133      if axis == 'y':
134             Ry=radians(float(input('Ry Degrees?: '))) #———input degrees of rotation
135             plotboxy(xc,yc,zc,Ry)
136      if axis == 'z':
137             Rz=radians(float(input('Rz Degrees?: '))) #———input degrees
138             plotboxz(xc,yc,zc,Rz)
139      if axis == ":
104           break
Listing 6-1Program HLBOX

6.2 金字塔

清单 6-2 用于绘制图形 6-6 和 6-7 。所用型号如图 6-8 所示。该分析类似于上一节中用于盒子的分析。不同之处在于,有四个面要处理,而且没有一个面是*行的,就像盒子一样,因此您必须独立处理每个面,以查看它是面向还是背离观察者。隐藏线在程序行 54-56、67-69 和 77-79 中被绘制为点。要删除这些点,请将这些行中的“:”替换为“”。清单 6-2 中的代码应该是不言自明的。

A456962_1_En_6_Fig8_HTML.jpg

图 6-8

Model for Listing 6-2. N not to scale.

A456962_1_En_6_Fig7_HTML.jpg

图 6-7

Pyramid with hidden lines removed: Rx=30°, Ry=45°, Rz=-90° (produced by Listing 6-2)

A456962_1_En_6_Fig6_HTML.jpg

图 6-6

Pyramid with hidden lines removed: Rx=30°, Ry=45°, Rz=0° (produced by Listing 6-2)

1   """
2   HLPYRAMID
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians
8   #————————————————————define lists
9   x=[0,-10,0,10]
10  y=[-20,0,0,0]
11  z=[0,10,-15,10]
12
13  xg=[0]*len(x)
14  yg=[0]*len(x)
15  zg=[0]*len(x)
16
17  #============================================define rotation function
18  def rotx(xc,yc,zc,xp,yp,zp,Rx):
19       xpp=xp
20       ypp=yp*cos(Rx)-zp*sin(Rx)
21       zpp=yp*sin(Rx)+zp*cos(Rx)
22       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
23       return[xg,yg,zg]
24
25  def roty(xc,yc,zc,xp,yp,zp,Ry):
26       xpp=xp*cos(Ry)+zp*sin(Ry)
27       ypp=yp
28       zpp=-xp*sin(Ry)+zp*cos(Ry)
29       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
30       return[xg,yg,zg]
31
32  def rotz(xc,yc,zc,xp,yp,zp,Rz):
33       xpp=xp*cos(Rz)-yp*sin(Rz)
34       ypp=xp*sin(Rz)+yp*cos(Rz)
35       zpp=zp
36       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
37       return[xg,yg,zg]
38
39  #======================================define pyramid plotting function
40
41  def plotpyramid(xg,yg,zg):
42       v01x=x[1]-x[0] #———0,1,2 face
43       v01y=y[1]-y[0]
44       v01z=z[1]-z[0]
45       v02x=x[2]-x[0]
46       v02y=y[2]-y[0]
47       v02z=z[2]-z[0]
48       nz=v01x*v02y-v01y*v02x
49       if nz<=0 :
50            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linewidth=2)
51            plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k',linewidth=2)
52            plt.plot([xg[2],xg[0]],[yg[2],yg[0]],color='k',linewidth=2)
53       else:
54            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linestyle=':')
55            plt.plot([xg[1],xg[2]],[yg[1],yg[2]],color='k',linestyle=':')
56            plt.plot([xg[2],xg[0]],[yg[2],yg[0]],color='k',linestyle=':')
57
58       v03x=x[3]-x[0] #—0,2,3 face
59       v03y=y[3]-y[0]
60       v03z=z[3]-z[0]
61       nz=v02x*v03y-v02y*v03x
62       if nz<=0 :
63            plt.plot([xg[0],xg[2]],[yg[0],yg[2]],color='k',linewidth=2)
64            plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k',linewidth=2)
65            plt.plot([xg[2],xg[3]],[yg[2],yg[3]],color='k',linewidth=2)
66       else:
67            plt.plot([xg[0],xg[2]],[yg[0],yg[2]],color='k',linestyle=':')
68            plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k',linestyle=':')
69            plt.plot([xg[2],xg[3]],[yg[2],yg[3]],color='k',linestyle=':')
70
71       nz=v03x*v01y-v03y*v01x #—0,2,3 face
72       if nz<=0 :
73            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linewidth=2)
74            plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k',linewidth=2)
75            plt.plot([xg[1],xg[3]],[yg[1],yg[3]],color='k',linewidth=2)
76       else:
77            plt.plot([xg[0],xg[1]],[yg[0],yg[1]],color='k',linestyle=':')
78            plt.plot([xg[0],xg[3]],[yg[0],yg[3]],color='k',linestyle=':')
79            plt.plot([xg[1],xg[3]],[yg[1],yg[3]],color='k',linestyle=':')
80
81       v21x=x[1]-x[2] #———1,2,3 face
82       v21y=y[1]-y[2]
83       v21z=z[1]-z[2]
84       v23x=x[3]-x[2]
85       v23y=y[3]-y[2]
86       v23z=z[3]-z[2]
87       nz=v21x*v23y-v21y*v23x
88       if nz¡0:
89            plt.plot([x[2],x[1]],[y[2],y[1]])
90            plt.plot([x[1],x[3]],[y[1],y[3]])
91            plt.plot([x[3],x[2]],[y[3],y[2]])
92
93       plt.scatter(xc,yc,s=5,color='k') #———plot a dot at the center
94       plt.axis([0,150,100,0]) #———replot axes and grid
95       plt.axis('on')
96       plt.grid(True)
97       plt.show() #–plot latest rotation
98
99  #========================transform coordinates and plotting fucntions
100 def plotpyramidx(xc,yc,zc,Rx): #——————transform & plot Rx pyramid
101      for i in range(len(x)):
102            [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx) 
103            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
104
105      plotpyramid(xg,yg,zg) #—————plot
106
107 def plotpyramidy(xc,yc,zc,Ry):
108      for i in range(len(x)): #——————transform & plot Ry pyramid
109            [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
110            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
111
112      plotpyramid(xg,yg,zg)
113
114 def plotpyramidz(xc,yc,zc,Rz):
115      for i in range(len(x)): #——————transform & plot Rz pyramid
116            [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
117            [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
118
119      plotpyramid(xg,yg,zg)
120
121 #———————————————————plot pyramids
122 xc=75 #——center coordinates
123 yc=50
124 zc=50
125
126 while True:
127      axis=input('x, y or z?: ') #———input axis of rotation (lower case)
128      if axis == 'x': #———if x axis
129            Rx=radians(float(input('Rx Degrees?: '))) #———input degrees of rotation
130            plotpyramidx(xc,yc,zc,Rx) #———call function plotpyramidx
131      if axis == 'y':
132            Ry=radians(float(input('Ry Degrees?: '))) #———input degrees of rotation
133            plotpyramidy(xc,yc,zc,Ry)
134      if axis == 'z':
135            Rz=radians(float(input('Rz Degrees?: '))) #———input degrees of rotation
136            plotpyramidz(xc,yc,zc,Rz)
137      if axis == ":
138            break
Listing 6-2Program HLPYRAMID

6.3 飞机

接下来是物体间隐藏线去除的例子。图 6-9 显示了两个*面(a)和(b);图 6-10 显示相同的两个*面部分重叠。很快您就会看到,*面(b)实际上位于*面(a)的下方,应该被部分遮挡。图 6-11 显示了移除了*面(b)的隐藏线的*面。图 6-12 为另一示例。图 6-13 显示了*面(a)旋转的示例。

在这个简单的模型中,两个*面*行于 x,y *面,*面(b)位于*面(a)的后面(即在+z 方向的更远处)。您不需要关心*面坐标的 z 分量,因为您不会将它们旋转到*面之外(即,围绕 x 或 y 方向),尽管您将围绕 z 方向旋转*面(a ),但为此您不需要 z 坐标。

图 6-14 为清单 6-3 使用的型号。飞机(a)是用黑色画的,飞机(b)是用蓝色画的。单位向量和显示在*面(a)的角 0 处。当*面(b)或其一部分在(a)后面且不可见时,您可以使用光线追踪技术来移除隐藏线。从*面(b)的边 0-1 开始,逐行执行此操作。从*面(b)的 0 角开始,想象一条从该点发出的光线传播到一个位于-z 方向的观察者,他正看着 x,y *面。如果*面(a)不干扰该光线(即不覆盖该点),则绘制该点。如果*面(a)确实干涉,则不绘制。因此,这个问题变成了一个相交问题:确定来自*面(b)边上一点的射线是否与*面(a)相交。

一次处理一个*面(b)的边缘。从角 0 开始,沿着边 0-1 以小步前进到角 1。向量 H 表示点 H 在边 0-1 上的位置。列表 6-3 确定该点的位置,以及它是否位于*面(a)之下(即,如果从 h 发出的光线照射到*面(a))。如果没有,则绘制点 p;如果是的话,就不画 p。

在清单 6-3 中,第 14-18 行在全局坐标中建立了两个*面的坐标,准备绘图。第 21-32 行定义了一个函数 dlinea,它画出了*面(a)的边缘线。它一次处理一条边缘线。dlinea 不会在*面(a)的边上进行隐藏线检查,因为您指定*面(a)位于*面(b)之上。调用参数 x1、x2、y1、y2 是边缘线的起点和终点坐标。第 22 行中的 q 是该行的长度;uxa 和 uya 是沿着从 x1,y1 到 x2,y2 的边缘线指向的单位向量的 x 和 y 分量。第 27-32 行中的循环按照第 27 行中设置的步长 0 . 2,沿着从 x1,y1 到 x2,y2 的线推进该点。第 28 和 29 行中的 hx 和 hy 是沿线 h 点的坐标。hxstart 和 hystart 允许用短线段连接这些点,比点绘制成点更好看。

第 35-38 行通过调用函数 dlinea 用四条边中每条边的开始和结束坐标来绘制*面(a)的边。线 40-42 确定了从*面(a)的角 0 到角 3 的距离 qa03。第 43 和 44 行中的 uxa 和 uya 是单位向量的 x 和 y 分量,单位向量从角 0 指向角 3。类似地,第 46-50 行给出了$$ \widehat{\mathbf{v}} $$的分量,一个从角 0 指向 1 的单位向量。他们将被要求进行相交检查,就像上一章中对线/*面相交所做的那样。

函数 dlineb 类似于 dlinea,只是调用参数现在包括 agx[0]和 agy[0],即*面(a)的角 0 的坐标。此外,该功能包括第 64 行和第 71 行之间的干扰检查。这被标为内部/外部检查。在第 64 行中,a 是点 h 的 x 坐标与*面(a)的角 0 的 x 坐标之间的距离;第 65 行中的 b 是 y 距离。这些基本上是向量 H 的 x 和 y 分量。在第 66 行,H 与单位向量的点(标量)积放弃了。这是 H 在*面(a)的 0-3 侧的投影。类似地,H 与第 67 行中的单位向量$$ \widehat{\mathbf{v}} $$的点积给出 vp,即 H 在*面(a)的 0-1 侧的投影。干扰检查很简单,总结在第 68 行。如果第 68 行中的所有问题都为真,则该点在第 69 行中以白色绘制,这意味着它是不可见的。如果第 68 行中的任何问题为假,这意味着该点没有被*面(a)阻挡,则第 71 行将其标为黑色。

你可能会问,为什么要用这种精心制作的向量分析?为什么不如图 6-14 所示,对照*面(a)的水*和垂直边界检查每个点的 x 和 y 坐标?如果两个*面都保持与 x 和 y 轴对齐,您可以这样做,如图所示。但是通过使用矢量方法,你可以使任一*面绕 z 方向旋转,如图 6-13 所示。

我通过指定*面(b)位于(a)之下来简化这个模型。一般来说,你可能不知道哪个*面离观察者更*,哪个应该是(a),哪个应该是(b)。这可以通过简单检查 z 坐标来完成。原则上,隐藏线删除过程与您在这里所做的类似,尽管编程在试图跟踪大量对象时会变得复杂。

A456962_1_En_6_Fig14_HTML.jpg

图 6-14

Model for Listing 6-3

A456962_1_En_6_Fig13_HTML.jpg

图 6-13

Two planes, one at an angle and overlapping the other, hidden lines removed by Listing 6-3

A456962_1_En_6_Fig12_HTML.jpg

图 6-12

Two planes, one overlapping the other, hidden lines removed by Listing 6-3

A456962_1_En_6_Fig11_HTML.jpg

图 6-11

Two planes overlapping, hidden lines removed by Listing 6-3

A456962_1_En_6_Fig10_HTML.jpg

图 6-10

Two planes, one partially overlapping the other, hidden lines not removed. Plane (b) is beneath (a).

A456962_1_En_6_Fig9_HTML.jpg

图 6-9

Two planes

1   """
2   HLPLANES
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sqrt, sin, cos, radians
8
9   plt.axis([0,150,100,0])
10  plt.axis('off')
11  plt.grid(False)
12
13  #———————————————————-define lists
14  axg=[40,80,80,40]
15  ayg=[20,20,60,60]
16
17  bxg=[20,120,120,20]
18  byg=[30,30,55,55]
19
20  #================================================define function dlinea
21  def dlinea(x1,x2,y1,y2):
22       q=sqrt((x2-x1)**2+(y2-y1)**2)
23       uxa=(x2-x1)/q
24       uya=(y2-y1)/q
25       hxstart=x1
26       hystart=y1
27       for l in np.arange(0,q,.2):
28              hx=x1+l*uxa #———global hit coordinates along the line
29              hy=y1+l*uya
30              plt.plot([hxstart,hx],[hystart,hy],color='k')
31              hxstart=hx
32              hystart=hy
33
34  #—————————————————————plane (a)
35  dlinea(axg[0],axg[1],ayg[0],ayg[1]) #———plot plane (a)
36  dlinea(axg[1],axg[2],ayg[1],ayg[2])
37  dlinea(axg[2],axg[3],ayg[2],ayg[3])
38  dlinea(axg[3],axg[0],ayg[3],ayg[0])
39
40  a=axg[3]-axg[0] #———unit vector u plane (a)
41  b=ayg[3]-ayg[0]
42  qa03=sqrt(a*a+b*b)
43  uxa=a/qa03
44  uya=b/qa03
45
46  a=axg[1]-axg[0] #———unit vector v plane (a)
47  b=ayg[1]-ayg[0]
48  qa01=sqrt(a*a+b*b)
49  vxa=a/qa01
50  vya=b/qa01
51
52  #=============================================================lineb( )
53  def dlineb(x1,x2,y1,y2,ax0,ay0):
54       a=x2-x1 #———unit vector line
55       b=y2-y1
56       ql=sqrt(a*a+b*b)
57       uxl=a/ql
58       uyl=b/ql
59       hxglast=x1
60       hyglast=y1
61       for l in np.arange(0,ql,.5):
62             hxg=x1+l*uxl
63             hyg=y1+l*uyl
64             a=hxg-ax0 #———inside/outside check
65             b=hyg-ay0
66             up=a*uxa+b*uya
67             vp=a*vxa+b*vya
68             if 0<up<qa03 and 0<vp<qa01: #———is it inside (a)?
79                  plt.plot([hxglast,hxg],[hyglast,hyg],color=’white’)
70             else:
71                  plt.plot([hxglast,hxg],[hyglast,hyg],color=’k’)
72       hxglast=hxg
73       hyglast=hyg
74
75  #———————————————————plot plane (b)
76  dlineb(bxg[0],bxg[1],byg[0],byg[1],axg[0],ayg[0])
77  dlineb(bxg[1],bxg[2],byg[1],byg[2],axg[0],ayg[0])
78  dlineb(bxg[2],bxg[3],byg[2],byg[3],axg[0],ayg[0])
79  dlineb(bxg[3],bxg[0],byg[3],byg[0],axg[0],ayg[0])
80
81  plt.show()
Listing 6-3Program HLPLANES

6.4 球体

在第五章,你画了一个球体,但没有旋转它。背面的线条与正面的重叠,因此不可见,所以删除隐藏的线条不是问题。在本章中,您将绘制一个球体并旋转它,同时移除背面的隐藏线。

图 6-15 和 6-18 显示了清单 6-4 的输出示例,它绘制了一个删除了隐藏线的球体。图 6-15 和 6-16 中的垂直线为纵向,以绿色绘制;水*纬度用蓝色标出。这个程序使用了一个隐藏线移除方案,就像你之前使用的盒子和金字塔一样。如果垂直于一个点的向量的 z 分量是正的(即指向远离位于-z 方向的观察者),则不绘制该点;否则就画了。

在清单 6-4 中,第 14 行将列表 g[]的长度设置为 3。这将用于从 rotx、roty 和 rotz 旋转函数中返回全局坐标 xg、yg 和 zg,这些函数在第 24-40 行中定义(它们与前面程序中使用的函数相同)。经度绘制在 55-79 行。该型号与第五章中清单 5-5 中使用的型号相同。第 55 行和第 79 行之间的算法计算经度上每个点的位置,一次一个,并旋转它。即每个点分别建立和旋转;除了 g[ ]列表外,不使用其他列表。从第 55 行开始的α环以第 47-49 行设置的 6 度步长从α= 0°到α= 360°扫描经度。在每一个α步,ϕ环画出一个经度,从-90 度开始,以 6 度为步长到+90 度。第 57-59 行的几何图形取自清单 5-5 。旋转前一点的坐标(Rx=0,Ry=0,Rz=0)为 xp,yp,zp,如 57-59 行所示。这个点位于球面上的球面坐标α,ϕ.线 60 围绕 x 方向旋转该点一个角度 Rx。这在第 61-63 行产生了新的坐标 xp,yp,zp。第 64 行围绕 y 方向旋转这些新坐标上的点。线 68 使它绕 z 方向旋转。这就产生了点的最终位置。

接下来,您必须确定该点是否在球体的背面,并且从视图中隐藏。如果为真,则不绘制。第 73-79 行执行这个功能。首先,在第 73-75 行中,你建立了连接第一点和第二点的直线的起始坐标。你用线而不是点来连接这些点,因为线看起来更好。第 73 行询问 phi 是否等于 phi1,即 phi 循环中的起始角度。如果是,开始坐标 xpglast 和 ypglast 被设置为等于循环计算的第一个坐标。接下来,在第 76 行,你问 nz,从球体中心到该点的向量的 z 分量,是否小于 0。nz 在第 72 行计算。如果为真,你知道这个点对于位于-z 方向的观察者是可见的;然后,该点通过线 77 连接到前一点。

第 77 行的 plt.plot()函数需要两组坐标:xpglast,ypglast 和 xpg,ypg。在循环的第一个循环中,起始坐标 xpglast,ypglast 设置为等于 xpg,ypg,这意味着第一个点与其自身相连,因此绘制的第一条线的长度为零。之后,在第 78-79 行中设置前一个点的坐标。第 73 行确定它是否是第一个点。如果第 76 行中的 nz 大于零,则该点位于旋转球体的背面,并且不可见,因此不会被绘制。坐标 xpglast 和 ypglast 仍然必须更新,这是在第 78-79 行完成的。纬度的处理方式基本相同,尽管几何图形不同,如清单 5-5 所述。可以通过改变第 77 行和第 104 行的 color='color '值来改变经度和纬度的颜色。

当运行这个程序时,请记住旋转不是像前面的一些程序那样是累加的。第 51-53 行中指定的旋转角度是球体将终止的角度;它们不会添加到任何以前的旋转中。要将球体旋转到另一个方向,请更改第 51-53 行中的值。

正如在关于连接的讨论中提到的,旋转的顺序很重要。Rx 后跟 Ry 不会给出与 Ry 后跟 Rx 相同的结果。该程序具有函数调用序列 Rx、Ry、Rz,如经度的第 60、64 和 68 行以及纬度的第 87、91 和 95 行所指定的。要更改旋转顺序,请更改这些函数调用的顺序。

图 6-17 和 6-18 中显示的球体具有黑色背景。为此,在列表 6-4 中任何其他绘图命令之前插入以下行,例如在第 12 行之后:

#———————————————————paint the background
for y in np.arange(1,100,1):
     plt.plot([0,150],[y,y],linewidth=4,color='k')

这将在绘图窗口中绘制从 x=0 到 x=150 以及从 y=1 到 y=100 的黑线。这将使用黑色背景填充该区域。颜色可以改变成任何想要的。为了防止水*线之间出现间隙,线宽被设置为 4。背景必须在构建球体之前绘制,因为您使用的是线段。新线覆盖旧线,因此按照这个顺序,球体线段将覆盖背景线;否则背景线会过度绘制球体。

在图 6-15 和 6-16 中,程序行 77 和 104 中的球体线宽设置为. 5。这在清晰的背景上给出了良好的结果,但是当背景变为黑色时,线条过于柔和。因此,在插入上面两行代码的同时,清单 6-4 中的行宽应该改为更大的值,比如 1.0。图 6-17 和 6-18 所示颜色为“浅绿色”。有些颜色不适合黑色背景,但浅绿色似乎很适合;你只需要尝试。

A456962_1_En_6_Fig18_HTML.jpg

图 6-18

Rotated sphere with hidden lines removed: Rx=60°, Ry=20°, Rz=10°, black background (produced by Listing 6-4)

A456962_1_En_6_Fig17_HTML.jpg

图 6-17

Rotated sphere with hidden lines removed: Rx=40°, Ry=-20°, Rz=40°, black background (produced by Listing 6-4)

A456962_1_En_6_Fig16_HTML.jpg

图 6-16

Rotated sphere with hidden lines removed: Rx=40°, Ry=-20°, Rz=40° (produced by Listing 6-4)

A456962_1_En_6_Fig15_HTML.jpg

图 6-15

Rotated sphere with hidden lines removed: Rx=55°, Ry=-20°, Rz=-40° (produced by Listing 6-4)

1   """
2   HLSPHERE
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians, sqrt
8
9   plt.axis([0,150,100,0])
10  plt.axis('off')
11  plt.grid(False)
12
13  #———————————————————————lists
14  g=[0]*3
15
16  #—————————————————————parameters
17  xc=80 #———sphere center
18  yc=50
19  zc=0
20
21  rs=40 #———sphere radius
22
23  #=========================================================
24  def rotx(xc,yc,zc,xp,yp,zp,Rx):
25       g[0]=xp+xc
26       g[1]=yp*cos(Rx)-zp*sin(Rx)+yc
27       g[2]=yp*sin(Rx)+zp*cos(Rx)+zc
28       return[g]
29
30  def roty(xc,yc,zc,xp,yp,zp,Ry):
31       g[0]=xp*cos(Ry)+zp*sin(Ry)+xc
32       g[1]=yp+yc
33       g[2]=-xp*sin(Ry)+zp*cos(Ry)+zc
34       return[g]
35
36  def rotz(xc,yc,zc,xp,yp,zp,Rz):
37       g[0]=xp*cos(Rz)-yp*sin(Rz)+xc
38       g[1]=xp*sin(Rz)+yp*cos(Rz)+yc
39       g[2]=zp+zc
40       return[g]
41
42  #————————————————-longitudes and latitudes
43  phi1=radians(-90)
44  phi2=radians(90)
45  dphi=radians(6)
46
47  alpha1=radians(0)
48  alpha2=radians(360)
49  dalpha=radians(6)
50
51  Rx=radians(45)
52  Ry=radians(-20)
53  Rz=radians(40)
54
55  for alpha in np.arange(alpha1,alpha2,dalpha):  #———longitudes
56       for phi in np.arange(phi1,phi2,dphi):
57            xp=rs*cos(phi)*cos(alpha)
58            yp=rs*sin(phi)
59            zp=-rs*cos(phi)*sin(alpha)
60            rotx(xc,yc,zc,xp,yp,zp,Rx)
61            xp=g[0]-xc
62            yp=g[1]-yc
63            zp=g[2]-zc
64            roty(xc,yc,zc,xp,yp,zp,Ry)
65            xp=g[0]-xc
66            yp=g[1]-yc
67            zp=g[2]-zc
68            rotz(xc,yc,zc,xp,yp,zp,Rz)
69            xpg=g[0]
70            ypg=g[1]
71            zpg=g[2]
72            nz=zpg-zc
73            if phi == phi1:
74                   xpglast=xpg
75                   ypglast=ypg
76            if nz < 0:
77                   plt.plot([xpglast,xpg],[ypglast,ypg],linewidth=.5,color='g')
78            xpglast=xpg
79            ypglast=ypg
80
81  for phi in np.arange(phi1,phi2,dphi):  #—————latitudes
82       r=rs*cos(phi)
83       for alpha in np.arange(alpha1,alpha2+dalpha,dalpha):
84            xp=r*cos(alpha)
85            yp=rs*sin(phi)
86            zp=-rs*cos(phi)*sin(alpha)
87            rotx(xc,yc,zc,xp,yp,zp,Rx)
88            xp=g[0]-xc
89            yp=g[1]-yc
90            zp=g[2]-zc
91            roty(xc,yc,zc,xp,yp,zp,Ry)
92            xp=g[0]-xc
93            yp=g[1]-yc
94            zp=g[2]-zc
95            rotz(xc,yc,zc,xp,yp,zp,Rz)
96            xpg=g[0]
97            ypg=g[1]
98            zpg=g[2]
99            nz=zpg-zc
100           if alpha == alpha1:
101                  xpglast=xpg
102                  ypglast=ypg
103           if nz < 0:
104                   plt.plot([xpglast,xpg],[ypglast,ypg],linewidth=.5,color='b')
105           xpglast=xpg
106           ypglast=ypg
107
108 plt.show()
Listing 6-4Program HLSPHERE

6.5 总结

您学习了如何删除单个对象和对象之间的隐藏线。在单个对象的情况下,比如盒子、金字塔和球体,你可以毫不费力地构造算法。当从单独的对象(如两个*面)中删除隐藏线时,您依赖于从点或从一个点到另一个点的短线段构建其中一个对象的技术。不管是哪种情况,你都还在和点打交道。从一个*面上的一个点,你画了一条假想的线,一条到-z 方向的观察者的射线。然后你检查光线是否与另一个*面相交。您使用了在第五章中开发的线-*面相交算法。如果它相交,则该点被隐藏,并且它或与之相连的线段不会被绘制。你用两架飞机来探索这项技术。你可以使用你在第五章中用过的任何其他形状。例如,通过用点构建*面并使用第五章中的交集算法,你可以很容易地从一个圆形线段下的*面中删除隐藏线。但是,您可能无法提前知道哪个对象覆盖哪个对象。你可以粗略地检查一下来回答这个问题。例如,在两个*面的情况下,如果一个*面的所有四个角的 z 坐标都小于另一个,则它更靠*观察者,在这种情况下,它可能会覆盖另一个*面的一部分。在这种情况下,应该检查另一个*面的隐藏线。

七、着色

在这一章中,你将学习如何给三维物体着色。阴影产生了更加真实的外观,并增强了立体感。总的想法是首先建立光线照射物体的方向,然后确定光线在物体表面的阴影效果。以我接下来将要讨论的盒子为例,六个*面构成了盒子的表面。这些*面相对于光的方向的取向将决定每个*面上的阴影程度。为了模拟阴影,可以用点或线填充*面。通过改变点或线的颜色强度以及通过颜色混合,可以获得不同的阴影强度。

正常情况下,正在绘制的对象将出现在白色背景上。如果使用背景色,如图 7-13 所示,可使用点或线来绘制背景。回想一下第一章,新点覆盖旧点和旧线总是覆盖旧点和旧线。这意味着无论被着色的对象是由点还是线构成的,如果它是用点绘制的,它们都会过度渲染背景色。使用圆点的缺点是用圆点填充背景要花很多时间。在这方面,线条是更好的选择,如果对象可以由线条构成,则首选线条。如果你必须在你的对象中使用点,那么你必须使用点作为你的背景色。

着色程序的核心是强度函数,它将着色强度与*面相对于入射光方向的方向联系起来。您不需要指定光源的位置;您可以定义从该光源照射到对象的光线的方向。例如,假设程序计算出*面和入射光线之间的角度为 50 度。强度函数将该角度转换为阴影强度,用于改变线条或点的颜色强度。

为了产生更逼真的计算机绘制图像,已经对阴影理论进行了大量的研究。这些图像通常针对每种原色具有单独的着色功能,并且考虑了表面材料的反射率和物理特性。光滑的表面将具有高反射性,而粗糙、有纹理的表面将散射入射光,产生更高程度的扩散性。在这里的工作中,您将保持简单,只使用一个着色函数,并忽略会影响表面反射率和扩散率的表面特征的差异,尽管它们可以很容易地引入到程序中。此外,假设曲面的明暗度仅取决于该曲面相对于光源的方向,而不取决于其相对于观察点的方向,通常,观察点位于-z 方向。

7.1 给方框加阴影

图 7-1 到 7-7 显示了列表 7-1 的输出样本。它们显示了一个旋转到不同方向的盒子,其表面带有阴影。它们以从黑到白的不同强度用单色黑色着色。

A456962_1_En_7_Fig7_HTML.jpg

图 7-7

Shaded box produced by Listing 7-1, Io=.4

A456962_1_En_7_Fig6_HTML.jpg

图 7-6

Shaded box produced by Listing 7-1, Io=.6

A456962_1_En_7_Fig5_HTML.jpg

图 7-5

Shaded box produced by Listing 7-1, Io=.8

A456962_1_En_7_Fig4_HTML.jpg

图 7-4

Shaded box produced by Listing 7-1, Io=1.0

A456962_1_En_7_Fig3_HTML.jpg

图 7-3

Shaded box produced by Listing 7-1, Io=1.0

A456962_1_En_7_Fig2_HTML.jpg

图 7-2

Shaded box produced by Listing 7-1, Io=1.0

A456962_1_En_7_Fig1_HTML.jpg

图 7-1

Shaded box produced by Listing 7-1, Io=.8

图 7-9 为清单 7-1 使用的型号。光源显示在左上角。你不需要明确地说明它的位置,只需要说明从它发出的光线的方向。你可以通过指定 lx、ly 和 lz 来实现,它们是一个单位向量$$ \widehat{\mathbf{l}} $$的组成部分,它与光线对齐。请记住,$$ \widehat{\mathbf{l}} $$是一个单位矢量,因此必须遵守其分量之间的以下关系:

$$ \sqrt{l{x}²+l{y}²+l{z}²}=1 $$

(7-1)

观察由角 0,1,2,3 定义的盒子的顶面,你可以在角 0 处看到一个单位法向量$$ \widehat{\mathbf{n}} $$。这从*面指向外。通过绘制从 B 到 e 横跨*面宽度的蓝色线条,可以对长方体进行着色。这些线条从边 0,1 到 3,2,然后沿*面向下绘制,从而对其进行着色。每个面上的线条的强度取决于$$ \widehat{\mathbf{n}} $$$$ \widehat{\mathbf{l}} $$的方向。你通过取$$ \widehat{\mathbf{n}} $$$$ \widehat{\mathbf{l}} $$的点积得到这个方向。如果$$ \widehat{\mathbf{n}} $$正对$$ \widehat{\mathbf{l}} $$,点积会为负,线条的强度会更小,也就是说色调会更浅;如果$$ \widehat{\mathbf{n}} $$背向$$ \widehat{\mathbf{l}} $$,则点积为正,强度更大,色调更暗。

图 7-10 对此进行了说明,显示了阴影强度 I 与$$ \widehat{\mathbf{n}}\cdot \mathbf{l} $$的关系。这是一个线性关系。正如您将在下一节中看到的,使用非线性关系和混合(r,g,b)颜色可以获得更好的结果。你可以通过考察得到这个线性强度函数的一个方程:

$$ I=\frac{Io}{2}+\frac{Io}{2}\widehat{\mathrm{n}}\cdot \widehat{\mathrm{l}} $$

(7-2)

$$ \boxed{I=\frac{Io}{2}\left(1=\widehat{\mathrm{n}}\cdot \widehat{\mathrm{l}}\right)} $$

(7-3)

请注意参数 Io。它通过增加或减少颜色的强度来控制阴影区域的黑暗程度。从 B 到 E 的线是用 plt.plot()函数绘制的,该函数包含属性 alpha。通过让 alpha=I,你可以控制颜色的强度。较高的 alpha 值会增加强度,使阴影区域看起来更暗;较低的 alpha 值会降低亮度,从而创建看起来更亮的区域。注意,α可以取从 0 到 1 的值,因此 I 限于相同的值范围。根据等式 7-3 ,这意味着 Io 可以有最大值 1。Io=1 将给出最暗、最强烈的色调。为了用更微妙的色调柔化图像,将 Io 降低到小于 1 的值。为了进一步修改功能,可以将左侧抬高,这将使灯光变暗。如果函数是水*的,所有的阴影将是一致的。为了查看 Io 对阴影的影响,图 7-2 到 7-4 的 Io=1.0。图 7-1 、 7-5 、 7-6 和 7-7 分别有 Io=.8、. 8、. 6 和. 4。颜色不一定是黑色或原色;它们可以混合使用。图 7-8 显示了使用 color=(r,g,b)的结果,其中 r=.5,g=0,b=.5,

$$ color=\left(.5,,0,,.5\right) $$

【7-4】是等量的红色和蓝色的紫色混合。回想一下,( r,g,b)混合中的红色、绿色和蓝色的值必须在 0 和 1 之间。

你已经将你的阴影强度 I 应用于单色。即使使用 r、g、b 混色,也还是单色底纹,虽然不是原色。这种方法的扩展将是对三原色中的每一种应用单独的强度。例如,当一个艺术家画一幅肖像时,他/她可能会把脸的浅色一侧渲染成浅粉色。为了使阴影部分变暗,他/她通常会在混合色中加入绿色,红色的补充色。如果你仔细观察一位有成就的艺术家的肖像,你会发现通常都是这样做的。很少有人加入黑色来加深颜色。事实上,许多画家甚至没有把黑色颜料放在他们的托盘上;他们通过混合赞美的色调来获得更深的颜色。红色的恭维是绿色;黄色是紫色。当然,绘画中的颜色混合并不那么简单,但这是最基本的想法。为了在编程中实现这一点,假设您正在使用(r,g,b)混合色给一个红色框加阴影。不是对红色应用强度因子来增加其强度,从而模拟变暗,而是对绿色应用强度因子,增加其在 r、g、b 混合中的贡献,从而使红色变暗。目前,在清单 7-1 中,你将保持简单,通过增加黑暗区域的颜色强度而不是使用颜色混合来模拟阴影。这适用于单色黑色图像,尽管它对彩色对象有限制。

清单 7-1 中盒子的定义包含在第 10、11 和 12 行的列表中。第 14、15 和 16 行打开了全局坐标列表,它们由 rotx、roty 和 rotz 旋转函数返回。它们与 len(x)指定的 x,y,z 列表具有相同的长度。

清单 7-1 ,第 54-84 行定义了一个名为 shade()的新函数。shade()在第 54 行接收到的参数如图 7-11 所示。当对特定*面调用 shade()时,盒子的角必须遵循图 7-11 所示的顺序。例如,*面 1,5,6,2 的顺序如图 7-12 所示。可能需要一些视觉训练来确定盒子的六个*面的方向,使它们符合图 7-11 中的顺序。六个*面中的每一个都是通过六次调用函数 shade()分别绘制和着色的。它们列在第 88-93 行。调用的参数分别是点 a、b、c、d 的 x、y、z 坐标。函数 shade()在第 55-61 行计算单位向量的分量,在第 62-68 行计算 vˇ的分量。第 69-71 行计算单位向量$$ \widehat{\mathbf{n}} $$的分量。$$ \widehat{\mathbf{n}} $$上与入射光线单位矢量$$ \widehat{\mathbf{l}} $$的点积,其分量在第 23-25 行中指定,在第 72 行中计算为 ndotl 第 73 行的阴影强度。如果 nz < =0(即$$ \widehat{\mathbf{n}} $$指向-z 方向的观察者),则在第 75-78 行绘制面部的边缘,并在循环 79-84 中对面部进行着色。如图 7-11 所示,第 79 行的范围为 h,从 0 到 qad,即从角 a 到 d 的距离,该距离在第 58 行中计算,步长为 1。第 80-81 行计算直线起点的 x 和 y 坐标;第 82 行和第 83 行获得了该行的终点坐标。第 84 行绘制了这条线。在行 84 中,α等于在行 73 中确定的阴影强度。盒子的颜色等于 clr,这是在第 27 行指定的;比如 color='k '会给出一个黑框。另一种方法是混合原色,如第 28 行所示。这将产生如图 7-8 所示的紫色方框。要得到这种颜色,只需去掉第 28 行中的#即可;否则,阴影将以黑色显示。我将在下一节更详细地讨论颜色混合。第 29 行规定了最大强度 i0。这可以是 0 到 1 之间的任何值。如果 nz > 0(即$$ \widehat{\mathbf{n}} $$指向远离观察者的方向),则不绘制该面。清单 7-1 的其余部分应该很熟悉。

A456962_1_En_7_Fig12_HTML.jpg

图 7-12

Plane 1,5,6,2

A456962_1_En_7_Fig11_HTML.jpg

图 7-11

Model of a generic plane used in Listing 7-1

A456962_1_En_7_Fig10_HTML.jpg

图 7-10

Shading function

A456962_1_En_7_Fig9_HTML.jpg

图 7-9

Shading model used by Listing 7-1

A456962_1_En_7_Fig8_HTML.jpg

图 7-8

Shaded box produced by Listing 7-1, (r,g,b)=(.5,0.,5) color mixing, Io=1.0

1   """
2   SHADEBOX
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians, sqrt
8
9   #—————————————————————————lists
10  x=[-20,20,20,-20,-20,20,20,-20]
11  y=[-10,-10,-10,-10,10,10,10,10]
12  z=[5,5,-5,-5,5,5,-5,-5]
13
14  xg=[0]*len(x)
15  yg=[0]*len(x)
16  zg=[0]*len(x)
17
18  #———————————————————————parameters
19  xc=75 #———center coordinates
20  yc=50
21  zc=50
22
23  lx=.707 #———light ray unit vector components
24  ly=.707
25  lz=0
26
27  clr='k' #———use this for black monochrome images, or use another color
28  #clr=(.5,0,.5) #———use this to mix colors, this mix produces purple
29  Io=.8 #———max intensity, must be 0 < 1
30
31  #=====================================================define rotation functions
32  def rotx(xc,yc,zc,xp,yp,zp,Rx):
33       xpp=xp
34       ypp=yp*cos(Rx)-zp*sin(Rx)
35       zpp=yp*sin(Rx)+zp*cos(Rx)
36       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
37       return[xg,yg,zg]
38
39  def roty(xc,yc,zc,xp,yp,zp,Ry):
40       xpp=xp*cos(Ry)+zp*sin(Ry)
41       ypp=yp
42       zpp=-xp*sin(Ry)+zp*cos(Ry)
43       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
44       return[xg,yg,zg]
45
46  def rotz(xc,yc,zc,xp,yp,zp,Rz):
47       xpp=xp*cos(Rz)-yp*sin(Rz)
48       ypp=xp*sin(Rz)+yp*cos(Rz)
49       zpp=zp
50       [xg,yg,zg]=[xpp+xc,ypp+yc,zpp+zc]
51       return[xg,yg,zg]
52
53  #==============================================================shading
54  def shade(ax,ay,az,bx,by,bz,cx,cy,cz,dx,dy,dz):
55       a=dx-ax
56       b=dy-ay
57       c=dz-az
58       qad=sqrt(a*a+b*b+c*c)
59       ux=a/qad
60       uy=b/qad
61       uz=c/qad
62       a=bx-ax
63       b=by-ay
64       c=bz-az
65       qab=sqrt(a*a+b*b+c*c)
66       vx=a/qab
67       vy=b/qab
68       vz=c/qab
69       nx=uy*vz-uz*vy
70       ny=uz*vx-ux*vz
71       nz=ux*vy-uy*vx
72       ndotl=nx*lx+ny*ly+nz*lz
73       I=.5*Io*(1+ndotl)
74       if nz<=0:
75            plt.plot([ax,bx],[ay,by],color='k',linewidth=1)
76            plt.plot([bx,cx],[by,cy],color='k',linewidth=1)
77            plt.plot([cx,dx],[cy,dy],color='k',linewidth=1)
78            plt.plot([dx,ax],[dy,ay],color='k',linewidth=1)
79            for h in np.arange(0,qad,1):
80                 xls=ax+h*ux
81                 yls=ay+h*uy
82                 xle=bx+h*ux
83                 yle=by+h*uy
84                 plt.plot([xls,xle],[yls,yle],linewidth=2,alpha=I,color=clr)
85
86  #=============================================================
87  def plotbox(xg,yg,zg):
88       shade(xg[0],yg[0],zg[0],xg[1],yg[1],zg[1],xg[2],yg[2],zg[2],xg[3],yg[3],zg[3])
89       shade(xg[7],yg[7],zg[7],xg[6],yg[6],zg[6],xg[5],yg[5],zg[5],xg[4],yg[4],zg[4])
90       shade(xg[0],yg[0],zg[0],xg[3],yg[3],zg[3],xg[7],yg[7],zg[7],xg[4],yg[4],zg[4])
91       shade(xg[1],yg[1],zg[1],xg[5],yg[5],zg[5],xg[6],yg[6],zg[6],xg[2],yg[2],zg[2])
92       shade(xg[3],yg[3],zg[3],xg[2],yg[2],zg[2],xg[6],yg[6],zg[6],xg[7],yg[7],zg[7])
93       shade(xg[4],yg[4],zg[4],xg[5],yg[5],zg[5],xg[1],yg[1],zg[1],xg[0],yg[0],zg[0])
94
95       plt.axis([0,150,100,0]) #———plot axes and grid
96       plt.axis('off')
97       plt.grid(False)
98       plt.show() #———plot latest rotation
99
100 #============================================================
101 def plotboxx(xc,yc,zc,Rx): #——————transform and plot Rx
102      for i in range(len(x)):
103           [xg[i],yg[i],zg[i]]=rotx(xc,yc,zc,x[i],y[i],z[i],Rx)
104           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
105
106           plotbox(xg,yg,zg) #—————plot
107
108 def plotboxy(xc,yc,zc,Ry):
109      for i in range(len(x)): #——————transform and plot Ry
110           [xg[i],yg[i],zg[i]]=roty(xc,yc,zc,x[i],y[i],z[i],Ry)
111           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
112
113           plotbox(xg,yg,zg)
114
115 def plotboxz(xc,yc,zc,Rz):
116      for i in range(len(x)): #——————transform and plot Rz
117           [xg[i],yg[i],zg[i]]=rotz(xc,yc,zc,x[i],y[i],z[i],Rz)
118           [x[i],y[i],z[i]]=[xg[i]-xc,yg[i]-yc,zg[i]-zc]
119
120           plotbox(xg,yg,zg)
121
122 #————————————————————————-input
123 while True:
124      axis=input('x, y or z?: ') #———input axis of rotation (lower case)
125      if axis == 'x': #–if x axis
126           Rx=radians(float(input('Rx Degrees?: '))) #———input degrees
127           plotboxx(xc,yc,zc,Rx) #———call function plotboxx
128      if axis == 'y':
129           Ry=radians(float(input('Ry Degrees?: '))) #———input degrees
130           plotboxy(xc,yc,zc,Ry)
131      if axis == 'z':
132           Rz=radians(float(input('Rz Degrees?: '))) #———input degrees
133           plotboxz(xc,yc,zc,Rz)
134      if axis == ":
135           break
Listing 7-1Program SHADEBOX

7.2 球体着色

在上一节中,您使用阴影函数的简单线性关系对一个框进行了阴影处理,其中阴影的强度 I 与点积$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$线性相关。在本节中,您将混合三原色,并使用非线性阴影功能控制每种颜色的强度。结果如图 7-13 所示,由清单 7-2 产生。

A456962_1_En_7_Fig13_HTML.jpg

图 7-13

Shading a sphere by color mixing with a non-linear intensity function (produced by Listing 7-2)

非线性阴影函数如图 7-14 中的红色、绿色和蓝色曲线所示;线性的是黑色的。非线性函数对阴影有更多的控制,可以产生更真实的效果。它们允许您通过放大和扩展较亮的阴影区域来控制阴影,同时更快地增加向较暗区域的亮度过渡。线性阴影函数类似于清单 7-1 中使用的函数,除了它现在从 I=IA 开始,其中 IA 可能大于零。这些曲线从 I=IA 开始,到 I=IB 结束,这里$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$ =+1。IA 和 IB 是可以在清单 7-2 中调整的参数。IA > 0 会使灯光变暗。这有时是必要的,因为当 I=0 或接* 0 时,音调可能不能很好地过渡到 I 的较高区域;有时可以观察到不连续性。为了纠正这一点,在 IA 大于 0 的某个小值处启动强度函数。增加 IA 也可以是一种降低亮区亮度的技术。

注意图 7-14 中$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}p $$$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$的区别。为了得到 I 与$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$的关系,你让函数的形式为

$$ I={C}_1+{C}_2{\left(\widehat{\mathrm{n}}\cdot \widehat{\mathrm{l}}p\right)}^n $$

(7-5),其中 C 1 和 C 2 是常数,n 是参数。n 可以在程序中改变。注意在$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}p=0 $$

$$ IA={C}_1+{C}_2{(0)}^n $$

(7-6)

$$ {C}_1= Ia $$

(7-7)处 I=IA

$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}p $$ = +2,($$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$ = +1),I=IB,

$$ IB= IA+{C}_2{(2)}^n $$

(7-8)

$$ {C}_2=\frac{IB- IA}{2^n} $$

(7-9)

$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}p=\widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}+\mathbf{1}, $$

$$ I= IA+\left( IB- IA\right){\left(\frac{\widehat{\mathrm{n}}\cdot \widehat{\mathrm{l}}+1}{2}\right)}^n $$

(7-10)

等式 7-10 是你的强度函数,$$ \mathrm{I}\left(\widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}\right) $$。因此,你有三个参数来调整 I: IA,它调节最亮区域的强度;IB,调节最暗的区域;以及调节从亮到暗的过渡的 n。n 值越高,过渡越快。图 7-14 显示了 n=1、2、3 和 4 的曲线。当 n=1 时,曲线变成线性。n、IA 和 IB 没有确定的值;它们应该通过反复试验来调整,以给出视觉上吸引人的结果。

关于颜色,图 7-13 所示的背景是‘午夜蓝’。一个很好的颜色样本来源是#https://matplotlib.org/examples/color/named_colors.html.

清单 7-2 通过绘制经度和纬度创建一个球体,就像你在清单 6-4 中所做的那样。在清单 6-4 中,这些间隔 6 度。为了执行清单 7-2 中的着色,您将把经度和纬度间隔得更*,相距 2 度,并根据垂直于表面$$ \widehat{\mathbf{n}} $$的局部单位矢量和表面上每一点的光源单位矢量$$ \widehat{\mathbf{l}} $$之间的角度来调整它们的绘图强度。然后,这将用于控制 r、g、b 对颜色混合的相对贡献。和以前一样,你通过点积$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$来建立这个关系。$$ \widehat{\mathbf{n}} $$通过获得一个从球体中心到球体表面上所讨论的点的矢量,然后除以球体的半径 rs,可以非常简单地确定每个点上的点。例如,假设你在球面上的一个点 p 上,坐标为 xp,yp,zp。从球体中心 xc,yc,zc 到 p 的矢量 Vp 是

$$ \mathbf{Vp}=\left( xp- xc\right)\widehat{\mathbf{i}}+\left( yp- yc\right)\widehat{\mathbf{j}}+\left( zp- zc\right)\widehat{\mathbf{k}} $$

(7-11)

Vp 在 p 处垂直于表面。单位法向量$$ \widehat{\mathbf{n}} $$

$$ \widehat{\mathbf{n}}=\left(\frac{xp- xc}{rs}\right)\widehat{\mathbf{i}}+\left(\frac{yp- yc}{rs}\right)\widehat{\mathbf{j}}+\left(\frac{zp- zc}{rs}\right)\widehat{\mathbf{k}} $$

(7-12),其中 rs 是球体的半径。取方程 7-12 中的$$ \widehat{\mathbf{n}} $$与入射光单位矢量$$ \widehat{\mathbf{l}} $$的点积得到$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$,你需要从方程 7-10 中确定 I。

在清单 7-2 中,第 22-24 行设置了入射光单位矢量的分量。第 26-28 行设置强度函数参数。这些值产生图 7-13 。第 37-39 行用点绘制背景。第 61-101 行标出了经度。注意,在第 69 和 70 行中,dalpha 和 dphi 被添加到 alpha2 和 phi2 中,因为 np.arange()函数中的舍入误差有时可能无法关闭球体;这确保它关闭。第 86-92 行确定了当前α和φ值下的$$ \widehat{\mathbf{n}} $$的分量。第 93 行计算点积$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}} $$;第 94 行计算强度。

在第 99 行,属性 linewidth 已经增加到 4。当与线 63 和 67 中的 2 度角间距结合时,这确保了在表面上没有间隙。同样在第 99 行,颜色声明显示红色为 100%,绿色为 80%,蓝色为 40%。(I-1)因子反映了着色功能的影响。回想一下,当颜色混合为(0,0,0)时,产生黑色;相反,当混合是(1,1,1)时,产生白色。由于您想要 I 接*或等于 1(背向光源)的暗色,所以(I-1)因子可以实现这一点,因为当 I=1 时它等于 0,从而产生黑色。如果你不包括(I-1)因素,混合(1.8.45)将简单地产生一个无阴影的圆形生锈的橙色圆盘。

A456962_1_En_7_Fig14_HTML.jpg

图 7-14

Nonlinear shading function

1   """
2   SHADESPHERE
3   """
4
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians, sqrt
8
9   plt.axis([0,150,100,0])
10  plt.axis('off')
11  plt.grid(False)
12
13  #—————————————————————————lists
14  g=[0]*3
15
16  #———————————————————————parameters
17  xc=80 #———sphere center
18  yc=50
19  zc=0
20  rs=35 #———sphere radius
21
22  lx=.707 #———light ray unit vector components
23  ly=.707
24  lz=0
25
26  IA=.01 #———define curve
27  IB=1
28  n=2.0
29
30  clrbg='midnightblue' #———background color
31
32  Rx=radians(-15) #———sphere angles of rotation
33  Ry=radians(0)
34  Rz=radians(30)
35
36  #———————————————————paint background color
37  for x in np.arange(0,150,1):
38       for y in np.arange(0,100,1):
39            plt.scatter(x,y,s=10,color='clrbg')
40
41  #============================================================rotation functions
42  def rotx(xc,yc,zc,xp,yp,zp,Rx):
43       g[0]=xp+xc
44       g[1]=yp*cos(Rx)-zp*sin(Rx)+yc
45       g[2]=yp*sin(Rx)+zp*cos(Rx)+zc
46       return[g]
47
48  def roty(xc,yc,zc,xp,yp,zp,Ry):
49       g[0]=xp*cos(Ry)+zp*sin(Ry)+xc
50       g[1]=yp+yc
51       g[2]=-xp*sin(Ry)+zp*cos(Ry)+zc
52       return[g]
53
53  def rotz(xc,yc,zc,xp,yp,zp,Rz):
55       g[0]=xp*cos(Rz)-yp*sin(Rz)+xc
56       g[1]=xp*sin(Rz)+yp*cos(Rz)+yc
57       g[2]=zp+zc
58       return[g]
59
60  #————————————————————longitudes
61  phi1=radians(-90)
62  phi2=radians(90)
63  dphi=radians(2)
64
65  alpha1=radians(0)
66  alpha2=radians(360)
67  dalpha=radians(2)
68
69  for alpha in np.arange(alpha1,alpha2+dalpha,dalpha):
70       for phi in np.arange(phi1,phi2+dphi,dphi):
71            xp=rs*cos(phi)*cos(alpha)
72            yp=rs*sin(phi)
73            zp=-rs*cos(phi)*sin(alpha)
74            rotx(xc,yc,zc,xp,yp,zp,Rx)
75            xp=g[0]-xc
76            yp=g[1]-yc
77            zp=g[2]-zc
78            roty(xc,yc,zc,xp,yp,zp,Ry)
79            xp=g[0]-xc
80            yp=g[1]-yc
81            zp=g[2]-zc
82            rotz(xc,yc,zc,xp,yp,zp,Rz)
83            xpg=g[0]
84            ypg=g[1]
85            zpg=g[2]
86            a=xpg-xc
87            b=ypg-yc
88            c=zpg-zc
89            qp=sqrt(a*a+b*b+c*c)
90            nx=a/qp
91            ny=b/qp
92            nz=c/qp
93            ndotl=nx*lx+ny*ly+nz*lz
94            I=IA+(IB-IA)*((1+ndotl)/2)**n
95            if phi == phi1:
96                xpglast=xpg
97                ypglast=ypg
98            if nz < 0:
99                plt.plot([xpglast,xpg],[ypglast,ypg],linewidth=4,color=((1-I),.8*(1-I),.45*(1-I))
100               xpglast=xpg
101               ypglast=ypg
102
103 #————————————————————latitudes
104 for phi in np.arange(phi1,phi2+dphi,dphi):
105      r=rs*cos(phi)
106      for alpha in np.arange(alpha1,alpha2+dalpha,dalpha):
107            xp=r*cos(alpha)
108            yp=rs*sin(phi)
109            zp=-rs*cos(phi)*sin(alpha)
110            rotx(xc,yc,zc,xp,yp,zp,Rx)
111            xp=g[0]-xc
112            yp=g[1]-yc
113            zp=g[2]-zc
114            roty(xc,yc,zc,xp,yp,zp,Ry)
115            xp=g[0]-xc
116            yp=g[1]-yc
117            zp=g[2]-zc
118            rotz(xc,yc,zc,xp,yp,zp,Rz)
119            xpg=g[0]
120            ypg=g[1]
121            zpg=g[2]
122            a=xpg-xc
123            b=ypg-yc
124            c=zpg-zc
125            qp=sqrt(a*a+b*b+c*c)
126            nx=a/qp
127            ny=b/qp
128            nz=c/qp
129            ndotl=nx*lx+ny*ly+nz*lz
130            textbfI=IA+(IB-IA)*((1+ndotl)/2)**n
131            if alpha == alpha1:
132                xpglast=xpg
133                ypglast=ypg
134            if nz < 0:
135                plt.plot([xpglast,xpg],[ypglast,ypg],linewidth=4,color=((1-I),.8*(1-I),.45*(1-I)))
136                xpglast=xpg
137                ypglast=ypg
138
139 plt.show()
Listing 7-2Program SHADESPHERE

7.3 总结

虽然添加背景色可以极大地增强对象的视觉外观,但底纹也非常有效。在本章中,您学习了为对象添加阴影的技术。阴影意味着照明光源的存在。在模型中,您使用了来自光源的光线方向,但没有指定光源的位置。在清单 7-1 中,你探索了如图 7-10 所示的阴影函数的概念,以及它如何决定*面上阴影的强度。这取决于*面相对于入射光线方向的方向,这是通过取垂直于表面的单位矢量$$ \widehat{\mathbf{n}}, $$与指向光线方向的单位矢量$$ \widehat{\mathbf{l}} $$的点积来确定的。在清单 7-2 中,你在一个球体上执行了相同的着色操作。但是,您改进了着色功能。在清单 7-1 中,您使用了阴影强度和点积$$ \widehat{\mathbf{n}}\cdot \widehat{\mathbf{l}}, $$之间的简单线性关系,而在清单 7-2 中,您使用了非线性关系,如图 7-14 所示。这极大地改善了着色的外观。

八、2D 数据绘图

在这一章中,你将看到绘制二维数据的样式和技术。您将从一些简单的图开始,然后发展到在同一个图上包含多组数据的图。虽然 Python 包含专门的内置函数,在这方面非常有效,通常只需要几行代码,但您会发现,您可以通过采取更实用的方法来美化您的图形,并通过用简单的 Python 命令补充专门的 Python 函数来发挥创造力。例如,输入设置和数据后,图 8-1 中的图只需要三行专用代码。另一方面,图 8-5 仅仅使用专门的 Python 命令来创建可能是一个挑战。使用简单的命令,再加上一点创造性,可以使工作变得容易得多。在简单的数据绘图之后,您将继续进行线性回归,将直线拟合到数据集。然后,您将看到如何将非线性数学函数拟合到数据中。你用样条来结束。样条是通过每个数据点的*滑曲线。

图 8-1 显示了一个数学函数的曲线图。这个情节是通过列举 8-1 创造出来的。其中,第 13 行设置了 x 轴的数值范围,在本例中是从 0 到 150,步长为 1。这意味着函数将在该范围内绘制。第 8 行中的轴定义具有相同的限制,但是它们可能不同。例如,如果第 8 行是 plt.axis([0,200,0,100]),则绘图区域的宽度将为 200,但仍将从 0 到 150 绘制函数。这种组合会将函数图定位在绘图区域的左侧。

第 14 行定义了正在绘制的函数。这是 y1 与 x 的简单指数函数。第 17 行用蓝色绘制,并附上标签' y1 ',这将由第 20 行的 legend()函数使用。在第 20 行,loc 等于图例的位置,可以是上、中、下与左、中、右的任意组合。这里你用的是'左上角的'。如果你指定' best ',Python 会为它确定最佳位置。如您所见,第 13、14 和 17 行基本上包含了整个绘图操作。

A456962_1_En_8_Fig1_HTML.jpg

图 8-1

Data plot produced by Listing 8-1

1  """
2  DATAPLOT1
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7
8  plt.axis([0,150,0,100])
9  plt.axis('on')
10 plt.grid(True)
11
12 #——————————define function y1 vs x
13 x=np.arange(0,150,1)
14 y1=10+np.exp(.035*x)
15
16 #——————————plot y1 vs x
17 plt.plot(x, y1,'b',label='y1')
18
19 #——————————–plot the legend
20 plt.legend(loc='upper left')

21
22 plt.show()
Listing 8-1
Program DATAPLOT1

在清单 8-2 中,您在同一个图上绘制了两个函数 y1 和 y2。第 18 行和第 19 行进行绘制。添加标签“温度”和“压力”,这将由 legend()函数使用。在第 25 行,您添加了 marker= ' s ',它在温度曲线的每个数据点绘制了一个正方形;第 26 行中的 marker= ' * '在压力曲线的每个点绘制了一个星形。 https://matplotlib.org/api/markers_api.html 还有其他标记样式。

在图 8-2 中,注意数据图的水*范围(20-140)小于绘图宽度(0-150)。让数据不撞上图的边缘有时可以使它更具可读性。要使数据图跨越图的整个宽度,只需将第 8 行改为 plt.axis([20,140,0,100])。同样,y 值的范围也可以改变。

A456962_1_En_8_Fig2_HTML.jpg

图 8-2

Data plot produced by Listing 8-2

1  """
2  DATAPLOT2
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7
8  plt.axis([0,150,0,100])
9  plt.axis('on')
10 plt.grid(True)
11
12 #—————————————define data points
13 x=[20,40,60,80,100,120,140]
14 y1=[30,50,30,45,70,43,80]
15 y2=[45,35,40,60,60,55,70]
16
17 #—————————————plot lines with labels
18 plt.plot(x,y1,'b',label='Temperature')
19 plt.plot(x,y2,'r',label='Pressure')
20
21 #—————————————legend
22 plt.legend(loc='upper left')
23
24 #—————————————add markers
25 plt.scatter(x,y1,color='b',marker='s')
26 plt.scatter(x,y2,color='r',marker='*',s=50)
27
28 plt.show()

Listing 8-2
Program DATAPLOT2

在清单 8-2 中,针对一个 y 轴显示了两个函数,温度和压力。当然,这是假设 y 轴上的值对 T 和 p 都合适,但是如果压力值比温度值大得多或小得多呢?压力图可能会脱离图表或太小而不明显。你需要两个垂直标尺,一个测量温度,另一个测量压力。

在清单 8-3 中,您降低了压力值,如第 14 行所示。如果用与温度相同的垂直刻度来绘制,它们在图上会显得太低。您可以通过引入第二个垂直轴来解决这个问题。第 17-20 行绘制了温度数据。第 20 行允许您将垂直刻度线的颜色更改为任何颜色,在本例中为红色。第 21 行在左上角绘制了一个图例。第 24-27 行在图的右侧绘制了第二个刻度。第 24 行建立了一个“孪生”绘图轴。这个孪生包括已经建立的水* x 轴加上右侧新的垂直 y 轴。该组中的其余命令指的是第二个 y 轴。第 26 行将该轴标为压力。第 27 行将刻度线和数字改为蓝色。第 28 行在右上角绘制了第二个图例。如果您尝试为温度和压力绘制一个图例,您会发现结果取决于您在代码中放置 legend()函数的位置。如果您使用两个独立的图例(),就像您在这里所做的一样,并将它们放在相同的位置,比如左上角,一个会覆盖另一个。如果您尝试在代码末尾仅使用一个图例(),它将显示一个图例,其中仅显示压力。见图 8-3 。在下一期节目中,你会看到解决这个问题的方法。

A456962_1_En_8_Fig3_HTML.jpg

图 8-3

Data plot produced by Listing 8-3

1  """
2  DATAPLOT3
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7
8  plt.axis([0,140,0,100])
9  plt.axis('on')
10 plt.grid(True)
11
12 t=[20,40,60,80,100,120,140] #———Time
13 T=[30,33,37.5,44,55,70,86] #———Temperature
14 p=[1.8,2.3,3,4,5.4,7.3,9.6] #———Pressure
15
16 #———————Plot T vs t in red on the left vertical axis.
17 plt.plot(t,T,color='r',label='Temperature')
18 plt.xlabel('Time')
19 plt.ylabel('Temperature',color='r')
20 plt.tick}_params(axis='y',labelcolor='r')
21 plt.legend(loc='upper left')
22
23 #———————Plot P vs t in blue on the right vertical axis.
24 plt.twinx()
25 plt.plot(t,p,color='b',label='Pressure')
26 plt.ylabel('Pressure', color="b")
27 plt.tick_params(axis='y', labelcolor="b")
28 plt.legend(loc='upper right')
29
30 #———————title the plot
31 plt.title('Test Results')
32
33 plt.show()

Listing 8-3
Program DATAPLOT3

在清单 8-4 中,您尝试解决在之前的程序中遇到的 legend()问题。第 12 行设置了一个名为 ax1 的情节,其中包括一个子情节。第 14 行绘制了一个网格。第 8-10 行建立了数据列表。第 16 行标记了 x 轴。第 18 行用红色绘制了温度曲线,并将其命名为 l1。第 20 行设置左侧垂直轴上的比例限制,范围从 0 到 100。第 21 行用红色标记它。第 23 行将 twin()的第二个垂直轴(包括 x 轴)设置为 ax2。第 25 行用蓝色绘制了曲线 l2。第 27 行将比例限制设置为 0-10。第 28 行标记了它。第 30 行和第 31 行指定了将出现在图例中的曲线。第 32 行绘制了图例。语法看起来有点神秘,但确实有效,如图 8-4 所示。

A456962_1_En_8_Fig4_HTML.jpg

图 8-4

Data plot produced by Listing 8-4

1  """
2  DATAPLOT4
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7
8  t=[0,20,40,60,80,100,120]
9  T=[28,30,35,43,55,70,85]
10 p=[1.8,2.3,3,4,5.4,7.3,9.6]
11
12 fig, ax1 = plt.subplots() #———set up a plot ax1 with subplots
13
14 plt.grid(True) #———draw grid
15
16 ax1.set_xlabel('Time (hrs)') #———label X axis of ax1
17
18 l1=plt.plot(t,T,'r',label='Temperature') #———plot temperature in red as curve l1
19
20 ax1.set_ylim([0,100]) #———set Y axis limits of ax1
21 ax1.set_ylabel(r'Temperature (° K)', color="r") #———label Y axis of ax1
22
23 ax2 = ax1.twinx() #———set up ax2 as twin of ax1
24
25 l2=plt.plot(t, p, 'b',label='Pressure') #———plot pressure in blue as curve l2
26
27 ax2.set_ylim([0,10]) #———set Y axis limits of ax2
28 ax2.set_ylabel('Pressure (psi)', color="b") #———label Y axis of ax2
29
30 line1,=plt.plot([1],label='Temperature',color='r') #———line 1 of legend
31 line2,=plt.plot([2],label='Pressure',color='b') #———line 2 of legend
32 plt.legend(handles=[line1,line2],loc='upper left') #———plot legend
33
34 plt.title('Test Data')

Listing 8-4
Program DATAPLOT4

在清单 8-5 中,您绘制了多条曲线,并给出了每条曲线自己的垂直刻度。第 12-14 行定义了时间、温度和压力数据的列表。在第 15 行中,您引入了第三个因变量 volume v。第 17 行打开了一个名为 pp=[ ]的新列表,它将用于垂直缩放压力数据。您可以简单地缩放和替换 p=[ ]中的项目,但这样会破坏原始值。这在这个程序中不是问题,但是让它们保持不变是个好习惯。第 18-19 行将包含在 p 中的原始压力值缩放 10 倍,并将它们附加到 pp 中。对第 21-23 行中的 v 执行相同的操作,其中体积数据被缩放 100 倍。第 25-28 行绘制了曲线和图例。第 30-33 行用蓝色绘制了右侧 y 轴上的压力刻度。第 35-37 行标记了三个轴。第 39-43 行用绿色标出了音量刻度值。第 45-46 行绘制了垂直的绿色轴。这是通过将字符“|”绘制为右侧的文本来实现的。通常,您会希望从垂直体积轴从上到下绘制一条直线,但是 Python 不允许在主绘制区域之外绘制直线或散点。但是,它允许文本。所以你用一系列的“|”符号构造了一条垂直线。如果您愿意,可以用这种方式添加更多的垂直轴。见图 8-5 。

本课程中使用的方法比以前更注重实践。以前的程序主要依赖于专门的 Python 语法。这种方法的优点是它有效,非常灵活,并且不需要太多代码。这种 Python 语法与实践技巧的创造性结合实际上非常强大。有时候跳出框框思考是值得的。

A456962_1_En_8_Fig5_HTML.jpg

图 8-5

Data plot produced by Listing 8-5

1  """
2  DATAPLOT5
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7
8  plt.axis([0,140,0,100])
9  plt.axis('on')
10 plt.grid(True)
11
12 t=[20,40,60,80,100,120] #———time
13 T=[30,35,43,55,70,85] #———temperature
14 p=[2,3,4,5.3,7.3,9.6] #———pressure
15 v=[.6,.58,.54,.46,.35,.2] #———volume
16
17 pp=[ ] #———list for scaled pressure for plotting
18 for i in np.arange(0,len(p),1):
19       pp.append(p[i]*10) #———scale p by 10
20
21 vv=[ ] #———list for scaled volume for plotting
22 for i in np.arange(0,len(v),1):
23       vv.append(v[i]*100) #———scale volume by 100
24
25 plt.plot(t,T,color='r',label='Temperature',marker='o') #———plot temperature
26 plt.plot(t,pp,color='b',label='Pressure',marker='s') #———plot scaled pressure
27 plt.plot(t,vv,color='g',label='Volume',marker='d') #———plot scaled volume
28 plt.legend(loc='upper left')
29
30 for y in np.arange(0,100+1,20): #———plot pressure scale values
31      a=y/10
32      a=str(a) #———convert to string for plotting as text
33      plt.text(142,y,a,color='b')
34
35 plt.xlabel('Time (hrs)') #———label axes
36 plt.ylabel('Temperature °K',color='r')
37 plt.text(151,65,'Pressure (psi)',rotation=90,color='b')
38
39 for y in np.arange(100,-1,-20): #———plot volume scale values
40      a=y/100
41      a=str(a)
42      plt.text(162,y,a,color='g')
43      plt.text(159,y+2,'_',color='g')
44
45 for y in np.arange(1,99,3):
46      plt.text(157,y,'—',color='g')
47
48 plt.text(170,65,r'Volume (cm3)',rotation=90,color='g') #———label volume scale
49
50 plt.title('Compression Test Results') #—title
51
52 plt.show()
Listing 8-5
DATAPLOT5

8.1 线性回归

线性回归是将一条直线拟合到一组数据点的过程。参考图 8-6 和 8-7 ,目标是确定一条直线的参数 A 和 B,

$$ y= Ax+B $$

(8-1),这些参数产生数据点的最佳拟合。b 是直线的 y 轴截距,A 是直线的斜率。每个数据点 I 具有坐标 x i ,y i 。每个都有一个相对于直线的误差 e i 。直线与数据点的最佳拟合将是 A 和 B 产生

$$ \sum \limits_{i=1}^n{e}_i²= minimum $$

【8-2】的直线,其中 n 是数据点的数量。这相当于将均方根误差降至最低。e i 在等式 8-2 中被*方,以说明 e i 的负值。可以看出,当

$$ A=\frac{C_3-n{C}_1{C}_2}{C_4-n{C}_1{C}_1} $$

【8-3】

$$ B={C}_2-A{C}_1 $$

【8-4】

$$ {C}_1=\frac{1}{n}\sum \limits_{n=1}^n{t}_i $$

【8-5】

$$ {C}_2=\frac{1}{n}\sum \limits_{n=1}^n{v}_i $$

【8-6】

$$ {C}_3=\sum \limits_{n=1}^n{v}_i{t}_i $$

【8-7】

$$ {C}_4=\sum \limits_{n=1}^n{t}_i{t}_i $$

【8-8】时,满足方程 8-2

在清单 8-6 中,从第 52 行开始,回归例程被添加到清单 8-5 中。它用一条回归线来拟合绿色体积曲线。第 55-60 行计算上面定义的 C1-C4 系数。第 55 行的 np.sum()对列表 t 中的元素求和。第 57 行的 np.multiply()将列表 v 和 t 中的元素逐个相乘,产生列表 A。第 58 行然后将 A 中的元素相加。第 62 和 63 行根据等式 8-3 和 8-4 计算 A 和 B。第 65-68 行使用散点绘制回归线;第 66 行将 v 对 t 的值计算为 vp,v 的标绘值;第 67 行将 vp 缩放 100 用于绘图;第 68 行进行了绘图。

等式 8-2 表明,最小化σe(i)2,其中 e(I)是数据点 I 与回归线的偏差,相当于最小化均方根值。均方根值为

$$ RMS={\left[\frac{\sum \limits_{1=1}ne{(i)}²}{n}\right]}{\frac{1}{2}} $$

(8-9)

这是在第 71-76 行计算的。e(i)在第 73 行计算。它在第 74 行被*方为 ee,然后在第 75 行被求和为 sumee,产生等式 8-9 中的分子。根据等式 8-9 在第 76 行计算 RMS。很明显,最小化σe(I)2相当于最小化均方根值。

程序的其余部分将标签和值放置在绘图上。第 83 行减少了回归线的起始值 vp1 的位数;第 84 行绘制了它。第 86-88 行绘制了最终值。a 和 B (Ap 和 Bp)类似地绘制在线 90-96 中。

除了第 83 行中使用的语法之外,Python 中还有其他减少位数的方法。但是,如果被缩短的数字是负数,减号可能不会出现在输出中。这可能是 Python 某些版本的问题。

A456962_1_En_8_Fig7_HTML.jpg

图 8-7

Model used by Listing 8-6 showing data points 1,2,3,4…i with straight line fit. ei=error from straight line for data point i.

A456962_1_En_8_Fig6_HTML.jpg

图 8-6

Straight line fit to the volume curve produced by Listing 8-6

1  """
2  REGRESSION1
3  """
   .
   .
   #——————————same as DATAPLOT5——————————
   .
   .
52 #—————————————straight line fit to Volume v vs t
53 n=len(v)
54
55 c1=np.sum(t)/n #———sum values of list t and divide by n, =average of t
56 c2=np.sum(v)/n #———sum values of list v and divide by n, =average of v
57 a=np.multiply(v,t) #———multiply list v by t element by element = list a
58 c3=np.sum(a) #———sum elements of a
59 a=np.multiply(t,t) #———multiply list t by t element by element = list a
60 c4=np.sum(a) #———sum elements of a
61
62 A=(c3-n*c1*c2)/(c4-n*c1*c1) #———line parameters A and B
63 B=c2-A*c1
64
65 for tp in np.arange(t[0],t[5],2): #———plot line with scatter dots
66      vp=A*tp+B
67      vp=vp*100 #———scale vp for plotting
68      plt.scatter(tp,vp,color='g',s=1)
69
70 #—————————————————————calculate RMS error
71 sumee=0
72 for i in range(len(t)):
73      e=(v[i]-(A*t[i]+B))
74      ee=e*e
75      sumee=sumee+ee
76      rms=np.sqrt(sumee/n)
77     
78 #—————————————————labels
79 plt.text(60,28,'v=At+B',color='g')
80 plt.arrow(78,30,6,6,head_length=3,head_width=1.5,color='g',linewidth=.5)
81
82 vp1=A*t[0]+B #——————beginning v value of line
83 vp1='%7.4f'%(vp1) #——————reduce the number of decimal places
84 plt.text(2,64,vp1,color='g') #———plot
85
86 vp2=A*t[5]+B #——————end v value of line
87 vp2='%7.4f'%(vp2)
88 plt.text(122,25,vp2,color='g')
89
90 Ap='%7.5f'%(A)
91 plt.text(65,18,'A=',color='g')
92 plt.text(72,18,Ap,color='g') #———print value of A
93
94 Bp='%7.5f'%(B)
95 plt.text(65,12,'B=',color='g')
96 plt.text(73,12,Bp,color='g') #———print value of B
97
98 rms='%7.3f'%(rms)
99 plt.text(95,3,'RMS error=',color='g')
100 plt.text(123,3,rms,color='g') #———print RMS error
101
102 plt.show()
Listing 8-6
REGRESSION1

8.2 功能拟合

在清单 8-6 中,您绘制了一条直线来拟合代表体积与时间测量值的数据点。你很幸运,这个问题有一个解析解,由方程 8-2 、 8-3 和 8-5 表示。在本节中,您将为同一个数据集拟合一个任意函数。该函数是用户定义的;也就是说,你可以指定任何你想要的函数,任何你认为合适的函数。在清单 8-7 中,您将尝试关系

$$ v=A{x}²+B $$

(8-10)

如前一节所述,您的任务是找到 A 和 B 的值,使该函数与数据点最佳拟合。因为你希望能够使用任何任意函数,所以为你希望尝试的每个函数推导出问题的封闭形式的解决方案显然是不省时的。在这里,您将使用一种强力方法,该方法涉及计算等式 8-10 中参数 A 和 B 的值,在两者的预期范围内获得最小均方根误差。这是一种实践方法;需要对这个问题有所了解。例如,检查图 8-8 和方程 8-10 中的 v(t)曲线表明,方程 8-10 中的参数 B,即 t=t[0]时的 V 轴(绿色)截距,应该位于. 5 和. 7 之间。类似地,你可以假设 A 会很小,因为等式 8-10 涉及 t 的*方,其值大到 t[5]=120。你也可以通过检查看到,A 应该是负数。因此,您可以尝试从-.001 到 0 的范围。计算 B 值在. 5 和. 7 之间以及 A 值在-.001 和 0 之间的许多组合的误差。这将给出对应于这些范围之间几乎最低误差的 A 和 B。我说“几乎”最低误差是因为,当在 A 和 B 的预期范围之间循环时,你是以小步进行的。这些步骤越精细,你的最终解决方案就越精确。虽然您可以使用自动迭代技术,但是这里描述的过程更容易编码,但是涉及到用户迭代。它的工作方式如下:猜测 A 和 B 的初始范围后,当您得到结果时,您可以通过关闭、打开或移动范围来使用精确的值进行另一次运行。您还可以更改搜索增量 dB(第 61 行)和 dA(第 64 行)。只要进行几次这样的手工迭代,您就应该能够得到您所需要的任何精度的解决方案。

参照清单 8-7 ,大部分与清单 8-6 相同。第 59-64 行定义了搜索例程 B1 和 B2 的界限,它们是 B 范围的开始和结束;A 范围的 A1 和 A2。dB 和 dA 是增量。较小的增量会产生更准确的结果,但需要更多的处理时间。从第 70 和 71 行开始的两个嵌套循环首先搜索 B 范围,然后对于 B 的每个值,搜索 A 范围。在 A 和 B 的每个组合处,从第 73 行开始的循环循环通过所有的数据点,len(t) (=len(v))。第 74 行计算每个数据点和假设函数方程 8-10 之间的误差;第 75 行对其求*方,第 76 行根据等式 8-2 对误差的*方求和。在第 72 行,总和最初被设置为零。第 77 行说,如果 A 和 B 的当前组合产生的*方和小于先前计算的总和,那么用当前值替换该值,并将 A 和 B 的当前值设置为 Amin 和 Bmin,这些值对应于当前最低误差。当 A 和 B 环路第一次循环时,第 76 行中的 eemin 是未知的。它在第 56 行被设置为非常高的值。这确保了第一个 eemin 会更少。在第一个周期之后,它将采用对应于产生最低 sumee 值的 A 和 B 的最新组合的值。所有这些的最终结果是在数据点和假设函数之间产生最小误差的 A 和 B 的值。他们是 Amin 和 Bmin。第 86-89 行使用第 87 行中的 Amin 和 Bmin 绘制了函数。第 92-97 行计算相应的均方根误差。

图 8-6 显示了 v(t)的直线*似值和 0.042 的均方根误差,如该图所示。使用非线性函数 Ax 2 +B 时,均方根误差为 0.0132,相当低。

程序的其余部分将标签放置在地块上。从图 8-8 中可以看出,第 59-64 行中设置的 A 和 B 的限值在图上以黑色打印为 A1、A2、B1 和 B2。程序找到的导致最小误差的值以绿色打印为 Amin 和 Bmin。在本例中,假设值为 A1、A2、B1 和 B2,Amin 和 Bmin 在假设范围内,因此您可以确信已经找到了接*最佳的值。但是让我们假设其中一个参数,比如 B1,选择错误。也就是说,假设你选择了 B1=.65,B2=.7。由程序计算的 Bmin 的结果是 B1 = .65 也就是说,它会撞上 B 下限。这将告诉你 B1 太高,你应该在下次运行时降低它。类似地,如果你选择了 B1=.5,B2=.6,那么 Bmin 将达到 B 的上限,这表明你应该提高 B2。

还有其他可用的曲线拟合函数,类似于您在这里开发的函数;去 https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html .其他的可以用互联网搜索找到。您在这里开发的这个具有开放、简单、易用的优势,而且您可以控制它。

A456962_1_En_8_Fig8_HTML.jpg

图 8-8

Function fit to volume curve produced by Listing 8-7

   """
   REGRESSION2
   """
52 #——————————same as REGRESSION1————————————
53
54 #———————————————————parabolic fit to v vs t
55 n=len(v)
56 eemin=10**10 #———starting value of eemin, deliberately set very large
57
58 #—————————————loop parameters
59 B1=.5
60 B2=.7
61 dB=.001
62 A1=-.001
63 A2=0.
64 dA=.0000001
65
66 #—————————————loop through all combinations of A and B
67 #—————————————within ranges defined by loop parameters
68 #—————————————searching for Amin, Bmin that produce
69 #—————————————best fit of function to data points
70 for B in np.arange(B1,B2,dB):
71     for A in np.arange(A1,A2,dA):
72         sumee=0
73         for i in range(len(t)):
74              e=(v[i]-(A*t[i]*t[i]+B)) #———error of data point i at A, B
75              ee=e*e #———error squared
76              sumee=sumee+ee #———sum of error squared
77              if sumee < eemin: #———if sum < present minimum eemin then
78                   eemin=sumee #———set new minimum = sumee
79                   Amin=A #———set new Amin = A
80                   Bmin=B #———set new Bmin = B
81
82 #—————————————Amin, Bmin above will produce best fit
83
84 #————————————-plot best fit function with scatter dots
85 #————————————-from t[0] to t[5] in steps=2
86 for tp in np.arange(t[0],t[5],2):
87      vp=Amin*tp*tp+Bmin
88      vp=vp*100 #—scale to plot
89      plt.scatter(tp,vp,color='g',s=1)
90
91 #—————————————————calculate RMS Error
92 sumee=0
93 for i in range(len(v)):
94      e=(v[i]-(Amin*t[i]*t[i]+Bmin)) #———error at each data point
95      ee=e*e #———error squared
96      sumee=sumee+ee #———sum of squared errors
97      rms=np.sqrt(sumee/n) #———RMS error
98
99 #————————————————————————labels
100 plt.text(100,50,'v=At+B',color='g')
101 plt.arrow(99,50,-6.5,-6.5,head_length=3,head_width=1.5,color='g',linewidth=.5)
102
103 A=Amin
104 B=Bmin
105
106 vp1=A*t[0]*t[0]+B
107 vp1='%7.3f'%(vp1)
108 plt.text(2,63,vp1,color='g')
109
110 vp2=A*t[5]*t[5]+B
111 vp2='%7.3f'%(vp2)
112 plt.text(119,22,vp2,color='g')
113
114 Ap='%8.6f'%(A)
115 plt.text(59,18,'Amin=',color='g')
116 plt.text(74,18,Ap,color='g')
117
118 Bp='%8.6f'%(B)
119 plt.text(59,12,'Bmin=',color='g')
120 plt.text(75.2,12,Bp,color='g')
121
122 rms='%7.4f'%(rms)
123 plt.text(95,3,'RMS error=',color='g')
124 plt.text(120,3,rms,color='g')
125
126 A1='%8.6f'%(A1)
127 plt.text(60,90,'A1=')
128 plt.text(69,90,A1)
129
130 A2='%8.6f'%(A2)
131 plt.text(60,85,'A2=')
132 plt.text(70.2,85,A2)
133
134 B1='%8.6f'%(B1)
135 plt.text(60,75,'B1=')
136 plt.text(70.2,75,B1)
137
138 B2='%8.6f'%(B2)
139 plt.text(60,70,'B2=')
140 plt.text(70.2,70,B2)
141
142 plt.show()

Listing 8-7Program REGRESSION2

8.3 花键

图 8-9 所示的曲线称为样条曲线。它们的特征在于它们通过它们各自的数据点,这些数据点显示为点。每一条都是“自然”的样条,因为末端没有扭曲。用微积分的说法,二阶导数在端点为零。

A456962_1_En_8_Fig9_HTML.jpg

图 8-9

Spline curves produced by Listing 8-8

由薄木板条构成的花键曾一度广泛用于造船,在造船中需要制造光滑的船体形状。在放样室里,工人们将钉子钉进地板,然后在钉子周围弯曲薄木条。然后,弯曲的长条形状被描摹到下面的纸张或胶合板上。这个形状被用来切割建造过程中使用的全尺寸模具。“spline”一词被认为源自丹麦的 splind 或北弗里斯兰的 splinj,这两个地方都是古代造船地区。第二次世界大战后,在造船和飞机设计和建造中,机械样条的使用被数学推导的曲线所取代。

您将在此使用的样条的数学关系称为三次样条。它的形式有

$$ x= Ax{q}³+ Bx{q}²+ Cxq+ Dx $$

(8-11)

由于样条曲线上的每个点都由两个坐标 x 和 y 定义,所以你需要两个版本的方程 8-11:

$$ x= Ax{q}³+ Bx{q}²+ Cxq+ Dx $$

(8-12)

$$ y= Ay{q}³+ By{q}²+ Cyq+ Dy $$

(8-13)

你的任务是确定系数 Ax → Cx 和 Ay → Cy。一旦你有了它们,你将能够绘制样条曲线。为此,在相邻数据点之间的线段之间拟合一个单独的 x 和 y 方程。例如,点 2 和点 3 之间的区域是一个线段;3 和 4 之间是另一段。您还可以使用每个数据段右侧和左侧的数据点信息。

图 8-10 显示了一组数据点和编号方案。nop=是数据点的数量。有六个数据点,所以 nop=6。有五个点间线段。您将使用列表来跟踪所有事情。记住,Python 希望列表以第[o] 元素开始。在点[3],即第四个数据点,i=3。你看到长度 q[2]在左边,q[3]在右边。每一个都是弦长,从一点到下一点的直线距离。

A456962_1_En_8_Fig10_HTML.jpg

图 8-10

Model used by Listing 8-8

现在只参考方程 8-12 中的 x 方程,你可以将点【3】,MX【3】处的“斜率”定义为

$$ mx\left[3\right]=\left(\frac{x\left[3\right]-x\left[2\right]}{q\left[2\right]}+\frac{x\left[4\right]-x\left[3\right]}{q\left[3\right]}\right)\ast .5 $$

(8-14)

这是左侧“斜率”和右侧点[3]的*均值。我用引号将“斜率”括起来,以强调它不是传统意义上的斜率,例如 y/x,而是每个 x 除以弦长 q[ ]。对于任意一点【我】

$$ mx\left[i\right]=\left(\frac{x\left[i\right]-x\left[i-1\right]}{q\left[i-1\right]}+\frac{x\left[i+1\right]-x\left[i\right]}{q\left[i\right]}\right)\ast .5 $$

(8-15)

我的[i]的等式是类似的。因为 mx[i]和 my[i]依赖于 I 之前和之后的坐标值,所以第一个点和最后一个点需要单独的方程,mx[0]和 MX[nop-1]:

$$ mx\left[0\right]=\left(x\left[1\right]-x\left[0\right]\right)/q\left[0\right] $$

(8-16)

$$ my\left[0\right]=\left(y\left[1\right]-y\left[0\right]\right)/q\left[0\right] $$

(8-17)

$$ mx\left[ nop-1\right]=\left(x\left[ nop-1\right]-x\left[ nop-2\right]\right)/q\left[ nop-2\right] $$

(8-18)

$$ my\left[ nop-1\right]=\left(y\left[ nop-1\right]-y\left[ nop-2\right]\right)/q\left[ nop-2\right] $$

(8-19)

有了这些定义,就可以看出

$$ dx\left[i\right]=x\left[i\right] $$

(8-20)

$$ dy\left[i\right]=y\left[i\right] $$

(8-21)

$$ cx\left[i\right]= mx\left[i\right] $$

(8-22)

$$ cy\left[i\right]= my\left[i\right] $$

(8-23)

$$ bx\left[i\right]=\left(3x\left[i+1\right]-2 cx\left[i\right]q\left[i\right]-3 dx\left[i\right]- mx\left[i+1\right]q\left[i\right]\right)/q\left[i\right]q\left[i\right] $$

(8-24)

$$ by\left[i\right]=\left(3y\left[i+1\right]-2 cy\left[i\right]q\left[i\right]-3 dy\left[i\right]- my\left[i+1\right]q\left[i\right]\right)/q\left[i\right]q\left[i\right] $$

(8-25)

$$ ax\left[i\right]=\Big( mx\left[i+1\right]-2 bx\left[i\right]q\left[i\right]- cx\left[i\right]/3q\left[i\right]q\left[i\right] $$

(8-26)

$$ ay\left[i\right]=\Big( my\left[i+1\right]-2 by\left[i\right]q\left[i\right]- cy\left[i\right]/3q\left[i\right]q\left[i\right] $$

(8-24)

这些系数基于这样的要求,即在数据点处样条线段的相交处,样条的位置及其斜率必须从一个截面到下一个截面匹配。此外,斜率(二阶导数)的变化率必须匹配;否则,样条的形状会出现角度不连续。在 i=0 的样条曲线的起点,没有相邻的线段,因此要求该点的斜率变化率(挠度的二阶导数)为零。这意味着,如果样条继续延伸到第一个点的左侧,它将是一条直线,与该点的样条线段具有相同的斜率。在梁的力学中,称为 M 的弯矩产生斜率 y 的变化率;也就是 d 2 y/dx 2 ≈ M .由于花键两端没有任何东西产生弯矩,所以 d 2 y/dx 2 = 0,斜率不会改变。这是直观的;如果造船工人正在将一个木花键安装到一组钉在地板上的钉子上,并且他使用了一条太长的木花键,那么多余的长度将以与最后一个钉子处的花键末端相同的角度笔直地延伸。同样的论点适用于 i=nop-1 处的样条末端;斜率没有限制,所以二阶导数为 0。这提供了一个“自然的”样条。您可以指定其他的端点条件,例如夹紧或扭转,但是上面的系数会有所不同。

以下等式沿着点[i ]和[i+1]之间的样条线定位点 xp,yp:

$$ xp= ax\left[i\right]q{q}³+ bx\left[i\right]q{q}²+ cx\left[i\right] qq+ dx\left[i\right] $$

(8-28)

$$ yp= ay\left[i\right]q{q}³+ by\left[i\right]q{q}²+ cy\left[i\right] qq+ dy\left[i\right] $$

(8-29)其中 qq 是弦 I 的长度

当列表 8-8 绘制样条曲线时,它从点[0]开始一段一段地绘制,一直到点[nop-1]。再次参考图 8-10 ,如果 i=3,上述方程将绘制从点【3】到点【4】的样条线段。为了绘制从点[0]到[5]的整个样条曲线,程序绘制从[0]开始到[nop-1]的线段。即程序自动绘制从[0] → [1],[1] → [2]的线段,.....[5] → [6].

参考清单 8-8 ,从第 17 行开始通过函数样条进行计算和绘图。在函数的参数中,x 和 y 位于第 73 和 74 行定义的列表中。每个 x,y 对是一个数据点的坐标。clr 是样条的颜色,ls 是线条样式。数据点绘制在第 19 行。第 21 行的 nop 是数据点的数量。第 23-33 行是长度为 nop 的零列表。通过逐项计算值来填充这些列表。您可以先定义空列表,然后再追加元素。通过现在定义列表长度,可以避免追加。两种方式都可以;只是喜好问题。

第 35-38 行计算弦长 q[i]。第 40 行和第 41 行计算样条起点的斜率。第 43-45 行计算在 0 8-28 的*均斜率。第 62-70 行将样条绘制成线段。

对程序的控制发生在第 73-83 行。在这里,您正在绘制两条样条曲线。第一条样条曲线的数据点集包含在行 73 和 74 的列表中。第 75 行和第 76 行设置了所需的颜色和线条样式。第 77 行调用函数 spline。第二条样条线在第 79-83 行以类似的方式创建。通过添加更多的这些例程,可以添加更多的样条线。

打印出样条线段范围内的 x,y 值很容易。例如,假设您想要点[2]和[3]之间的线段内的点的坐标。在第 71 行插入以下内容:

if i==2:
     print(xp,yp)

这将打印到点[3]的坐标值,在这里我将等于 3。

1  """
2  SPLINE2D
3  """
4
5  import matplotlib.pyplot as plt
6  import numpy as np
7  from math import sqrt
8
9  plt.axis([0,140,0,100])
10 plt.axis('on')
11 plt.grid(True)
12
13 plt.xlabel('x')
14 plt.ylabel('y')
15 plt.title('2D Splines')
16
17 def spline(x,y,clr,ls):
18
19      plt.scatter(x,y,s=30,color=clr)
20
21      nop=len(x)
22
23      q=[0]*nop
24      mx=[0]*
nop

25      my=[0]*nop
26      cx=[0]*nop
27      cy=[0]*nop
28      dx=[0]*nop
29      dy=[0]*nop
30      bx=[0]*nop
31      by=[0]*nop
32      ax=[0]*nop
33      ay=[0]*nop
34
35      for i in range(1,nop): #———chords q(i)
36           a=x[i]-x[i-1]
37           b=y[i]-y[i-1]
38           q[i-1]=sqrt(a*a+b*b)
39
40      mx[0]=(x[1]-x[0])/q[0]
41      my[0]=(y[1]-y[0])/q[0]
42
43      for i in range(1,nop-1): #———average m[i]
44           mx[i]=((x[i]-x[i-1])/q[i-1]+(x[i+1]-x[i])/q[i])*.5
45           my[i]=((y[i]-y[i-1])/q[i-1]+(y[i+1]-y[i])/q[i])*.5
46
47      mx[nop-1]=(x[nop-1]-x[nop-2])/q[nop-2]
48      my[nop-1]=(y[nop-1]-y[nop-2])/q[nop-2]
49
50 #———————————-calculate coefficients
51      for i in range(0,nop-1):
52           dx[i]=x[i]
53           dy[i]=y[i]
54           cx[i]=mx[i]
55           cy[i]=my[i]
56           bx[i]=(3*x[i+1]-2*cx[i]*q[i]-3*dx[i]-mx[i+1]*q[i])/(q[i]*q[i])
57           by[i]=(3*y[i+1]-2*cy[i]*q[i]-3*dy[i]-my[i+1]*q[i])/(q[i]*q[i])
58           ax[i]=(mx[i+1]-2*bx[i]*q[i]-cx[i])/(3*q[i]*q[i])
59           ay[i]=(my[i+1]-2*by[i]*q[i]-cy[i])/(3*q[i]*q[i])
60
61 #————————————plot the spline
62      xplast=x[0]
63      yplast=y[0]
64      for i in range(0,nop-1):
65           for qq in np.arange(0,q[i],4):
66                xp=ax[i]*qq*qq*qq+bx[i]*qq*qq+cx[i]*qq+dx[i]
67                yp=ay[i]*qq*qq*qq+by[i]*qq*qq+cy[i]*qq+dy[i]
68                plt.plot([xplast,xp],[yplast,yp],linewidth=1,color=clr,linestyle=ls)
69                xplast=xp
70                yplast=yp
71
72 #—————————————————control
73 x=[20,40,60,80,100,120]
74 y=[80,35,70,30,60,40]
75 clr="b"
76 ls='–'
77 spline(x,y,clr,ls)
78
79 x=[20,40,60,80,100,120]
80 y=[30,45,18,65,50,80]
81 clr="g"
82 ls='-'
83 spline(x,y,clr,ls)
84
85 plt.show()
Listing 8-8Program SPLINE2D

8.4 总结

本章涵盖了一系列数据绘制技术:绘制简单的点和函数、在同一图上绘制多个函数、用多个函数标记轴、将直线拟合到数据集的线性回归、将用户定义的函数拟合到数据集的函数拟合,以及通过每个数据点拟合*滑曲线的样条。虽然在 Python 社区中有许多数据绘制例程,您可以通过互联网搜索找到它们,但是这里的方法更加实用。通过了解如何自己做,用一点创造力,你可以制作出符合你自己需要的情节。在第九章,你将把你在这里所做的扩展到三维空间。

九、3D 数据绘图

将第八章中开发的用于生成二维样条的技术外推到三维样条是很容易的:你需要做的只是在程序中添加几行代码。这些行是清单 9-1 中粗体突出显示的行,特别是函数 plotspline()中从第 89 行到第 161 行的那些行。它们引入 z 坐标的语法与 x 和 y 坐标使用的语法基本相同。

列表 9-1 的控制从第 175 行开始。第一组数据点由第 175-177 行中的列表 x、y 和 z 定义。这些已经用#符号取消了,但是如果您想使用它们,它们会保留在原来的位置。他们制作图 9-1 。第 179-181 行中的活动列表产生数字 9-2 到 9-4 。第 183 行的 nop 是数据点的数量。这等于 len(x ),当然,也等于 len(y)和 len(z)。第 85 行的列表 g 保存由 rotx()、roty()和 rotz()旋转函数返回的值。旋转中心的坐标 xc、yc 和 zc 在第 187-189 行中定义。

第 191-193 行中的旋转角度 Rxd、Ryd 和 Rzd 可以使用一些解释。参考图 9-5 ,右边的坐标系定义了旋转(Rxd,Ryd,Rzd)和*移(xc,yc,zc)方向的数据点和样条。左侧的系统显示了全局坐标系,这是在指定旋转时应该使用的坐标系。x 和 y 方向由第 9 行的 plt.axis()函数定义。由于这是一个右手坐标系,所以+z 方向指向屏幕之外。例如,围绕 z 方向的正旋转 Rzd 将使右侧的图形逆时针旋转。

在图上显示网格线主要是为了帮助确定 xc、yc、zc 的位置。当轴(例如图 9-4 中的 x 轴和 z 轴)位于绘图*面内时,它们可用作数据点和样条坐标值的测量。然而,当旋转图时,如图 9-3 所示,它们并不给出真实的测量值,但可以在定位中心 xc、yc、zc 时作为辅助。

第 200-210 行通过调用第 33-43 行的函数 plotaxis()绘制了定义数据点和样条的轴。每个长度为 30 个单位。第 43 行的列表 g 保存每个轴的端点坐标。线 202 绘出了 x 轴;y 轴和 z 轴也是如此。

没有旋转(即 Rxd=Ryd=Rzd=0 ),轴将出现在图 9-5 的左侧。当绘制数据时,我们通常认为 z 是 x 和 y 的函数(即 z=z(x,y)),我们更喜欢 z 轴指向上。要做到这一点,我们必须旋转坐标系,使 z 向上。举个例子,在图 9-4 中,Rx=-90,Ry=0,Rz=0。这些值显示在图的右上角。这将获取+z 轴,该轴在未旋转的位置指向屏幕外,并围绕 x 轴逆时针旋转,使其指向上方。+y 现在指向屏幕。这是一个很好的开始方向。围绕该方向的后续旋转可以给出三维视图。然而,请记住,这个程序已经硬连线,以给出序列 Rx,Ry,Rz 的旋转。例如,在从第 46 行开始的函数 plotdata()中,第 51 行执行 Rx 旋转,接下来是第 55 行的 Ry,然后是第 59 行的 Rz。

数据点绘制在第 213 行,它调用函数 plotdata()。这个函数很简单。在第 51、55 和 59 行中,每个数据点旋转量 Rx,然后 Ry,接着 Rz。在线 66 中,每个点被绘制为绿色散射点。第 64 行用红色标出了第一个点。第 68-86 行绘制了从每个点到 x,y *面的灰线。每条线的顶部具有与数据点 g[0],g[1]相同的全局绘图坐标。绘图时不需要 z 坐标 g[3]。每条线底部的局部坐标与数据点具有相同的局部 x,y 坐标,但是现在局部 z 坐标为零,如第 72 行中所指定的。你需要这些局部坐标来旋转每条线的底部点。第 73、77 和 81 行执行旋转。第 83 行用红色标出了第一个点;线 86 用黑色绘制其余的点,用灰色绘制线。

接下来,在第 217 行绘制样条曲线,这将调用函数 plotspline()。颜色设置在第 216 行。该功能与前一章中使用的样条绘图算法相同,只是在程序列表中增加了以粗体显示的 z 轴线。

接下来,通过调用第 221 行中的函数 plotbottomspline(),用样条线连接垂直线的底部。颜色设置在第 220 行。plotbottomspline()打开每个点的 x、y 和 z 坐标列表:xbottom[ ]、ybottom[ ]、zbottom[ ]。每个中的项目最初设置为零。它们等同于第 168-171 行中的 x 和 y 数据点坐标。因为 z 坐标位于 x,y *面,所以在第 171 行中它被设置为零。这些都是本地坐标。第 172 行调用函数 plotspline(),该函数用于绘制主样条,参数是底部点的本地坐标。如前所述,plotspline()将执行旋转并绘制样条曲线。程序的其余部分打印数据和标签。

A456962_1_En_9_Fig5_HTML.jpg

图 9-5

Rotation model used by Listing 9-1

A456962_1_En_9_Fig4_HTML.jpg

图 9-4

Spline produced by Listing 9-1

A456962_1_En_9_Fig3_HTML.jpg

图 9-3

Spline produced by Listing 9-1

A456962_1_En_9_Fig2_HTML.jpg

图 9-2

Spline produced by Listing 9-1

A456962_1_En_9_Fig1_HTML.jpg

图 9-1

Spline produced by Listing 9-1

1   """
2   SPLINE3D
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sqrt, radians, sin, cos
8
9   plt.axis([0,150,0,100])
10  plt.axis('on')
11  plt.grid(True)
12
13  #=================================================rotation transformations
14  def rotx(xp,yp,zp,Rx):
15      g[0]=xp+xc
16      g[1]=yp*cos(Rx)-zp*sin(Rx)+yc
17      g[2]=yp*sin(Rx)+zp*cos(Rx)+zc
18      return[g]
19
20  def roty(xp,yp,zp,Ry):
21      g[0]=xp*cos(Ry)+zp*sin(Ry)+xc
22      g[1]=yp+yc
23      g[2]=-xp*sin(Ry)+zp*cos(Ry)+zc
24      return[g]
25
26  def rotz(xp,yp,zp,Rz):
27      g[0]=xp*cos(Rz)-yp*sin(Rz)+xc
28      g[1]=xp*sin(Rz)+yp*cos(Rz)+yc
29      g[2]=zp+zc
30      return[g]
31
32  #==========================================================plot axis
33  def plotaxis(xp,yp,zp,Rx,Ry,Rz):
34      rotx(xp,yp,zp,Rx) #—Rx rotation
35      xp=g[0]-xc
36      yp=g[1]-yc
37      zp=g[2]-zc
38      roty(xp,yp,zp,Ry) #—Ry rotation
39      xp=g[0]-xc
40      yp=g[1]-yc
41      zp=g[2]-zc
42      rotz(xp,yp,zp,Rz) #—Rz rotation
43      return[g]
44
45  #===============================================plot data points
46  def plotdata(x,y,z,Rx,Ry,Rz):
47      for i in range(0,nop):

48          xp=x[i]
49          yp=y[i]
50          zp=z[i]
51          rotx(xp,yp,zp,Rx)
52          xp=g[0]-xc
53          yp=g[1]-yc
54          zp=g[2]-zc
55          roty(xp,yp,zp,Ry)
56          xp=g[0]-xc
57          yp=g[1]-yc
58          zp=g[2]-zc
59          rotz(xp,yp,zp,Rz)
60          xp=g[0]-xc
61          yp=g[1]-yc
62          zp=g[2]-zc
63          if i==0: #———plot first point red
64              plt.scatter(g[0],g[1],s=25,color='r')
65          else:
66              plt.scatter(g[0],g[1],s=25,color='g')
67          #———————plot vertical lines from data points to the x,y plane
68          xt=g[0] #-global line top coords=rotated data point coords
69          yt=g[1]
70          xp=x[i] #—coords of line bottom (zp=0) before rotation)
71          yp=y[i]
72          zp=0
73          rotx(xp,yp,zp,Rx) #———rotate bottom coords
74          xp=g[0]-xc
75          yp=g[1]-yc
76          zp=g[2]-zc
77          roty(xp,yp,zp,Ry)
78          xp=g[0]-xc
79          yp=g[1]-yc
80          zp=g[2]-zc
81          rotz(xp,yp,zp,Rz)
82          if i==0: #———————plot first bottom point red
83              plt.scatter(g[0],g[1],s=25,color='r')
84          else:
85              plt.scatter(g[0],g[1],s=25,color='k')
86          plt.plot([xt,g[0]],[yt,g[1]],color='grey') #———plot line
87
88  #====================================================plot spline
89  def plotspline(x,y,z,Rx,Ry,Rz,clr):
90      q=[0]*nop
91      mx=[0]*nop
92      my=[0]*nop
93      mz=[0]*nop
94      cx=[0]*nop
95      cy=[0]*nop
96      cz=[0]*nop
97      dx=[0]*nop
98      dy=[0]*nop
99      dz=[0]*nop
100     bx=[0]*nop
101     by=[0]*nop
102     bz=[0]*nop
103     ax=[0]*nop
104     ay=[0]*nop
105     az=[0]*nop
106
107     for i in range(1,nop): #———chords q(i)
108         a=x[i]-x[i-1]
109         b=y[i]-y[i-1]
110         c=z[i]-z[i-1]
111         q[i-1]=sqrt(a*a+b*b+c*c) #———nop=6 gives q[5]
112
113     mx[0]=(x[1]-x[0])/q[0] #———mx[0]
114     my[0]=(y[1]-y[0])/q[0] #———my[0]
115     mz[0]=(z[1]-z[0])/q[0] #———mx[0]
116
117     for i in range(1,nop-1): #———average m[i]
118         mx[i]=((x[i]-x[i-1])/q[i-1]+(x[i+1]-x[i])/q[i])*.5
119         my[i]=((y[i]-y[i-1])/q[i-1]+(y[i+1]-y[i])/q[i])*.5
120         mz[i]=((z[i]-z[i-1])/q[i-1]+(z[i+1]-z[i])/q[i])*.5
121
122     mx[nop-1]=(x[nop-1]-x[nop-2])/q[nop-2] #—mx[nop-1]
123     my[nop-1]=(y[nop-1]-y[nop-2])/q[nop-2] #—my[nop-1]
124     mz[nop-1]=(z[nop-1]-z[nop-2])/q[nop-2] #—mz[nop-1]
125
126     #————————————calculate coefficients
127     for i in range(0,nop-1):
128         dx[i]=x[i]

129         dy[i]=y[i]
130         dz[i]=z[i]
131         cx[i]=mx[i]
132         cy[i]=my[i]
133         cz[i]=mz[i]
134         bx[i]=(3*x[i+1]-2*cx[i]*q[i]-3*dx[i]-mx[i+1]*q[i])/(q[i]*q[i])
135         by[i]=(3*y[i+1]-2*cy[i]*q[i]-3*dy[i]-my[i+1]*q[i])/(q[i]*q[i])
136         bz[i]=(3*z[i+1]-2*cz[i]*q[i]-3*dz[i]-mz[i+1]*q[i])/(q[i]*q[i])
137         ax[i]=(mx[i+1]-2*bx[i]*q[i]-cx[i])/(3*q[i]*q[i])
138         ay[i]=(my[i+1]-2*by[i]*q[i]-cy[i])/(3*q[i]*q[i])
139         az[i]=(mz[i+1]-2*bz[i]*q[i]-cz[i])/(3*q[i]*q[i])
140
141     #————————————plot spline between data points
142     for i in range(0,nop-1):
143         for qq in np.arange(0,q[i],2):
144             xp=ax[i]*qq*qq*qq+bx[i]*qq*qq+cx[i]*qq+dx[i]
145             yp=ay[i]*qq*qq*qq+by[i]*qq*qq+cy[i]*qq+dy[i]
146             zp=az[i]*qq*qq*qq+bz[i]*qq*qq+cz[i]*qq+dz[i]
147             rotx(xp,yp,zp,Rx) #———Rx rotation
148             xp=g[0]-xc
149             yp=g[1]-yc
150             zp=g[2]-zc
151             roty(xp,yp,zp,Ry) #———Ry rotation
152             xp=g[0]-xc
153             yp=g[1]-yc
154             zp=g[2]-zc
155             rotz(xp,yp,zp,Rz) #———Rz rotation
156             if qq==0: #—plot first point red
157                 xplast=g[0]
158                 yplast=g[1]
159             plt.plot([xplast,g[0]],[yplast,g[1]],linewidth=.7,color=clr)
160             xplast=g[0]
161             yplast=g[1]

162
163 #=============================================plot bottom spline
164 def plotbottomspline(x,y,z,Rx,Ry,Rz,clr):
165     xbottom=[0]*nop
166     ybottom=[0]*nop
167     zbottom=[0]*nop
168     for i in range(0,nop):
169         xbottom[i]=x[i]
170         ybottom[i]=y[i]
171         zbottom[i]=0
172     plotspline(xbottom,ybottom,zbottom,Rx,Ry,Rz, clr)
173
174 #=================================================control
175 #x=[20,40,60,80] #—LOCAL coords-Fig(3D Spline 1)
176 #y=[30,30,30,30]
177 #z=[15,33,28,17]
178
179 x=[10,30,65,60,80,95,130,140 #–LOCAL coordinates-Figs(3D Splines 2,3 and 4)
180 y=[20,35,50,32,60,50,65,60]
181 z=[42,30,22,28,45,55,55,55]
182
183 nop=len(x) #—number of data points
184
185 g=[0]*3 #—global plotting coords returned by rotx, roty and rotz
186
187 xc=80 #—origin of X,Y,Z coordinate system
188 yc=20
189 zc=10
190
191 Rxd=-100 #—rotations of X,Y,Z system degrees
192 Ryd=-135
193 Rzd=8
194
195 Rx=radians(Rxd) #———rotations of X,Y,Z system radians
196 Ry=radians(Ryd)
197 Rz=radians(Rzd)

198
199 #———————————————————plot X,Y,Z axes
200 plotaxis(30,0,0,Rx,Ry,Rz) #—plot X axis
201 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
202 plt.text(g[0]-5,g[1]-1,'X')
203
204 plotaxis(0,30,0,Rx,Ry,Rz) #—plot Y axis
205 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
206 plt.text(g[0],g[1]-5,'Y')
207
208 plotaxis(0,0,30,Rx,Ry,Rz) #—plot Z axis
209 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
210 plt.text(g[0]-2,g[1]+3,'Z')
211
212 #———————————————————plot data
213 plotdata(x,y,z,Rx,Ry,Rz)
214
215 #———————————————————plot spline
216 clr='g' #—————————–spline color
217 plotspline(x,y,z,Rx,Ry,Rz,clr)
218
219 #———————————————————plot bottom spline
220 clr='b' #——————————bottom spline color
221 plotbottomspline(x,y,z,Rx,Ry,Rz,clr)
222
223 #————————————————————labels
224 plt.text(120,90,'Rx=')
225 Rxd='%7.1f'%(Rxd)
226 plt.text(132,90,Rxd)
227
228 plt.text(120,85,'Ry=')
229 Ryd='%7.1f'%(Ryd)
230 plt.text(132,85,Ryd)
231
232 plt.text(120,80,'Rz=')
233 Rzd='%7.1f'%(Rzd)
234 plt.text(132,80,Rzd)
235
236 plt.text(90,90,'xc=')
237 xc='%7.1f'%(xc)
238 plt.text(100,90,xc)
239
240 plt.text(90,85,'yc=')
241 yc='%7.1f'%(yc)
242 plt.text(100,85,yc)
243
244 plt.text(90,80,'zc=')

245 zc='%7.1f'%(zc)
246 plt.text(100,80,zc)
247
248 plt.text(4,90,'x')
249 plt.text(7,90,x)
250 plt.text(4,85,'y')
251 plt.text(7,85,y)
252 plt.text(4,80,'z')
253 plt.text(7,80,z)
254
255 plt.title('3D Spline 4')
256
257 plt.show()
Listing 9-1Program SPLINE3D

9.1 3D 曲面

在上一节中,您了解了如何用三维样条连接数据点。在本节中,您将使用这些技术来创建一个三维表面。图 9-6 显示了一个表面 z=z(x,y)。它由 x,y,z 空间中的 16 个数据点定义。为了给出表面的外观,这些点通过样条线相互连接。绿色样条连接 y 方向的点,蓝色样条连接 x 方向的点。既然您已经知道如何在三维空间中创建样条,那么问题就变成了以正确的顺序排列数据点。

清单 9-2 与清单 9-1 相似,尽管为了简单起见,已经删除了该程序的一些特性;不绘制从数据点到 x,y *面的垂直线,也不绘制样条在 x,y *面上的投影。

清单 9-2 的本质包含在从第 140 行开始的“控制”部分中。图 9-6 所示的 16 个数据点由第 168-182 行的列表定义。第 168-170 行列表中的第一组点定义了第一个 y 方向样条(绿色)中显示的数据点。该样条位于 y,z *面,其中 x=0。点 x1[ ]、y1[ ]、z1[ ]指的是该样条内的四个点;x2[ ]、y2[ ]、z2[ ]指的是第二条样条内的点,依此类推。第一条样条曲线中的第一个点位于 0,0,0 处。这些坐标在行 168-170 中被指定为 x1[0],y1[0],z1[0]。第一条样条曲线中的第二个点位于 0,10,43。这些坐标被指定为 x1[1],y1[1],z1[1]。类似地,x1[2],y1[2],z1[2]和 x1[3],y1[3],z1[3]指的是第一个 y 方向样条中的第三和第四个点。第 187-190 行通过调用函数 plotdata()以这些列表作为参数来绘制数据点。第 194-197 行调用函数 plotspline(),同样使用这些列表作为参数,绘制第一条 y 方向样条线。第 172-174 行以及第 188 和 195 行绘制了 x = 20 处的数据点和第二条绿色样条曲线,对于 x = 40 和 x = 60 处的其余两条样条曲线,以此类推。要绘制 x 方向样条曲线,您可以做同样的事情,只是您必须首先重新定义坐标列表。这发生在第 200-214 行。蓝色样条线绘制在线 218-221 中。

当然,每个坐标列表可以包含四个以上的项目。第 170-184 行的列表中定义的数据点都位于网格中。他们不需要。

虽然它的工作,这里使用的方法来安排数据绘图是非常繁琐的。这也需要大量的编码。这里这样做是为了说明所用的程序。通过使用数组可以大大缩短这个时间,我们将在下一节中使用数组。

A456962_1_En_9_Fig6_HTML.jpg

图 9-6

Surface produced by Listing 9-2

1   """
2   SURFACE3D
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sqrt, radians, sin, cos
8
9   plt.axis([0,150,0,100])
10  plt.axis('on')
11  plt.grid(True)
12
13  #===================================================rotation transformations
14  def rotx(xp,yp,zp,Rx):
15      g[0]=xp+xc
16      g[1]=yp*cos(Rx)-zp*sin(Rx)+yc
17      g[2]=yp*sin(Rx)+zp*cos(Rx)+zc
18      return[g]
19
20  def roty(xp,yp,zp,Ry):
21      g[0]=xp*cos(Ry)+zp*sin(Ry)+xc
22      g[1]=yp+yc
23      g[2]=-xp*sin(Ry)+zp*cos(Ry)+zc
24      return[g]
25
26  def rotz(xp,yp,zp,Rz):
27      g[0]=xp*cos(Rz)-yp*sin(Rz)+xc
28      g[1]=xp*sin(Rz)+yp*cos(Rz)+yc
29      g[2]=zp+zc
30      return[g]
31
32  #============================================================plot axis
33  def plotaxis(xp,yp,zp,Rx,Ry,Rz):
34      rotx(xp,yp,zp,Rx) #———Rx rotation
35      xp=g[0]-xc
36      yp=g[1]-yc
37      zp=g[2]-zc
38      roty(xp,yp,zp,Ry) #———Ry rotation
39      xp=g[0]-xc
40      yp=g[1]-yc
41      zp=g[2]-zc
42      rotz(xp,yp,zp,Rz) #———Rz rotation
43      return[g]
44
45  #======================================================plot data 46  def plotdata(x,y,z,Rx,Ry,Rz):
47      for i in range(0,nop):
48          xp=x[i]
49          yp=y[i]
50          zp=z[i]
51          rotx(xp,yp,zp,Rx)
52          xp=g[0]-xc
53          yp=g[1]-yc
54          zp=g[2]-zc
55          roty(xp,yp,zp,Ry)
56          xp=g[0]-xc
57          yp=g[1]-yc
58          zp=g[2]-zc
59          rotz(xp,yp,zp,Rz)
60          xp=g[0]-xc
61          yp=g[1]-yc
62          zp=g[2]-zc
63          plt.scatter(g[0],g[1],s=25,color='g')
64
65  #========================================================plotspline( )
66  def plotspline(x,y,z,Rx,Ry,Rz,clr):
67      q=[0]*nop
68      mx=[0]*nop
69      my=[0]*nop
70      mz=[0]*nop
71      cx=[0]*nop
72      cy=[0]*nop
73      cz=[0]*nop
74      dx=[0]*nop
75      dy=[0]*nop
76      dz=[0]*nop
77      bx=[0]*nop
78      by=[0]*nop
79      bz=[0]*nop
80      ax=[0]*nop
81      ay=[0]*nop
82      az=[0]*nop
83
84      for i in range(1,nop): #—chords q(i)
85          a=x[i]-x[i-1]
86          b=y[i]-y[i-1]

87          c=z[i]-z[i-1]
88          q[i-1]=sqrt(a*a+b*b+c*c) #—nop=6 gives q[5]
89
90      mx[0]=(x[1]-x[0])/q[0] #—mx[0]
91      my[0]=(y[1]-y[0])/q[0] #—my[0]
92      mz[0]=(z[1]-z[0])/q[0] #—mx[0]
93
94      for i in range(1,nop-1): #—average m[i]
95          mx[i]=((x[i]-x[i-1])/q[i-1]+(x[i+1]-x[i])/q[i])*.5
96          my[i]=((y[i]-y[i-1])/q[i-1]+(y[i+1]-y[i])/q[i])*.5
97          mz[i]=((z[i]-z[i-1])/q[i-1]+(z[i+1]-z[i])/q[i])*.5
98
99      mx[nop-1]=(x[nop-1]-x[nop-2])/q[nop-2] #—mx[nop-1]
100     my[nop-1]=(y[nop-1]-y[nop-2])/q[nop-2] #—my[nop-1]
101     mz[nop-1]=(z[nop-1]-z[nop-2])/q[nop-2] #—mz[nop-1]
102
103     #————————————calculate coefficients
104     for i in range(0,nop-1):
105         dx[i]=x[i]
106         dy[i]=y[i]
107         dz[i]=z[i]
108         cx[i]=mx[i]
109         cy[i]=my[i]
110         cz[i]=mz[i]
111         bx[i]=(3*x[i+1]-2*cx[i]*q[i]-3*dx[i]-mx[i+1]*q[i])/(q[i]*q[i])
112         by[i]=(3*y[i+1]-2*cy[i]*q[i]-3*dy[i]-my[i+1]*q[i])/(q[i]*q[i])
113         bz[i]=(3*z[i+1]-2*cz[i]*q[i]-3*dz[i]-mz[i+1]*q[i])/(q[i]*q[i])
114         ax[i]=(mx[i+1]-2*bx[i]*q[i]-cx[i])/(3*q[i]*q[i])
115         ay[i]=(my[i+1]-2*by[i]*q[i]-cy[i])/(3*q[i]*q[i])
116         az[i]=(mz[i+1]-2*bz[i]*q[i]-cz[i])/(3*q[i]*q[i])
117
118     #——————————————plot splines between data points
119     for i in range(0,nop-1):
120         for qq in np.arange(0,q[i],2):
121             xp=ax[i]*qq*qq*qq+bx[i]*qq*qq+cx[i]*qq+dx[i]
122             yp=ay[i]*qq*qq*qq+by[i]*qq*qq+cy[i]*qq+dy[i]
123             zp=az[i]*qq*qq*qq+bz[i]*qq*qq+cz[i]*qq+dz[i]
124             xp=g[0]-xc
125             yp=g[1]-yc
126             zp=g[2]-zc
127             roty(xp,yp,zp,Ry) #—Ry rotation
128             xp=g[0]-xc
129             yp=g[1]-yc
130             zp=g[2]-zc
131             rotz(xp,yp,zp,Rz) #—Rz rotation
132             if qq==0:
133                 xplast=g[0]
134                 yplast=g[1]
135             plt.plot([xplast,g[0]],[yplast,g[1]],linewidth=.7,color=clr)
136             xplast=g[0]
137             yplast=g[1]
138
139 #==================================================control
140 g=[0]*3 #—global plotting coords returned by rotx, roty and rotz
141
142 xc=80 #—origin of X,Y,Z coordinate system
143 yc=20
144 zc=10
145
146 Rxd=-100 #–rotations of X,Y,Z system degrees
147 Ryd=-135
148 Rzd=8
149
150 Rx=radians(Rxd) #—rotations of X,Y,Z system radians
151 Ry=radians(Ryd)
152 Rz=radians(Rzd)
153
154 #—————————————————————plot X,Y,Z axes
155 plotaxis(60,0,0,Rx,Ry,Rz) #—plot X axis
156 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
157 plt.text(g[0]-5,g[1]-1,'X')
158
159 plotaxis(0,60,0,Rx,Ry,Rz) #—plot Y axis
160 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
161 plt.text(g[0],g[1]-5,'Y')

162
163 plotaxis(0,0,60,Rx,Ry,Rz) #—plot Z axis
164 plt.plot([xc,g[0]],[yc,g[1]],linewidth=2,color='k')
165 plt.text(g[0]-2,g[1]+3,'Z')
166
167 #————————-define 4 sets of data points at different values of X
168 x1=[0,0,0,0] #———LOCAL coords
169 y1=[0,10,20,30]
170 z1=[50,43,30,14]
171
172 x2=[20,20,20,20]
173 y2=y1
174 z2=[25,23,19,12]
175
176 x3=[40,40,40,40]
177 y3=y1
178 z3=[14,15,13,9]
179
180 x4=[60,60,60,60]
181 y4=y1
182 z4=[7,10,10,9]
183
184 nop=len(x1) #———number of data points
185
186 #—————————————————————plot data points
187 plotdata(x1,y1,z1,Rx,Ry,Rz)
188 plotdata(x2,y2,z2,Rx,Ry,Rz)
189 plotdata(x3,y3,z3,Rx,Ry,Rz)
190 plotdata(x4,y4,z4,Rx,Ry,Rz)
191
192 #——————————————————plot Y direction splines
193 clr='g' #——————————spline color
194 plotspline(x1,y1,z1,Rx,Ry,Rz,clr)
195 plotspline(x2,y2,z2,Rx,Ry,Rz,clr)
196 plotspline(x3,y3,z3,Rx,Ry,Rz,clr)
197 plotspline(x4,y4,z4,Rx,Ry,Rz,clr)
198
199 #——————————redefine the data points at different values of y
200 xx1=[0,20,40,60]
201 yy1=[y1[3],y2[3],y3[3],y4[3]]

202 zz1=[z1[3],z2[3],z3[3],z4[3]]
203
204 xx2=xx1
205 yy2=[y1[2],y2[2],y3[2],y4[2]]
206 zz2=[z1[2],z2[2],z3[2],z4[2]]
207
208 xx3=xx1
209 yy3=[y1[1],y2[1],y3[1],y4[1]]
210 zz3=[z1[1],z2[1],z3[1],z4[1]]
211
212 xx4=xx1
213 yy4=[y1[0],y2[0],y3[0],y4[0]]
214 zz4=[z1[0],z2[0],z3[0],z4[0]]
215
216 #——————————————————plot X direction splines
217 clr='b' #——————————spline color
218 plotspline(xx1,yy1,zz1,Rx,Ry,Rz,clr)
219 plotspline(xx2,yy2,zz2,Rx,Ry,Rz,clr)
220 plotspline(xx3,yy3,zz3,Rx,Ry,Rz,clr)
221 plotspline(xx4,yy4,zz4,Rx,Ry,Rz,clr)
222
223 #————————————————————————labels
224 plt.text(120,90,'Rx=')
225 Rxd='%7.1f'%(Rxd)
226 plt.text(130,90,Rxd)
227
228 plt.text(120,85,'Ry=')
229 Ryd='%7.1f'%(Ryd)
230 plt.text(130,85,Ryd)
231
232 plt.text(120,80,'Rz=')
233 Rzd='%7.1f'%(Rzd)
234 plt.text(130,80,Rzd)
235
236 plt.title('3D Surface')
237
238 plt.show()

Listing 9-2Program SURFACE3D

9.2 3D 表面着色

在上一节中,您通过用样条线连接数据点构建了一个曲面。你没有使用数组,而是依赖于一个繁琐的编号系统。虽然这保持了过程的开放性和易理解性,但却导致了太多的代码行。在本节中,您将使用相同的数据集,但有两个不同之处:首先,您将通过直线连接数据点;其次,您将使用数组来组织您的绘图。当您看到数组的使用是如此简单和优雅时,您可能会问哪种方法最容易编码和遵循。

使用与前一节中相同的三维数据集,定义数据的数组是

A456962_1_En_9_Figa_HTML.jpg

列表 9-3 使用该数组产生图形 9-7 。用于将 A 与表面点相关联的编号方案如图 9-8 所示。

A456962_1_En_9_Fig8_HTML.jpg

图 9-8

Data point numbering scheme used in Listing 9-3

A456962_1_En_9_Fig7_HTML.jpg

图 9-7

Shaded 3D surface produced by Listing 9-3

中的每个元素都是一个列表。共有 16 个列表:A[0]到 A[15]。列表 I 被引用为[i],其中 i=0→15。比如 A[3]=[0,30,14]。每个列表 I 定义了数据点 I 的 x,y,z 坐标,即

$$ A\left[i,1\right]=x(i) $$

(9-2)

$$ A\left[i,2\right]=y(i) $$

(9-3)

$$ A\left[i,3\right]=z(i) $$

(9-4)

比如第一个点,点 0,有坐标

$$ A\left[0,1\right]=x(0)=0 $$

(9-5)

$$ A\left[0,2\right]=y(0)=0 $$

(9-6)

$$ A\left[0,3\right]=z(0)=50 $$

(9-7)

这种方法取代了上一节中使用的列表编号系统。

参考图 9-8 ,为了得到编号为 3 的第四个数据点的 z 坐标,通过让 i=3,j=2 来访问数组 A 的第四个列表的第三个元素。与列表一样,数组中元素的编号从 0 开始,因此第四个数据点的坐标包含在列表 i=3 中。z 分量是列表中的第三个元素,j=2。因此,第四个数据点的 z 坐标是 A[3,2],所以

print(A[3,2])

14

图 9-8 中的编号方案从点 0 开始,该点位于 x=0,y=0,z=50 处表面的上角,并沿 y 方向前进,总共 4 个数据点。然后,对于另一组 4 个 y 方向点,它前进到新的 x 值。这总共给出了 16 个数据点。可以使用其他编号方案。例如,你可以从同一点开始,但是先从 x 方向开始,而不是 y 方向。或者你可以从表面的另一个角落开始。正如您将看到的,无论选择什么编号方案,都会对该数据的后续操作产生影响。

曲面由四边形组成,称之为面片。您将对这些补丁进行着色。每个面片由四个数据点定义。因为它们位于三维空间中,所以贴片通常不会是*的。此外,由于边的长度可以是任意的,所以贴片不一定是矩形的。将使用前几章中使用的基本着色技术(即通过在补丁上画线来给补丁着色),但必须修改该技术。

图 9-9 显示了模型。这是一个由编号为 0 → 3 的四个角定义的普通倾斜面片。q03 和 q12 是从 0 → 3 和 1 → 2 的边长。如前所述,这些边是三维的,不一定*行。就像前面章节中关于阴影的处理一样,你可以通过在四边形上画线来填充颜色。显示的蓝线是示例。如图 9-10 和 9-11 所示,你将在这里开发的算法将适用于任何四边形。

要绘制这些线,您只需确定每条线沿 0,3 边的起始位置 S 和沿 1,2 边的终止位置 E。由于这些边的长度不同,S 从 0 点到 0,3 边的距离 q 与 E 从 1 点到 1,2 边的距离不同。线 S 的起点在 q=0 时从补片的顶部(角 0)开始,并前进到 q=q03 时的底部。为了得到 E 下边 1,2 的相应位置,你用距离 q 除以 q12/q03。然后在 S 和 e 之间画一条线。图 9-9 中显示的蓝线分别是补丁两边的 70%、80%和 90%。

图 9-9 中所示的单位矢量 nˇ不是画线所必需的,但在确定着色强度时会用到。这是像以前一样通过取 n 与光源单位矢量l 的点积来完成的

在清单 9-3 中,图 9-9 中的普通补片角的编号 0→3 被替换为数组 a 中曲面上每个补片的相应编号。第 169 行给出了 A 中数据点的数量,它等于列表的数量,每个列表定义一个点的位置。在这种情况下,nop=16。数据点绘制在线 172-194 中。这个简单的例程说明了使用数组的好处,取代了以前程序中使用的数据绘图功能。第 178-185 行通过第 177 行中指定的颜色 clr 线连接四个 y 方向点。函数 plotline()绘制直线。第 188-194 行在 x 方向做同样的事情。

A456962_1_En_9_Fig9_HTML.jpg

图 9-9

Patch model used in Listing 9-3

通过调用从第 65 行开始的函数 shade(),第 197-205 行中的补丁被着色。参数的排列符合图 9-9 所示的通用补丁角。第 197-199 行在 y 方向上对第一行补片加阴影。第一个面片的左上角位于 A[0,0],第二个面片的左上角位于 A[1,0],依此类推。在循环的第一个循环中,i=0,第 198 行和第 199 行给出了以下面片角坐标,这些坐标用作函数 shade 调用中的参数:

  • A[0,0] = x[0] = x0 = 0 角 0
  • A[0,1] = y[0] = y0 = 0
  • A[0,2] = z[0] = z0 = 50
  • A[1,0] = x[1] = x1 = 0 角 1
  • A[1,2] = y[1] = y1 = 10
  • A[1,3] = z[1] = z1 = 43
  • A[5,0] = x[5] = x2 = 0 角 2
  • A[5,1] = y[5] = y2 = 20
  • A[5,2] = z[5] = z2 = 30
  • A[4,0] = x[4] = x3 = 0 角 3
  • A[4,1] = y[4] = y3 = 30
  • A[4,2] = z[4] = z3 = 14

在函数 shade()中,第 65 行的参数与上面的面片角 0、1、2 和 3、

$$ \left(\underset{corner\kern0.5em 0}{\underbrace{\overset{A\left[0\right]}{\overbrace{x0,\kern0.5em y0,\kern0.5em z0,}}}}\kern0.5em \underset{corner\kern0.5em 1}{\underbrace{\overset{A\left[1\right]}{\overbrace{x1,\kern0.5em y1,\kern0.5em z1,}}\kern0.5em }}\underset{corner\kern0.5em 2}{\underbrace{\overset{A\left[5\right]}{\overbrace{x2,\kern0.5em y2,\kern0.5em z2,}}}}\underset{corner\kern0.5em 3}{\underbrace{\kern0.5em \overset{A\left[4\right]}{\overbrace{x3,\kern0.5em y3,\kern0.5em z3,}}}}\right) $$

重合

当 i=1 时,这些相同的程序行给出了 y 方向上的下一个面片的角坐标,该面片具有角 1、2、6 和 5。这些对应于通用面片角 0、1、2 和 3。循环的剩余循环在 y 方向上对剩余的两个面片进行着色。线 200-202 和 203-205 在 x 方向上前进并执行相同的操作,从而对所有九个补片进行着色。

您可能想知道为什么第 197 行中的 for 循环,对于范围(0,3)中的 I,使用索引 3 而不是 2。毕竟只有三个 y 方向的面片需要着色;0 → 3 似乎等于 4。它与 range()函数的工作方式有关。一般来说,语法是 range(开始、停止、步进)。如果没有指定步长,则假设步长为 1。范围将从 start 开始,以 step 的步长转到 stop,但不会返回 stop 处的值。在第 197 行,0 是起始值,3 是终止值。这将返回 i=0,1 和 2,但不是 3。这在第一章中有所解释。你可以自己试试这个:

for i in range(0,3):
    print(i)

0
1
2

人们很容易将 stop 视为要返回的值的数量,但事实并非如此。例如,

for i in range(1,3):
    print(i)

1
2

如果未指定起始值,它会自动设置为 0:

for i in range(3):
    print(i)

0
1
2

在本文中,为了清楚起见,我通常包括起始值,但我通常不指定步长值,除非它不同于 1。

在函数 shade()中,第 66-92 行计算单位向量 uˇ、vˇ、wˇ和 nˇ。

第 94-96 行指定了ˇl 的分量,即入射光方向单位向量,就像在以前的着色程序中所做的那样。第 98 行取 nˇ与l的点积。第 100-103 行定义了阴影函数并建立了影响补片的光强 I。第 105 行混合了红、绿、蓝三种颜色。线 107-115 绘出了穿过补丁的线。第 117 行绘制了这些线。请注意,这些线具有在第 105 行中建立的颜色。

A456962_1_En_9_Fig11_HTML.jpg

图 9-11

Shaded oblique patch

A456962_1_En_9_Fig10_HTML.jpg

图 9-10

Shaded oblique patch

1   """
2   SHADEDSURFACE3D
3   """
4
5   import matplotlib.pyplot as plt
6   import numpy as np
7   from math import sqrt, radians, sin, cos
8
9   plt.axis([0,150,0,100])
10  plt.axis('on')
11  plt.grid(True)

12
13  #=========================================rotation transformations
14
15  #————————same as Listing 9-2, Program SURFACE3D———————
16
17  #========================================================plot axes
18
19  #————————same as Listing 9-2, Program SURFACE3D———————
20
21  #=======================================================plot point
22  def plotpoint(xp,yp,zp,Rx,Ry,Rz,clr):
23      rotx(xp,yp,zp,Rx)
24      xp=g[0]-xc
25      yp=g[1]-yc
26      zp=g[2]-zc
27      roty(xp,yp,zp,Ry)
28      xp=g[0]-xc
29      yp=g[1]-yc
30      zp=g[2]-zc
31      rotz(xp,yp,zp,Rz)
32      plt.scatter(g[0],g[1],s=10,color=clr)
33
34  #=======================================================plotline
35  def plotline(xb,yb,zb,xe,ye,ze,Rx,Ry,Rz,clr):
36      rotx(xb,yb,zb,Rx) #———rotate line beginning coordinates
37      xb=g[0]-xc
38      yb=g[1]-yc
39      zb=g[2]-zc
40      roty(xb,yb,zb,Ry)
41      xb=g[0]-xc
42      yb=g[1]-yc
43      zb=g[2]-zc
44      rotz(xb,yb,zb,Rz)
45      xb=g[0]
46      yb=g[1]
47      zb=g[2]
48
49      rotx(xe,ye,ze,Rx) #———rotate line end coordinates
50      xe=g[0]-xc
51      ye=g[1]-yc
52      ze=g[2]-zc
53      roty(xe,ye,ze,Ry)
54      xe=g[0]-xc
55      ye=g[1]-yc
56      ze=g[2]-zc
57      rotz(xe,ye,ze,Rz)
58      xe=g[0]
59      ye=g[1]
60      ze=g[2]
61
62  plt.plot([xb,xe],[yb,ye],linewidth=.7,color=clr)
63
64  #================================================shade
65  def shade(x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3,Rx,Ry,Rz,clr):
66      a=x3-x0
67      b=y3-y0
68      c=z3-z0
69      q03=np.sqrt(a*a+b*b+c*c)
70      ux=a/q03
71      uy=b/q03
72      uz=c/q03
73
74      a=x1-x0
75      b=y1-y0
76      c=z1-z0
77      q02=sqrt(a*a+b*b+c*c)
78      vx=a/q02
79      vy=b/q02
80      vz=c/q02
81
82      a=x2-x1
83      b=y2-y1
84      c=z2-z1
85      q12=np.sqrt(a*a+b*b+c*c)
86      wx=a/q12
87      wy=b/q12
88      wz=c/
q12

89
90      nx=uy*vz-uz*vy
91      ny=uz*vx-ux*vz
92      nz=ux*vy-uy*vx
93
94      lx=0
95      ly=-.7
96      lz=0
97
98      ndotl=nx*lx+ny*ly+nz*lz
99
100     IA=.01
101     IB=1
102     n=2.8
103     I=IA+(IB-IA)*((1-ndotl)/2)**n
104
105     clr=(1-I,.4*(1-I),.6*(1-I))
106
107     r=q12/q03
108     dq=q03/50
109     for q in np.arange(0,q03+1,dq):
110        xb=x0+ux*q
111        yb=y0+uy*q
112        zb=z0+uz*q
113        xe=x1+wx*q*r
114        ye=y1+wy*q*r
115        ze=z1+wz*q*r
116
117        plotline(xb,yb,zb,xe,ye,ze,Rx,Ry,Rz,clr)
118
119     plt.text(121,70,'lx=')
120     lx='%7.3f'%(lx)
121     plt.text(130,70,lx)
122
123     plt.text(121,65,'ly=')
124     ly='%7.3f'%(ly)
125     plt.text(130,65,ly)
126
127     plt.text(121,60,'lz=')
128     lz='%7.3f'%(lz)
129     plt.text(130,60,lz)
130
131     plt.text(121,50,'IA=')

132     IA='%7.3f'%(IA)
133     plt.text(130,50,IA)
134
135     plt.text(121,45,'IB=')
136     IB='%7.3f'%(IB)
137     plt.text(130,45,IB)
138
139     plt.text(121,40,'n=')
140     n='%7.3f'%(n)
141     plt.text(130,40,n)
142
143 #======================================================control
144 g=[0]*3 #———global plotting coords returned by rotx, roty and rotz
145
146 xc=80 #———origin of X,Y,Z coordinate system
147 yc=20
148 zc=10
149
150 Rxd=-100 #——–rotations of X,Y,Z system degrees
151 Ryd=-135
152 Rzd=8
153
154 Rx=radians(Rxd) #———rotations of X,Y,Z system radians
155 Ry=radians(Ryd)
156 Rz=radians(Rzd)
157
158 #—————————————————————plot X,Y,Z axes
159
160 #————————same as Listing 9-2, Program SURFACE3D———————
161
162 #———————————————define data point array A
163
164 A=np.array([ [0,0,50], [0,10,43], [0,20,30], [0,30,14],
165             [20,0,25], [20,10,23], [20,20,19], [20,30,12],
166             [40,0,14], [40,10,15], [40,20,13], [40,30,9],
167             [60,0,7], [60,10,10], [60,20,10], [60,30,9] ])
168
169 nop=len(A) #———number of data points
170
171 #—————————————————————plot data points
172 clr="k"
173 for i in range(0,16):
174     plotpoint(A[i,0],A[i,1],A[i,2],Rx,Ry,Rz,clr)
175
176 #——————————————–connect data points in Y direction
177 clr="k" #——————————line color
178 for i in range(0,3):
179     plotline(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],Rx,Ry,Rz,clr)
180 for i in range(4,7):
181     plotline(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],Rx,Ry,Rz,clr)
182 for i in range(8,11):
183     plotline(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],Rx,Ry,Rz,clr)
184 for i in range(12,15):
185     plotline(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],Rx,Ry,Rz,clr)
186
187 #———————————————connect data points in X direction
188 clr="k" #——————————line color
189 for i in range(0,4):
190     plotline(A[i,0],A[i,1],A[i,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
191 for i in range(4,8):
192     plotline(A[i,0],A[i,1],A[i,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
193 for i in range(8,12):
194     plotline(A[i,0],A[i,1],A[i,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
195
196 #——————————————————————shade patches
197 for i in range(0,3):
198     shade(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],A[i+5,0],
199         A[i+5,1],A[i+5,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
200 for i in range(4,7):
201     shade(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],A[i+5,0],
202         A[i+5,1],A[i+5,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
203 for i in range(8,11):
204     shade(A[i,0],A[i,1],A[i,2],A[i+1,0],A[i+1,1],A[i+1,2],A[i+5,0],
205         A[i+5,1],A[i+5,2],A[i+4,0],A[i+4,1],A[i+4,2],Rx,Ry,Rz,clr)
206
207 #————————————————————————labels
208 plt.text(121,90,'Rx=')
209 Rxd='%7.1f'%(Rxd)
210 plt.text(130,90,Rxd)
211
212 plt.text(121,85,'Ry=')
213 Ryd='%7.1f'%(Ryd)
214 plt.text(130,85,Ryd)
215
216 plt.text(121,80,'Rz=')
217 Rzd='%7.1f'%(Rzd)
218 plt.text(130,80,Rzd)
219
220 plt.title('Shaded 3D Surface')
221
222 plt.show()
Listing 9-3Program SHADEDSURFACE3D

9.3 总结

在本章中,您了解了如何绘制三维数据。为了做到这一点,您将通常的 z 轴指向屏幕的方向改为 z 向上;x 和 y 在水*面上。这是 Pz=f(Px,Py)时常用的数据显示方式;Px、Py 和 Pz 是数据点的坐标。在清单 9-1 中,您用样条连接了数据点。作为可视化的辅助手段,您将样条向下投影到 x,y *面上。它可以毫无困难地投影到其他坐标*面上。你使用的 3D 样条算法是第八章中介绍的 2D 样条的外推。在清单 9-2 中,你通过在 x 和 y 方向用样条连接点构建了一个表面。然后你给三维表面涂上阴影。这需要用直线而不是样条来连接数据点。结果是一组倾斜的补丁,不一定是*面的;它们中的每一个都可能扭曲出*面。您学习了如何通过对每个面片进行着色来对曲面进行着色。这需要开发一种能够对非*面倾斜四边形进行着色的算法。阴影是通过在每个小块的表面上画线来实现的;颜色的强度由贴片相对于照明光线方向的方向决定。

十、示例

在这一章中,你将应用在前面章节中开发的一些技术来制作一些有趣的图像。这些图片应该让您对 Python 图形可以完成的事情有所了解。

10.1 土星

土星以它的光环而闻名。虽然木星、土星、天王星和海王星也有光环,但土星是太阳系中最大、最亮、最著名的。它们由小到灰尘,大到巨石大小的物体组成。这些物体主要由冰组成,被认为是彗星或大型小行星与土星的一颗卫星相撞时产生的,两者都碎成了碎片。土星自古以来就为人所知,但在 1610 年,伽利略首次用望远镜观察到了它。这颗行星是以罗马农业之神土星命名的,就像我们的第六天,星期六一样。

清单 10-1 建立在更早的程序上,清单 7-2 来自第七章。除了引入构建土星环和土星在环上的阴影的算法之外,这个程序在这里基本保持不变。

图 10-1 到 10-5 显示了列表 10-1 产生的图像。它们处于不同的方位角度,这些都列在标题中。还列出了入射光线的单位矢量分量。比如 lx=+.707,ly=+.707,lz=0 表示左上象限的一个源;lx=-1,ly=0,lz=0 表示光源来自右侧。在图像中,请注意行星投在光环上的阴影,特别是图 10-5 ,它显示了行星的曲率。

作为比较,土星的照片可以在 www.jpl.nasa.gov/spaceimages/?search=saturn&category=#submit 找到。

A456962_1_En_10_Fig5_HTML.jpg

图 10-5

Saturn with rings and shadow 5: Rx=20, Ry=0, Rz=30, lx=-1, ly=0, lz=0 (produced by Listing 10-1)

A456962_1_En_10_Fig4_HTML.jpg

图 10-4

Saturn with rings and shadow 4: Rx=-10, ry=0, Rz=25, lx=-.707, ly=-.707, lz=0 (produced by Listing 10-1)

A456962_1_En_10_Fig3_HTML.jpg

图 10-3

Saturn with rings and shadow 3, Rx=-20, Ry=0, Rz=25, lx=.707, ly=.707, lz=0 (produced by Listing 10-1)

A456962_1_En_10_Fig2_HTML.jpg

图 10-2

Saturn with rings and shadow 2: Rx=-8, Ry=0, Rz=30, lx=.707, ly=.707, lz=0 (produced by Listing 10-1)

A456962_1_En_10_Fig1_HTML.jpg

图 10-1

Saturn with rings and shadow 1: Rx=-20, Ry=0, Rz=-10, lx=1, ly=0, lz=0 (produced by Listing 10-1)

图 10-6 显示了用于构建环的模型。在第七章中,你通过首先创建一个直立球体开发了阴影球体算法。也就是说,经度是垂直的,纬度是水*的(即*行于 x,z *面)。从这个起始方向开始,围绕 x、y 和 z 轴旋转球体。你在这里为戒指做同样的事情。创建*行于 x,z *面的水*环,然后将它们与球形行星体一起旋转相同的角度。环位于通过球体中心的*面上,因此球体和环具有相同的旋转中心。

A456962_1_En_10_Fig6_HTML.jpg

图 10-6

Rings model: top view of planet and rings looking down on the x,z plane with Rx=0, Ry=0, Rz=0

该波段被绘制为一系列相邻的同心圆,每个同心圆由短线段组成。参考图 10-6 和清单 10-1 ,程序行 42 和 43 设置环的内半径和外半径。第 44 行设置圆之间的距离。环被分成七个环形带(图 10-6 中未显示)以适应不同的颜色;它们的宽度是第 45 行中的δr。

每条线段都是单独旋转和绘制的。线 48 开始了从 r1 到 r2 的径向循环,绘制了圆段。线 49 开始沿圆周方向绘制的循环。线 50-61 进行旋转,在线 62 和 63 中产生全局标绘坐标 xpg 和 ypg。旋转功能与先前程序中的功能相同。

接下来,设置线段的颜色。这些光环被排列成不同颜色的条带,这是美国宇航局图像中看到的它们的物理组成的结果。这是在第 66-75 行完成的。从 r=r1 到 R1+δr 的第一个波段的颜色为 clr=(.63,. 54,.. 18),其余波段以此类推。你省略了第五个波段,它是空的;背景色清晰可见。第六波段是其他波段的两倍宽。这为七个波段提供了颜色。

对于给定的光线方向,在大多数方向上,行星的身体会在光环上投下阴影。参考图 10-7 ,你的目标是确定 p 点是在行星的阴影区之内还是之外。球形行星投下圆形阴影。阴影的直径将等于行星的大小,或者更准确地说,是球体的“大圆”这是用穿过球体中心的*面切割球体所能得到的最大圆。这就像把一个橘子切成两半;你看到的是橘子的大圆。在图 10-7 中,阴影可能是由这种大小的圆盘造成的,也可能是由球形行星造成的;无论哪种情况,阴影的大小都是一样的。土星大圆的侧视图显示为穿过飞机中心的粗线。从图 10-7 中的几何图形可以看出,如果 p 位于这样一个位置,使得|B| > rs,其中 rs 是土星的半径,它在阴影区之外;如果|B| < rs,p 在阴影区内。一旦你确定了 p 的位置,如果它在阴影区域内,当你画圆环的时候,你将把那个点涂成灰色。如果它在外面,你将在第 66-75 行中给它一个带颜色。

A456962_1_En_10_Fig7_HTML.jpg

图 10-7

Shadow model

你现在的工作是对于 p 的给定位置得到|B|,你从图 10-7 看到

$$ \left|\mathrm{B}\right|=\left|\mathrm{V}\right|\mathit{\sin}\left(\phi \right) $$

(10-1)

你知道

$$ \mathrm{V}\times \widehat{\mathrm{u}}=\left|\mathrm{V}\right|\left|\widehat{\mathrm{u}}\right|\mathit{\sin}\left(\phi \right) $$

(10-2)其中 uˇ=-ˇl .结合以上等式用| uˇ| = 1、

$$ \mathrm{B}=\mathrm{V}\times \widehat{\mathrm{u}} $$

(10-3)

$$ \left|\mathrm{B}\right|=\left|\mathrm{V}\times \widehat{\mathrm{u}}\right| $$

(10-4)

在清单 10-1 中,第 78 行确定了入射光矢量的长度,ˇl。它应该等于 1,但如果第 23-25 行输入的分量不等于 1(即$$ \sqrt{l{x}²+l{y}²+l{y}²}\ne 1 $$),它可能不等于 1。如果需要,第 79-81 行重新建立组件。第 82-84 行建立了矢量 v 的分量。第 85-87 行计算 B 的分量。第 88 行给出了它的大小 magB=|B|。第 89 行确定 p 是否在阴影区内。如果是,则执行第 90 行。这是 V 与ˇl 的点积,它决定了 p 是否位于行星朝向光源的一侧,在这种情况下,它与行星的黑暗面相对,不在阴影区。这是必要的,因为第 78-89 行的阴影算法没有进行这种区分。如果 p 确实位于阴影区内的暗侧,则颜色在第 91 行被设置为中灰色。

你会注意到在上面的图片中,光环内有一条黑色的带。这是因为土星环在那一带有一个空洞:那里没有粒子来反射光线;你看到的是背景色,“午夜蓝”,透过。这就产生了一个问题,因为阴影颜色会在空白区域覆盖背景颜色。第 93 行和第 94 行将其重建为“午夜蓝”。

现在,波段颜色已经确定,您可以绘制环。这是通过绘制短线段来完成的。第 97-100 行计算第一段的起始位置。参考图 10-6 ,线 100-101 确定该线段是否在行星前面,在这种情况下,绘制该线段。第 103-108 行确定它是否在行星后面,在这种情况下,它不被标绘。这是通过计算该点的全球坐标与行星中心的距离 c 来实现的。第 107 行说,如果 c 大于球的半径乘以 1.075,那么画出线段。包括因子 1.075 是为了防止线段蚕食球体的边缘。需要经过这个逻辑;否则,位于球体半径内的前方可见线段将不会被绘制。

关于清单 10-1 生成的上述图像,可以注意到两点。首先是颜色。美国宇航局的照片显示了灰色的色调,几乎没有颜色。但是许多土星的观察者描述它有金色的色调,因此我选择了它的颜色。任何摄影师都知道,在摄影图像中捕捉物体的真实颜色是很困难的;这很大程度上取决于入射光的颜色和图像捕捉介质。也许最好依靠观星者的观察。如果你不同意清单 10-1 生成的图像中的颜色,你可以通过改变程序中的 clr 定义来修改它们。第二件要注意的事情是在图 10-5 中跟随行星曲率的阴影曲率。它表明着色算法按预期工作。

关于程序的使用,你可以在第 24-26 行改变入射光的方向,在第 32-34 行改变旋转角度。清单 10-1 需要一些时间来运行,所以请耐心等待。

1   """
2   SATURN
3   """
4  
5   import numpy as np
6   import matplotlib.pyplot as plt
7   from math import sin, cos, radians, sqrt
8  
9   plt.axis([0,150,100,0])
10  plt.axis('off')
11  plt.grid(False)
12 
13  print('running')
14  #—————————————————parameters
15  g=[0]*3
16
17  xc=80 #———sphere center
18  yc=50
19  zc=0
20
21  rs=25 #———sphere radius
22
23  lx=-1 #———light ray unit vector components
24  ly=0
25  lz=0
26
27  IA=0
28  IB=.8
29  +n=2
30
31  Rx=radians(-20)
32  Ry=radians(0)
33  Rz=radians(30)
34
35  #————————same as SHADESPHERE—————–
36
37  #———————————————————rings
38  alpha1=radians(-10)
39  alpha2=radians(370)
40  dalpha=radians(.5)

41
42  r1=rs*1.5
43  r2=rs*2.2
44  dr=rs*.02
45  deltar=(r2-r1)/7 #———ring band width
46
47  #—————————————rotate ring point p which is at r, alpha
48  for r in np.arange(r1,r2,dr):
49      for alpha in np.arange(alpha1,alpha2,dalpha):
50          xp=r*cos(alpha)
51          yp=0
52          zp=-r*sin(alpha)
53          rotx(xc,yc,zc,xp,yp,zp,Rx)
54          xp=g[0]-xc
55          yp=g[1]-yc
56          zp=g[2]-zc
57          roty(xc,yc,zc,xp,yp,zp,Ry)
58          xp=g[0]-xc
59          yp=g[1]-yc
60          zp=g[2]-zc
61          rotz(xc,yc,zc,xp,yp,zp,Rz)
62          xpg=g[0]
63          ypg=g[1]
64
65  #—————————————————select ring band color
66      if r1 <= r < r1+1*deltar:
67          clr=(.63,.54,.18)
68      if r1+1*deltar <= r <= r1+2*deltar:
69          clr=(.78,.7,.1)
70      if r1+2*deltar <= r <= r1+3*deltar:
71          clr=(.95,.85,.1)
72      if r1+3*deltar <= r <= r1+4*deltar:
73          clr=(.87,.8,.1)
74      if r1+5*deltar <= r <= r1+7*deltar:
75          clr=(.7,.6,.2)
76
77  #———————————————————————shadow
78      magu=sqrt(lx*lx+ly*ly+lz*lz)
79      ux=-lx/magu
80      uy=-ly/magu
81      uz=-lz/magu
82      vx=xc-xpg
83      vy=yc-ypg
84      vz=zc-zpg
85      Bx=uy*vz-uz*vy
86      By=uz*vx-ux*vz
87      Bz=ux*vy-uy*vx
88      magB=sqrt(Bx*Bx+By*By+Bz*Bz)
89      if magB < rs: #—————————if in the shadow region
90          if vx*lx+vy*ly+vz*lz <= 0: #———if v points toward light source
91              clr=(.5,.5,.2) #———shadow color
92
93      if r1+4*deltar <= r <= r1+5*deltar: #———overplot empty band
94          clr='midnightblue' #———with background color
95
96  #——————————————————–plot line segment
97      if alpha == alpha1:
98          xstart=xpg
99          ystart=ypg
100     if zpg <= zc: #–front (z axis points into the screen)
101         plt.plot([xstart,xpg],[ystart,ypg],linewidth=2,color=clr)
102
103     if zpg >= zc: #–back
104         a=xpg-xc
105         b=ypg-yc
106         c=sqrt(a*a+b*b)
107         if c > rs*1.075: #——plot only the visible portion of rings
108             plt.plot([xstart,xpg],[ystart,ypg],linewidth=2,color=clr)
109         xstart=xpg
110         ystart=ypg
111
112 plt.show()

Listing 10-1
Program SATURN

10.2 太阳辐射

本节展示了一个典型的场景:使用 Python 来绘制和标记代表数学函数的曲线。这里你画出了马克斯·普朗克的辐射光谱。你用它来表示太阳发出的能谱,计算太阳的总功率输出,以及到达地球的量,这个量叫做太阳常数。这一部分的科学方面非常有趣,就像它们的发展历史一样。Python 编程的主要好处是可以看到程序如何执行数值积分、建立图表以及显示数值数据。

光子和太阳

像所有辐射体一样,太阳以光子的形式发射电磁能。我们知道光子以不同的频率或波长发射,波长与频率成反比,如

$$ \lambda =\frac{s_m}{v} $$

(10-5)其中λ是波长,s m 是光在介质中的速度,ν是波在该介质中传播的频率。由于我们主要关心的是光在真空中的传播(即从太阳到地球),s m =c 其中 c 是光在真空中的速度,因此等式 10-5 变成了

$$ \lambda =\frac{c}{v} $$

(10-6)

像太阳能这样的函数,当用一系列频率或波长来表示时,叫做光谱。在电磁辐射的情况下,我们最关心的是不同频率的光的功率,或者说是波长。这被称为功率谱。例如图 10-8 所示的曲线,其中绘制了功率谱密度(通常简称为功率谱,S(λ))与波长λ的关系。这条曲线源于方程 10-7 。

A456962_1_En_10_Fig8_HTML.jpg

图 10-8

Max Planck’s Solar Spectrum

太阳发出的大部分频率,从高频、短波长的紫外线到低频、长波长的红外线,我们的人眼是看不见的。我们只能看到光谱的一小部分,幸运的是,它位于太阳发射的功率谱的峰值附*。这一定让我们狩猎采集的祖先很高兴,因为这使他们能够在一天的早些时候和晚些时候狩猎采集。虽然我们可以为此感谢太阳功率谱的形状,但这也是我们眼睛生物学的一个特征,如果我们相信查尔斯·达尔文的话,这可能是在太阳最大功率输出附*的频率下优化进化的。

10.2.2 普朗克黑体辐射

如前所述,光是光子。但是什么是光子呢?我们知道光子是电磁能量的量子化形式。但是在 19 世纪晚期,这仍然是一个谜。人们曾多次试图解释受热物质发出的光谱。例如,当我们加热一根铁拨火棒时,起初我们看不到颜色有任何变化,但在达到一定温度后,我们可以看到它发出一系列颜色:暗红色、亮红色、橙色、黄色、白色、蓝色,然后是紫色。这些颜色对应于物体发出的电磁辐射的不同频率。早期试图解释这一现象是基于当时称为麦克斯韦方程组的经典理论。这些方程描述了一个电磁场,其中电磁能量被假设为一个*滑的连续体。尽管做了许多尝试,这种方法还是无法解释所观察到的现象。

当时许多科学家都在努力解决这个问题。然后在 1900 年,德国物理学家马克斯·普朗克给一位同事寄了一张明信片。在背面,他写了一个精确描述光谱的方程式。与当时流行的理论相反,普朗克的突破是假设电磁场不是能量的连续体。相反,他猜测电磁能以离散的包存在,而不是像麦克斯韦方程所假设的那样是一个连续的场。这导致了他的突破性公式,如方程式 10-7 所示。1900 年 12 月 14 日,他向德国物理学会提交了他的想法,这一天被称为量子力学的诞生。他的方程被称为黑体辐射公式。稍后你会看到更多。

1901 年,普朗克在《物理学年鉴》的一篇文章中发表了他的结果,在这篇文章中,他假设电磁能量只能由一个源,如太阳,以离散的能量包的形式释放,而不是连续的波。因为人们知道光表现出波动特性,1905 年阿尔伯特·爱因斯坦扩展了这一观点,提出普朗克的离散“包”只能以离散“波包”的形式存在。他称这种包为 Lichtquant 或“光量子”后来,在 1928 年,阿瑟·康普顿使用了“光子”一词,这个词来源于希腊语 Phos,意为光。

普朗克还假设波包的来源是热激发的电荷,每个电荷以特定的频率发射一包电磁能量,即光子。以相同频率发射光子的电荷越多,以该频率发射的光的功率就越大。他进一步提出理论,认为波包的能量只能出现在特定的固定能级或状态。

回到 1900 年,普朗克写在明信片背面的方程,它预言了一个黑体发出的光 S(λ)的功率谱是

$$ S\left(\lambda \right)=\frac{2\pi {c}²h}{\lambda⁵}\frac{\varepsilon }{e^{\frac{hc}{\lambda kT}}-1}\kern1em J/s/{m}³=W/{m}³ $$

(10-7)其中 c 是光速(m/s),h 是普朗克常数(J s),λ是波长(m),K 是玻尔兹曼常数(J/K),T 是温度(K),ε是辐射体表面的发射率。ϵ本质上是对表面辐射能力有效性的一种度量。它的范围可以从 0 到 1。正如你可能想象的那样,这个等式的发展背后有很多想法,我在这里就不赘述了。

在过去的 100 多年里,这种关系经受住了时间的考验,并给出了非常准确的结果。如图 10-8 所示,通常被称为普朗克黑体辐射公式。它同样适用于所有辐射体和太阳。尽管太阳看起来肯定不像一个黑体,但就其辐射特性而言,它的行为就像一个黑体——一个非常热的黑体。作为一个类比,你可能会认为太阳是一个非常热(大约 5800 K)的黑色火炉,发出非常明亮的光。

图 10-8 显示了由方程 10-7 预测的太阳输出光谱(红色曲线)。这被称为功率谱密度,或简称为功率谱。曲线上的每个点给出了相应波长λ下的功率密度 S(λ)。稍后将解释图中所示的绿色带。

10.2.3 太阳总功率输出

图 10-8 中显示的量 S(λ)是功率密度。什么是功率密度,它与简单功率有何不同?注意方程 10-7 中 S(λ)的单位是每立方体积的功率。这些是密度的单位。你可能会认为这个“密度”类似于质量密度,单位是每立方体积的质量。在 S(λ)的情况下,你处理的是功率密度。

等式 10-7 中使功率谱类似于太阳输出而不是任何其他黑体输出的特征是温度 T。对于太阳,T 约为 5800 K。要获得太阳在λ 1 到λ 2 的带宽上发射的功率 P(λ),需要对该频带上的 S(λ)求和。在微积分中,这相当于求 S 对λ的积分,相当于求 S(λ)曲线下这些极限之间的面积。

$$ {P}_{\lambda_1\to {\lambda}_2}={\int}_{\lambda_1}^{\lambda_2}S\left(\lambda \right) d\lambda \kern2.1em J/s/{m}²=W/{m}² $$

(10-8)

用方程式 10-7 这就变成了

$$ {P}_{\lambda_1\to {\lambda}_2}=2\pi {c}²h\kern0.34em {\int}_{\lambda_1}{\lambda_2}\frac{\lambda{-5}\varepsilon }{e^{\frac{hc}{\lambda kT}}-1} d\lambda \kern0.72em J/s/{m}²=W/{m}² $$

(10-9)

等式 10-9 给出了太阳在λ 1 到λ 2 的带宽上发射的功率。它等于 S(λ)乘以无穷小带宽 dλ的积分。换句话说,如果你沿着 S(λ)曲线选择一个点,如图 10-9 所示,将它乘以 dλ,然后将从λ 1 到λ 2 的所有值相加,你将得到在λ 1 到λ 2 波段的波长所发射的总电磁功率。这是从λ 1 到λ 2 的 S(λ)曲线下的区域。

为了得到整个太阳光谱产生的能量,你从波长λ=0 延伸到λ=∞,对方程 10-9 进行积分。对于那些喜欢对方程 10-9 进行数学积分的人,我在附录 b 中展示了如何进行积分。积分就是找到 S(λ)曲线下的面积。你可以用数字来避免计算。为此,将无限小的波段 dλ替换为有限宽度∏λ的小波段,并将积分替换为求和,如在

$$ P\left(\lambda \right)=2\pi {c}²h\sum \limits_{i=1}^{i=N}\frac{1}{\lambda_i⁵}\frac{\varepsilon }{e^{\frac{hc}{\lambda_i kT}}-1}\varDelta \lambda \kern0.84em J/s/{m}²=W/{m}² $$

(10-10)中,其中 I 是指以λ i 为中心的第 i 波段,N 是λ i 和λ N 之间宽度∏λ的波段数。图 10-9 显示了宽度为∏λ的典型带。为了说明的目的,所示的带的宽度被夸大了。实际上,它应该显得更窄。

A456962_1_En_10_Fig9_HTML.jpg

图 10-9

Numerical integration of power S(λ)dλ emitted by spectrum bandi across a .01 μm bandwidth at λi=1.5 (produced by Listing 10-2)

方程 10-10 是方程 10-9 的*似,因为它假设 S(λ)的值在每个波段∈λ的宽度上是常数。然而,如果选择的∏λ足够小,曲线 S(λI-∏λ/2)→S(λI+∏λ/2)可以用带宽∏λ上的常数值 S(λ i )来*似,在这种情况下,结果可能相当精确。利用这个简单的积分方案,频带中的功率等于频带的矩形面积。虽然您可以使用更复杂的集成方案,但是这个方案很简单,易于编程,并且足以满足您的需求。

让我们计算小波段∏λ上的波长发射的功率 P(λ)。图 10-9 是图 10-8 中以λ= 1.5 μm 为中心的谱带的放大图。这可能被认为是等式 10-10 中的典型带 i 。清单 10-2 评估了该带宽内波长产生的功率。曲线 S(λ)已根据方程式 10-7 生成。根据这个简化的积分方案,这个频带产生的功率,也就是它的矩形面积,由

$$ P\left(\lambda \right)=S\left(\lambda \right)\varDelta \lambda \kern3em J/s/{m}²=W/{m}² $$

(10-11)给出

在绘制图 10-9 的清单 10-2 中,条带的面积是根据方程 10-11 计算的。∏λ的大小是任意的。在程序中,它是参数 dla,设置为 0.01 X106米或 0.01 μm。无论∏λ是大还是小,它发出的功率都是该带宽内波长辐射的功率。带宽越宽,产生的功率越大,带宽越窄,产生的功率越小。稍后,当您对整个 S(λ)曲线下的面积进行数值积分以获得太阳在其整个光谱上辐射的总功率时,选择较小的∏λ值将会导致更精确的结果。

在图 10-9 中,波段显示为λ=1.5 um。根据程序计算,对应的 S 值为 1.164x10 7 MW/m 3 。带宽为 0.01 微米,相当于 1.0x 108米,这个频段产生的功率为(1.164 X107)x(1 X108)= . 1164 MW/m2,大约相当于一个小型发电厂的发电量。

注意,S(λ)的单位将与输入参数的单位一致:光速、普朗克常数、玻尔兹曼常数和波长λ。这些参数的单位应该彼此一致。为了避免混淆,在这项工作中,当计算方程 10-7 时,你将保持所有这些量在米的空间维度中。S(λ)将有单位(J/s)/m 3 ,与 W/m 3 相同。如果需要另一种功率尺寸的输出,如 kW 或 MW,可以在计算 S(λ)后进行转换,即将 S(λ)乘以 103得到千瓦,或将 S(λ)乘以 106得到兆瓦。计算某个波段上发射的功率时,该波段的宽度∈λ也应该以米为单位。例如,1.5μm 应指定为 1.5x 10-6m。将λ米乘以 10 +6 可以将米转换回微米微米,以便以后显示或用于其他目的。这在列表 10-2 中显示,在直线 lag=la*10**6 中绘制 s 曲线。

图中显示 S i 的值为 1.164x10 7 MW/m 3 。S(λ)轴表示 11.64 MW/m3X106的值,这表示 11.64 的值已经乘以 106用于显示。这将使其实际值为 11.64x10 +6 ,等于程序计算的值。这在图上显示为 1.164 X10+7MW/m3

在创建了图 10-9 的列表 10-2 中,截面图 S 曲线求解方程 10-7 得到波长 la 的值,该值以增量 dla 从 la=lamin 到 lamax。代码中的注释描述了 s 单位的演变。如公式 10-7 所示,当参数如“建立参数”一节所示时,s 的单位以焦耳/秒/立方米开始(记住 s(λ)是密度)。因为 1 焦耳每秒定义了瓦特,所以单位是瓦特每立方米。这些被转换成兆瓦每立方米,然后换算成以单位(MW/m3)X106表示的垂直轴,作为变量 sg。106因子表示实际值已经乘以该数值。接下来,绘制绿色带,并显示温度和发射率的值。

使用公式 10-7 计算λ=1.5 时的 S(λ)值,转换为 MW/m 3 ,然后乘以带宽 dl = . 01x 106,得到 pl MW/m 2 ,即该带宽内的功率。程序的其余部分显示数据并清理地块。

"""
BANDINTEGRAL
"""

import numpy as np
import matplotlib.pyplot as plt

#---------------------------------------------------------- set up axes
ymax=20
plt.axis([1.,2.,0,ymax])
plt.xlabel('Wavelength $\lambda$ ($\mu$m)')
plt.ylabel('S($\lambda$) (MW/m$^{3}$) x 10$^{-6}$')
plt.grid(True)
plt.title('Max Planck's Solar Spectrum - Band Integral')

#------------------------------------------------ establish parameters
c=2.9979*(10.**8)           # speed of light in a vacuum m/s
h=6.63*(10.**-34)           # Planck's Constant J.s
kb=1.38*(10**-23)           # Boltzmann's Constant J/K

t=5800\.                     # temperature K
e=1.0                       # emissivity

lamin=.01*10**-6            # starting wavelength m
lamax=2.*10**-6             # ending wavelength m
dla=.01*10**-6              # incremental wavelength m

#———————————————————————————————————————————— plot s curve
for la in np.arange(lamin,lamax,dla):
    a1=2.*np.pi*c*c*h/(la**5.)
    a2=h*c/(la*kb*t)
    sl=e*a1/(np.exp(a2)-1.)                 # J/s/m³ = W/m³
    sl=sl*10**-6                            # MW/m³
    slg=sl*10**-6                           # scale plot at 10^-6 scale
    lag=la*10**6                            # scale to plot at 10⁶ scale
    plt.scatter(lag,slg,s=1,color='r')
#——————————————————————————————————————————————— plot band
plt.plot([1.495,1.495],[0.,11.64],color='g')
plt.plot([1.4975,1.4975],[0.,11.64],color='g')
plt.plot([1.5,1.5],[0,11.64],color='g')
plt.plot([1.5025,1.5025],[0.,11.64],color='g')
plt.plot([1.5005,1.505],[0.,11.64],color='g')

#——————————————————————————————— plot temperature and emissivity
d=str(t)
plt.text(1.6,15,'T=')
plt.text(1.65,15,d)
plt.text(1.6,14,'e=')
d=str(e)
plt.text(1.65,14,d)

#———————————————————— calculate s and band power pl at lambda=1.5
la=1.5*10**-6
a1=2.*np.pi*c*c*h/(la**5.)
a2=h*c/(la*kb*t)
sl=e*a1/(np.exp(a2)-1.)  # J/s/m³ = W/m³
sl=sl*10**-6             # MW/m³
dl=.01*10**-6            # bandwidth m
pl=sl*dl

#———————————————————————————————— plot results and labels
plt.plot([1.53,1.59],[11.6,11.6],'k')
plt.text(1.6,11.5,'si=')
d='%7.3e'%(sl)
plt.text(1.65,11.5,d)
plt.text(1.83,11.5,'MW/m³')

plt.arrow(1.4,5,.085,0,head_width=.5,head_length=.01,linewidth=.2)
plt.arrow(1.6,5,-.085,0,head_width=.5,head_length=.01,linewidth=.2)

plt.text(1.15,5,'$\Delta \lambda$=')

dle='%7.3e'% (dl)
dls=str(dle)
plt.text(1.18,5,dls)
plt.text(1.35,5,'m')')

plt.text(1.145,4,'=')
dl=dl*10**6
dle='%7.3e'%(dl)
dls=str(dle)
plt.text(1.18,4,dls)
plt.text(1.35,4,'um')

plt.text(1.35,16.5,'s($\lambda$)')
plt.text(1.52,2.5,'power$_{i}$=')
pl='%7.3e'%(pl)
pl=str(pl)
plt.text(1.65,2.5,pl)
plt.text(1.823,2.5,'MW/m²')
plt.text(1.45,-1.1,'$\lambda_{i}$=1.5')

plt.show()

Listing 10-2Program BANDINTEGRAL

接下来我们来看一下如图 10-8 所示的马克斯普朗克的整个黑体谱。因为使用的温度是太阳的温度,大约 5800k,所以它被称为“马普太阳光谱”

生成此图的程序(列表 10-3 )遵循前面程序(列表 10-2 )中的逻辑,但这里您对从λ= . 01 X106到 10 . X106米(. 01μm 到 10.μm)的各个频带功率求和,以获得整个(几乎)S(λ)曲线下的面积。你在清单 10-2 中看到的波段显示为λ=1.5 um。

这里使用的过程是简单地沿着波长前进,计算每个波长的 S(λ)值,乘以∏λ以获得该波段内的功率,然后根据等式 10-10 对每个波段产生的功率求和。这将给出所有波长发射的总功率。

为了更精确地测量 S(λ)曲线下的总功率,您将积分范围扩展至 10x 106米。这将是每*方米太阳表面发出的总光谱能量。然后你把它乘以太阳的球面面积,就得到太阳发出的总功率,这就是所谓的太阳光度。在图 10-8 中,它被称为“太阳能总输出”如图所示,程序计算出的值为 3.816x10 26 瓦。这与公布的数值非常一致。

许多研究人员使用 e=1.0 作为发射率,这是一种理想化的假设,即太阳是一个完美的辐射体(你可以假设它不是)。这里你使用 e = .984 的发射率。当你使用普朗克光谱计算太阳常数时,太阳常数已经由卫星测量(下一节),你必须在你的计算中降低太阳的温度,或者将其发射率降低到小于 1.0,以便使结果与测量值一致。如果你选择保持 5800 K 的太阳温度,那么你必须将发射率降低到 0.984 才能获得一致。另一个选择,你会看到,是保持 e=1.0,把太阳的温度降到 5777 K。

"""
PLANCKSSOLARSPECTRUM
"""

import numpy as np
import matplotlib.pyplot as plt
#—————————————————————————————————————————————————— set up axes
ymax=100
plt.axis([0,3,0,ymax])
plt.xlabel('Wavelength _ (_m)')
plt.ylabel('S(λ) (MW/m^{3}) x 10^-6')

plt.grid(True)
plt.title('Max Planck's Solar Spectrum')
#———————————————————————————————————————— establish parameters
c=2.9979*(10.**8)           # speed of light in a vacuum m/s
h=6.63*(10.**-34)           # Planck's Constant J.s
kb=1.38*(10**-23)           # Boltzmann's Constant J/K
e=.984                      # emissivity
t=5800\.                     # K
lamin=.01*10**-6            # m
lamax=10.*10**-6            # m
dla=.01*10**-6              # m
st=0\.                       # set area under s curve to zero
#—————————————————————————————— plot s curve and calculate area
for la in np.arange(lamin,lamax,dla):
    a1=2.*np.pi*c*c*h/(la**5.)
    a2=h*c/(la*kb*t)
    sl=e*a1/(np.exp(a2)-1.)                # W/m³
    sl=sl*10**-6                           # MW/m³
    bandarea=sl*dls                        # band area MW/m²
    st=st+bandarea                         # sum band areas MW/m²
    slg=sl*10**-6                          # scale to plot
    lag=la*10**6                           # scale to plot
    plt.scatter(lag,slg,s=1,color='r')
#—————————————————————————————————— multiply the Sun's surface area
ds=1.39*10**9                # Sun's diameter m
spas=np.pi*ds**2\.            # Sun's spherical area m²
to=spas*st                   # Sun's total output MW
to=to*10**6                  # Sun's total output W
#———————————————————————————————————————————————— plot results
plt.text(.8,58.,'5800')
plt.text(1.05,58, '°K')
plt.plot([.39,.39],[-0.,100.],'b–')
plt.plot([.7,.7],[-0.,100.],'b–')

plt.text(.3,-10,'.390')
plt.text(.6,-10,'.700')
plt.text(.15,90.,'UV')
plt.text(.8,90.,'long wave infrared')
plt.arrow(1.75,91.,.8,0.,head_width=1.,head_length=.1,color='r')
plt.text(1.2,40.,'total solar output =')
so='dd=str(so)
plt.text(2.1,40,dd)
plt.text(2.7,40,'W')
plt.text(1.2,34,'emissivity =')
e=str(e)
plt.text(1.8,34,e)
plt.text(.5,75.,'v')
plt.text(.53,70.,'i')
plt.text(.5,65.,'s')
plt.text(.53,60.,'i')
plt.text(.5,55.,'b')
plt.text(.53,50.,'l')
plt.text(.5,45.,'e')
plt.plot([1.49,1.49],[0.,11.61],color='g')
plt.plot([1.5,1.5],[0.,11.61],color='g')
plt.plot([1.51,1.51],[0.,11.61],color='g')
#—————————————————— calculate s at la=1.5x10^-6 m and band power pband
laband=1.5*10**-6
a1=2.*np.pi*c*c*h/(laband**5.)
a2=h*c/(laband*kb*t)
sband=a1/(np.exp(a2)-1.)
sband=sband*10**-12
pband=sband*dla # MW/sq meter
pband=pband*10**6 # W/sq meter
#———————————————————————————————————————————————— plot band
plt.plot([1.55,1.7],[12.5,15.],color='k')
plt.text(1.72,14.,' p=')
pband='pband=str(pband)
plt.text(1.9,14,pband)
plt.text(2.4,14,'MW/m²')

plt.arrow(1.35,5,.1,0,head_width=1, head_length=.05, ec="k", fc="k")
plt.arrow(1.65,5,-.1,0,head_width=1, head_length=.05, ec="k", fc="k")
plt.text(.82,4.9,'Δλ = :01μm' )

plt.show()

Listing 10-3Program PLANCKSSOLARSPECTRUM

10.3 地球辐照度

图 10-10 显示了到达地球的太阳辐射光谱。图 10-11 显示地球围绕太阳旋转。这是用来计算地球截获的太阳总发电量的模型,即太阳常数。两个球体之间的距离*均为 1 天文单位,约为 93,000,000 英里。它在一个轨道上是变化的。标有 p 的圆盘的面积等于地球的横截面积。A p 截取的太阳能负责加热地球。

A456962_1_En_10_Fig10_HTML.jpg

图 10-10

Svpectrum of solar power reaching Earth, the Earth’s solar irradiance, produced by Listing 10-3, which has been modified by inclusion of the inverse square law

注意这些值比图 10-8 中的值低多少。这是因为到达地球并被 A p 拦截的太阳输出的功率强度根据

$$ {p}_p={p}_s{\left(\frac{r_s}{r_{es}}\right)}² $$

(10-12)的*方反比定律随着从太阳到地球的距离而减小,其中 p s 是太阳表面的功率强度,p p 是被 A p 拦截的强度,r s 是太阳的半径,r A p 截获的总功率因此为

$ {P}_p={A}_p{p}_p $

(10-13)

$ {P}_p={A}_p{p}_s{\left(\frac{r_s}{r_{es}}\right)}² $

(10-14)

当等式 10-13 包含在列表 10-3 中时,频谱减少到图 10-10 。再次注意,由于*方反比定律,这些值比图 10-8 中的值低了多少。

P p ,即到达地球大气层顶部的太阳能功率,称为太阳常数。卫星测得的数值约为 1361 瓦/米 2 。其中大约 30%是通过反照率效应从地球表面和大气反射的,如雪、冰、云、水等。其余的被地球吸收了。其中大部分被重新辐射回太空,让地球达到热*衡。地球也是一个热(暖)体,它向太空展示自己的热辐射。但是一些应该再辐射的物质被包括二氧化碳在内的温室气体阻挡,导致全球变暖。众所周知,所有这些都是气候研究人员正在积极研究的。

10.3.1 地球太阳模型

图 10-11 显示地球绕太阳运行,清单 10-4 包含代码。

A456962_1_En_10_Fig11_HTML.jpg

图 10-11

The Earth-Sun Model produced by Listing 10-4

"""
EARTHSUN
"""

import matplotlib.pyplot as plt
import numpy as np
from math import radians, sin, cos, sqrt

plt.axis([-100,150,-100,150])

plt.grid(False)
plt.axis('off')
sfx=2.5/3.8

#———————————————————————background
for x in range(-100,150,2):
    for y in range(-100,150,2):
        plt.scatter(x,y,s=40,color='midnightblue')

phimin=0.
phimax=2.*np.pi
dphi=phimax/100.

rs=40.
re=20.

ys=15.
ye=2.

xos=50.
yos=0.
zos=0.

#———————————————————Sun's core
plt.scatter(xos,yos,s=4300,color='yellow')

#———————————————————Sun horizontals
rx=radians(20)

for ys in np.arange(-rs,rs,5):
    for phi in np.arange(phimin,phimax,dphi):
        rp=np.sqrt(rs*rs-ys*ys)
        xp=rp*np.sin(phi)
        yp=ys
        zp=rp*np.cos(phi)
        px=xos +sfx*xp*1\. +yp*0\. +zp*0.
        py=yos +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
        pz=zos +xp*0\. +yp*np.sin(rx) +zp*np.cos(rx)
        if pz > 0 :
            plt.scatter(px,py,s=1,color='red')

#————————————————————Sun verticals
alphamin=0.
alphamax=2.*np.pi
dalpha=alphamax/30.

for alpha in np.arange(alphamin,alphamax,dalpha):
    for phi in np.arange(phimin,phimax,dphi):
        xp=rs*np.sin(phi)*np.sin(alpha)
        yp=rs*np.cos(phi)
        zp=rs*np.sin(phi)*np.cos(alpha)
        px=xos +sfx*(xp*1\. +yp*0\. +zp*0.)
        py=yos +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
        pz=zos +xp*0\. +yp*(np.sin(rx)) +zp*np.cos(rx)
        if pz > 0 :
            plt.scatter(px,py,s=1,color='red')

#——————————————————————Earth's clouds
xoe=-50.
yoe=20.
zoe=-10.

plt.scatter(xoe,yoe,s=800,color='white')

#———————————————————— Earth horizontals
rx=20.*np.pi/180.
dphi=phimax/100.

for ys in np.arange(-re,re,2):
    for phi in np.arange(phimin,phimax,dphi):
        rp=np.sqrt(re*re-ys*ys)
        xp=rp*np.sin(phi)
        yp=ys
        zp=rp*np.cos(phi)
        px=xoe +sfx*(+xp*1\. +yp*0\. +zp*0.)
        py=yoe +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
        pz=zoe +xp*0\. +yp*(np.sin(rx)) +zp*np.cos(rx)
        if pz > 0 :
            plt.scatter(px,py,s=.1,color='#add8e6')

#—————————————————————Earth verticals
alphamin=0.
alphamax=2.*np.pi
dalpha=alphamax/30.

for alpha in np.arange(alphamin,alphamax,dalpha):
    for phi in np.arange(phimin,phimax,dphi):
        xp=re*np.sin(phi)*np.sin(alpha)
        yp=re*np.cos(phi)
        zp=re*np.sin(phi)*np.cos(alpha)
        px=xoe +sfx*(xp*1\. +yp*0\. +zp*0.)
        py=yoe +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
        pz=zoe +xp*0\. +yp*(np.sin(rx)) +zp*np.cos(rx)
        if pz > 0 :
            plt.scatter(px,py,s=.1,color='#add8e6')

plt.arrow(xos-rs*sfx-3,yos+2,xoe-(xos-rs*sfx)+re+3,yoe-yos-6.2,color='r',
                              head_length=4.,head_width=3.)
plt.text(-14,16,'1 AU',color='white')
plt.text(80,-29,'Sun',color='white')
plt.text(-84,10,'Earth',color='white')

#———————————————————————front orbit
deltamin=0.*np.pi/180.
deltamax=195.*np.pi/180.
ddelta=deltamax/60.

for delta in np.arange(deltamin,deltamax,ddelta):
    r=108./sfx
    xp=r*np.cos(delta)
    yp=0.
    zp=r*np.sin(delta)
    px=xos +sfx*(xp*1\. +yp*0\. +zp*0.)
    py=yos +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
    pz=zos +xp*0\. +yp*(np.sin(rx)) +zp*np.cos(rx)
    plt.scatter(px,py,s=1,color='white')

#———————————————————————back orbit
deltamin=220.*np.pi/180.
deltamax=360.*np.pi/180.

for delta in np.arange(deltamin,deltamax,ddelta):
    r=108./sfx
    xp=r*np.cos(delta)
    yp=0.
    zp=r*np.sin(delta)
    px=xos +sfx*xp*1\. +yp*0\. +zp*0.
    py=yos +xp*0\. +yp*np.cos(rx) -zp*np.sin(rx)
    pz=zos +xp*0\. +yp*(np.sin(rx)) +zp*np.cos(rx)
    plt.scatter(px,py,s=1,color='white')

#———————————————————Ap disc
xoc=xoe+re*sfx
yoc=yoe-2.5
zoc=zoe
rc=.83*re
phi1=0
phi2=2*np.pi
dphi=(phi2-phi1)/200
ry=-25*np.pi/180

for phi in np.arange(phi1,phi2,dphi):
    xc=xoc
    yc=rc*np.sin(phi)
    zc=rc*np.cos(phi)
    px=xoc+zc*np.sin(ry)
    py=yoc+yc
    pz=zoc+zc*np.cos(ry)
    plt.scatter(px,py,s=.03 ,color='white')

plt.scatter(xoe+re*sfx,yoe-2,s=6,color='white')

plt.arrow(-20,60,(xoe+re*sfx)+24,(yoe+re/2)-60-2,color='white',
                             linewidth=.5,head_width=2.,head_length=3)
plt.text(-18,60,'A
p
',color='white')

plt.show()

Listing 10-4Program EARTHSUN

10.4 总结

在本章中,你已经看到了 Python 图形编程的一些典型应用。在第一部分中,你看到了创建土星的图像是多么的容易。这颗行星的身体利用了前几章开发的球体和阴影算法;这些带是通过在 x,z *面上构建同心环而形成的。阴影算法需要一点原始几何。然后整个东西在空间绕 x,y,z 轴旋转。在关于太阳辐射的章节中,您学习了太阳辐射的物理学,尤其是马克斯·普朗克的黑体辐射公式,以及 Python 构建技术插图的能力。特别值得注意的是用于绘制的缩放变量的技术。然后你学习了如何构建图像,如图 10-8 所示,显示了用于理解地球太阳辐照度的模型。

十一、从哪里获得 Python

互联网上有几个地方可以下载各种版本的 Python。我将 Anaconda 与 Spyder 2 和 Python 3.5 一起使用。这可以从 https://docs.continuum.io/anaconda/install 的 Continuum Analytics 下载。

很洒脱;请按照说明操作。虽然我使用 Python 3.5,但我建议使用最新版本。

桌面上会出现一个图标。如果没有,请查看您的已安装程序列表,并将其拖到桌面上。双击它以运行环境。您将在左侧窗格中输入 Python 脚本。输入程序代码后,单击顶部的运行按钮或按键盘上的 F5 键。您可能会被告知打开一个新的控制台。单击顶部的控制台按钮,然后选择“打开 IPython 控制台”选项。试着再运行一次。结果应该出现在右下方的窗格中。

右上方有一个显示变量状态的窗格。我从来不用它;事实上,我关闭它是为了给输出留出更多空间。如果我想知道一个特定的变量在做什么,我通常会在程序中放一个 print 语句。变量的历史记录将出现在输出窗格中。

如果您发现您的程序正在做意想不到的事情,打开一个新的控制台并重新运行程序有时会有所帮助。

十二、普朗克辐射定律和斯特凡-玻尔兹曼方程

在第十章中,向你介绍了马普著名的黑体辐射方程:

$$ S\left(\uplambda \right)=\frac{2\pi {c}²h}{\uplambda⁵}\frac{\varepsilon }{e^{\frac{hc}{\lambda kT}}-1}\kern0.5em J/s/{m}³=W/{m}³ $$

(B-1)

表面在带宽λ 1 → λ 2 上发射的功率为

$$ {P}_{\uplambda_1\to {\uplambda}_2}=\underset{\uplambda_1}{\overset{\uplambda_2}{\int }}S\left(\uplambda \right)d\uplambda \kern1em J/s/{m}²=W/{m}² $$

(B-2)

用方程 B-1,这就变成了

$$ {P}_{\uplambda_1\to {\uplambda}_2}=2\pi {c}²h{\int}_{\uplambda_1}{\uplambda_2}\frac{\uplambda{-5}\varepsilon }{e^{\frac{hc}{\uplambda kT}}-1}d\uplambda \kern1.2em J/s/{m}²=W/{m}² $$

(B-3)

在第十章中,你对方程 B-3 进行了数值积分。在这里,您将对其进行数学积分,并表明它可用于推导黑体辐射的 Stefan-Boltzmann 定律

$$ p=\frac{\varepsilon 2{\pi}⁵{k}_B⁴}{15{h}³{c}²}{T}⁴ $$

(B-4)其中 t 是表面的绝对温度,p 是单位面积辐射的功率,k B 是 Boltzmann 常数,h 是 Planck 常数,c 是光速,ϵ是表面的发射率。从区域 A 的表面辐射的功率是

$$ P= pA=\kern0.6em \varepsilon \kern0.1em A\sigma {T}⁴ $$

(B-5),其中

$$ \sigma =\frac{2{\pi}⁵{k}_B⁴}{15{h}³{c}²}=5.6696x{10}^{-8}\kern1em W/{m}²/{K}⁴ $$

(B-6) σ被称为 Stefan-Boltzmann 常数。等式 B-4 将表面辐射的功率强度与其温度 t 的四次方相关联。该等式通常用于科学和工程中。

为了进行导致方程 B-4 的积分,你从普朗克的辐射方程开始(也在上面的 12-1 中示出):

$$ S\left(\uplambda \right)=\frac{2\pi h{c}²}{\uplambda⁵}\frac{\varepsilon }{e^{\frac{hc}{\uplambda {\kappa}_BT}}-1} $$

(B-7)你想要从λ=0 到λ=∞对其积分以获得每单位的总功率

所有波长辐射的区域 p。让 C 1 =ϵ2πhc 2

$$ {C}_2=\frac{k_BT}{h} $$

,你得到

$$ p={C}_1\underset{0}{\overset{\infty }{\int }}\frac{\uplambda{-5}d\uplambda}{e{\frac{1}{C_2\uplambda}}-1} $$

(B-8)

如果你做了下面的替换

$$ x={C}_2\uplambda, \kern1.12em dx={C}_2d\uplambda $$

(B-9)稍微忙乱一下你就有了

$$ p={C}_1{C}_2⁴\underset{0}{\overset{\infty }{\int }}\frac{dx}{x⁵\left({e}^{\frac{1}{x}}-1\right)} $$

(B-10)

利用众所周知的:)关系

$$ \underset{0}{\overset{\infty }{\int }}\frac{dx}{x⁵\left({e}^{\frac{1}{x}}-1\right)}=\frac{\pi⁴}{15} $$

(B-11),将 C 1 和 C 2 代入方程 B-10,得到

$$ p=\frac{\varepsilon 2{\pi}⁵{k}_B⁴}{15{h}³{c}²}{T}⁴ $$

(B-12),与上面的方程 B-4 相同。

posted @ 2024-08-09 17:43  绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报