7. 图片与 for-each 循环¶
在本章中,你将学习计算机图像表示的基础知识,以及如何完成简单的图像操作。
7.1. 首先,基础知识¶
这个简短视频解释了像素、RGB 颜色以及如何通过更改像素来变换图像:
7.1.1. 像素类¶
数字图像由一个二维的点网格组成。这些点称为像素(pixel),是"picture element"(图像元素)的缩写。放大后可以看到单个像素。组成图像的像素数量决定了图像的质量(分辨率)。像素越多(例如 640 x 480,以水平 x 垂直数字,即宽度 x 高度表示),分辨率越高。
在我们的程序中,Pixel 是一个表示单个像素(或"图像元素")的类,它表示构成图像的一个微小点或方块。该类提供的方法允许你检索其颜色分量或修改其颜色。你将主要使用此类来读取或更改图像中单个像素的颜色。一个 <code>Pixel</code> 对象提供了读取其颜色 4 个关键分量的取值方法:getRed()、getGreen()、getBlue() 和 getAlpha()。它还提供了相应的设值方法来更改每个颜色值:setRed(int)、setGreen(int)、setBlue(int) 和 setAlpha(int)。所有这些方法处理 0 到 255 之间的整数值,正如上面视频中解释的那样。
7.1.2. RGB 颜色¶
每个像素值表示为三个 int 分量——红色、绿色和蓝色,每个分量的值范围为 0 到 255。我们经常以这种方式将颜色表示为 3 个 int 的集合。例如,RGB 值 (0, 0, 0) 表示黑色(所有三种颜色的强度为零),RGB 值 (255, 255, 255) 表示白色(所有三种颜色的最大强度)。更多示例,请查看 RGB 颜色表。
使用三个整数值,我们可以将任何给定颜色表示为三维颜色"立方体"上的一个点:
这种立方体被称为三维 颜色空间。从上面的图像可以看出,从 (0,0,0) 的黑色到 (255, 255, 255) 的白色有一条直线对角线,表示不同深浅的灰色。
在下图中,我们可以看到左侧的一系列颜色及其右侧相同位置的 RGB 值。例如,你可以在左侧网格的左上角看到一个白色方块,在右侧网格的左上角看到白色的 RGB 值:
要更改图像,我们只需更改部分或全部像素的 RGB 值。这项任务可以使用 Adobe Photoshop 等图像处理程序来完成。但使用我们在本课程中编写的简单程序来变换图像也是可能的。
Pixel 类提供的处理 RGB 颜色的基本方法如下:
| Pixel Method | Description |
|---|---|
int getRed() |
Get the red intensity (an integer from 0-255) |
int getGreen() |
Get the green intensity (an integer from 0-255) |
int getBlue() |
Get the blue intensity (an integer from 0-255) |
void setRed(int) |
Set the red intensity to a value from 0-255 |
void setGreen(int) |
Set the green intensity to a value from 0-255 |
void setBlue(int) |
Set the blue intensity to a value from 0-255 |
int getX() |
Get the x coordinate where this pixel is located in the image |
int getY() |
Get the y coordinate where this pixel is located in the image |
void setColor(int red, int green, int blue) |
Set all three color values at once |
7.1.3. 想象像素的工作原理¶
你可以想象 Pixel 类在内部可能如何工作,即使像素有多种不同的表示方式。作为一个例子,你可以想象它是这样定义的:
public class Pixel
{
private int rValue;
private int gValue;
private int bValue;
private int xCoord;
private int yCoord;
public Pixel(int x, int y)
{
this.xCoord = x;
this.yCoord = y;
this.rValue = 0;
this.gValue = 0;
this.bValue = 0;
}
public int getRed()
{
return this.rValue;
}
public void setRed(int rValue)
{
this.rValue = rValue;
}
// getter and setter methods for the other colors would go here
}
值得注意的是,由于我们希望任何给定的 Pixel 对象在屏幕上表示一个像素,我们可能不会为 xCoord 和 yCoord 创建设值方法,因为我们不希望其他代码能够更改我们正在讨论的像素。虽然实际的 Pixel 类比这稍微复杂一些,但这里展示的基础知识有助于人们推理 Pixel 类提供的功能以及取值方法和设值方法的行为方式。
7.1.4. Picture 类¶
Picture 是一个表示可以显示在屏幕上的单个图像的类。我们将使用此类来表示你将要操作的图像。有多种方式可以访问 Picture 中的像素。首先,Picture 类提供了一个访问图像中任何像素的方法:
Picture image = new Picture("image.png");
Pixel corner = image.getPixel(0, 0);
corner.setRed(0);
corner.setGreen(0);
corner.setBlue(255);
此代码段将图像左上角的像素变为蓝色。getPixel() 方法允许我们访问图像中的任何像素,Picture 还提供了 getWidth() 和 getHeight() 访问器来确定图像的大小。但是,如果我们想对整个图像进行更改,最好使用循环遍历所有像素。
7.2. for-each 循环¶
到目前为止,我们已经在程序中创建了 while 循环。当我们知道需要达到什么条件才能完成(或停止)时,即使我们不一定知道需要多长时间才能到达那里,这类循环都非常有用。例如,while 循环非常适合当我们希望 Jeroo 只要看到花就一直跳跃,不管它将看到多少朵花的情况。
然而,这些循环也有缺点。编写条件时可能出错,导致循环不会在正确的位置停止,或者可能根本不停止(!)。此外,循环内部也可能出错,使得执行的步骤无法让我们更接近目标。
然而,循环的另一个常见用途是对组或集合中的每个项目执行某些操作。在这种情况下,我们希望循环为组中的每个项目重复一次,并在处理完所有项目后停止。在这种情况下,Java 提供了一种不同类型的循环,它能替我们完成更多工作,并帮助确保循环正确运行。这种循环称为 for-each 循环,当你希望对组中的所有项目重复某个操作时,它是最佳选择。这是因为 for-each 循环不会面临编写循环条件或逐步接近目标时出错的风险——循环自身提供了所有决策。它总是会查看集合中的每个项目,然后移至下一个,然后完成。你自己不编写任何条件,因此不可能在这方面出错。
Picture 实际上是一大组 Pixel。Picture 类提供了一个名为 getPixels() 的方法,允许你遍历图像中的所有像素。我们可以在 for-each 循环中这样使用它:
Picture image = new Picture("image.png");
for (Pixel pix : image.getPixels())
{
pix.setRed(255);
}
用英语来读,上面的代码是"for each pixel in the image, set its red intensity to 255."(对于图像中的每个像素,将其红色强度设置为 255。)
7.2.1. for-each 循环语法¶
编写 for-each 循环时使用的模板如下:
for (<datatype> <variable-name> : <collection>)
{
// body of for-each loop
}
这里,<collection> 是要遍历的对象组或集合。在上面的示例中,集合是图像中的所有像素。<datatype> 是组中单个项目的类型——在我们的示例中,就是一个 Pixel。最后,除了提供集合和单个元素的类型外,for-each 循环还需要一个新变量名,我们可以将其用作集合中单个项目的名称。在 for-each 循环体中,我们可以使用这个名称在循环重复时引用当前项目。
观察这些,你应该能看出与本课程中见过的其他控制结构的相似之处。我们有一个关键字(在这种情况下是 for)、一组围绕"控制"循环运行方式的短语的圆括号,以及一对围绕构成循环体的语句组的花括号。
在圆括号内,<datatype> 和 <variable-name> 构成一个局部变量声明,类似于你自己编写过的其他声明。
for (Pixel pix : image.getPixels())
{
pix.setRed(255);
}
这里,我们声明了一个名为 pixel 的变量,类型为 Pixel。此变量仅在 for-each 循环的花括号内——即循环体——可用。在那个右花括号之后,你不能使用或引用 pixel。但在循环体内,你可以使用该变量名来引用当前元素并对其执行所需的任何操作。
然而,我们不是用等号和初始值完成变量声明,而是使用冒号(:)后跟项目集合。循环将为该集合中的所有项目重复,每次循环重复时,循环中的局部变量将依次获取下一个项目的值。
第一次进入此循环时,pixel 将表示索引 (0, 0) 处的 Pixel 对象。一旦循环体执行一次,pixel 将变为表示索引 (1, 0) 处的 Pixel 对象。循环重复遍历第 0 行中横跨图片顶部的所有像素,然后移至从 (0, 1) 开始的下一行,逐行遍历直到所有像素都被处理。一旦循环体对图像中的每个像素都执行了一次,循环将终止并继续执行循环之后的代码。
7.2.2. 在循环中使用外部变量¶
for-each 循环的优势之一是它保证始终遍历集合中的所有对象,并且永远不会导致非终止循环的错误。然而,一个限制是循环中引入的变量仅引用组中的一个项目,并且仅在循环体内可访问。
但是,有时我们希望在循环重复时收集信息,以便在循环结束后使用。为此,我们需要在循环前引入一个或多个其他变量,以便在循环期间更新它们以记住我们需要的信息,并在循环完成后仍然可以访问它们。
例如,我们可能想记住图像中任何位置出现的最强红色值。我们可以通过引入一个变量来记住到目前为止看到的最大红色强度,并在每次循环重复时更新它来实现:
public int getMaxRed(Picture image)
{
int maxRed = 0; // set to minimum value to start
for (Pixel pix : image.getPixels())
{
if (pix.getRed() > maxRed)
{
// if this pixel has more red than any we've seen before,
// remember it
maxRed = pix.getRed();
}
}
return maxRed;
}
当变量 maxRed 最初被声明时,它被初始化为 0。许多程序员会称这个变量为"累加器",因为它持有我们在循环遍历所有像素时逐步构建的答案。本质上,在任何时间点 maxRed 代表到目前为止我们在循环已遍历的所有像素中看到的最大红色强度。在循环运行之前,我们将其设置为最小可能的强度,因为我们知道图像中每个像素的红色强度值都将为零或更大。
在循环内部,我们使用 if 语句将当前像素的红色值与到目前为止看到的最大值进行比较,如果更大,我们使用赋值语句用新的"最大"值更新 maxRed。每次循环时,我们只在看到更大值时更新 maxRed,否则保持不变。当循环结束时,我们将遍历图像中所有可能的像素,maxRed 将等于整个图片中任何像素的最大红色值。
7.2.3. Picture 的方法¶
Picture 类提供以下可用方法:
| Picture Method | Description |
|---|---|
new Picture(String) |
Use this constructor to create a Picture
from an image file by providing the file name in double-quotes |
new Picture(int width, int height) |
Use this constructor to create a new, blank Picture
with the specified dimensions |
int getWidth() |
Get the width of this image, in pixels |
int getHeight() |
Get the height of this image, in pixels |
Pixel getPixel(int x, int y) |
Get the pixel at the specified location |
Pixel[] getPixels() |
Get all the pixels in the image in a form suitable for use in a for-each loop |
void show() |
Show this picture on the screen |
void repaint() |
Update the on-screen image shown using show() |
void hide() |
Hide the image shown on the screen using show() |
void explore() |
Show the image using an image explorer view that allows you to inspect the color of any pixel in the image |
void reload() |
If this image was loaded from the file, throw away any changes made to the image and reload it fresh from the original file to restore it to its original appearance |
从上面的方法可以看出,你可以使用 show() 在屏幕上显示图像以便查看其外观,然后如果进行了任何更改,可以使用 repaint() 来查看更新后的图像版本。Picture 类还提供了一个 explore() 方法,它会打开一个单独的窗口显示图像,允许你点击图像中的任何位置查看其坐标和颜色值。explore() 方法在你需要检查图像细节时非常有用。
