像素、像素和更多像素
如果您刚刚开始使用处理,您可能会错误地认为,唯一提供的绘制屏幕的方法是通过函数调用。”在这些点之间画一条线,或者“用红色填充椭圆”,或者“加载这个JPG图像并将其放在这里的屏幕上”,但是不知怎么的,有些人不得不编写代码,将这些函数调用转换为设置屏幕上的各个像素,以反映所需的形状。因为我们说line(),所以不会出现一条线,因为我们沿着两点之间的线性路径对所有像素进行着色。幸运的是,我们不必每天都管理这个较低级别的像素设置。我们要感谢处理(和Java)的开发人员提供了许多绘图功能来处理这项业务。
尽管如此,我们还是时不时地想突破我们平淡无奇的图形存在,直接处理屏幕上的像素。处理通过像素数组提供此功能。
我们熟悉屏幕上每个像素在二维窗口中的X和Y位置。然而,阵列像素只有一维,以线性顺序存储颜色值。
举个简单的例子。此程序将窗口中的每个像素设置为随机灰度值。像素数组和其他数组一样,唯一的区别是我们不必声明它,因为它是一个处理内置变量。
示例:设置像素
size(200, 200);
// 在我们处理像素之前
loadPixels();
// 遍历每个像素
for (int i = 0; i < pixels.length; i++) {
// 选择一个随机数,0到255
float rand = random(255);
// 基于随机数创建灰度颜色
color c = color(rand);
// 将该位置的像素设置为随机颜色
pixels[i] = c;
}
// 当我们处理完像素
updatePixels();
首先,我们应该在上面的例子中指出一些重要的东西。无论何时访问处理窗口的像素,都必须向处理此活动发出警报。这有两个功能:
- loadPixels() 此函数在访问像素数组之前调用,表示“加载像素,我要与它们对话!”
- updatePixels() 这个函数是在像素数组显示“继续更新像素,我都完成了!”之后调用的
在上面的例子中,因为颜色是随机设置的,所以我们在访问像素时不必担心像素在屏幕上的位置,因为我们只是简单地设置所有像素,而不考虑它们的相对位置。然而,在许多图像处理应用中,像素本身的XY位置是至关重要的信息。一个简单的例子是,将像素的每一个偶数列设置为白色,将每一个奇数列设置为黑色。你怎么能用一维像素阵列做到这一点?如何知道给定像素所在的列或行?在使用像素编程时,我们需要能够将每个像素都看作生活在二维世界中,但要继续在一个世界中访问数据(因为这就是它提供给我们的方式)。我们可以通过以下公式来实现:
- 假设窗口或图像具有给定的 WIDTH 和 HEIGHT.
- 我们知道像素阵列的元素总数等于 WIDTH * HEIGHT.
- 对于窗口中的任何给定X,Y点,一维像素阵列中的位置为:LOCATION = X + Y*WIDTH
(温馨提示:首先我要知道窗口中像素的数量=宽度乘以高度,然后知道像素排序顺序,从左到右,从上到下,然后根据面积公式=宽 x 高 + 多余部分,这样就可以找到我们想查找的像素的序号)
这可能会让您想起我们的二维数组教程。实际上,我们需要使用相同的嵌套for循环技术。不同的是,虽然我们希望使用循环来考虑二维像素,但是当我们要实际访问像素时,它们位于一维数组中,我们必须应用上图中的公式。
让我们看看是怎么做的。
示例:根据像素的二维位置设置像素
size(200, 200);
loadPixels();
// 循环遍历每个像素列
for (int x = 0; x < width; x++) {
// 循环遍历每个像素行
for (int y = 0; y < height; y++) {
// 使用公式查找1D位置
int loc = x + y * width;
if (x % 2 == 0) { // 如果我们是偶数列
pixels[loc] = color(255);
} else { // 如果我们是奇数列
pixels[loc] = color(0);
}
}
}
updatePixels();
图像处理简介
上一节介绍了根据任意计算设置像素值的示例。现在我们将看看如何根据现有PImage对象中的像素设置像素。这是一些伪代码。
- 将图像文件加载到PImage对象中
- 对于PImage中的每个像素,检索像素的颜色并将显示像素设置为该颜色。
PImage类包含一些有用的字段,这些字段存储与图像宽度、高度和像素相关的数据。与用户定义的类一样,我们可以通过点语法访问这些字段。
PImage img = createImage(320,240,RGB); // 创建图像
println(img.width); // 打印 图像宽度
println(img.height); // 打印 图像高度
img.pixels[0] = color(255,0,0); // 设置序号为0的像素颜色为红色
访问这些字段允许我们循环浏览图像的所有像素并在屏幕上显示它们。
示例:显示图像的像素
PImage img;
void setup() {
size(200, 200);
img = loadImage("sunflower.jpg");
}
void draw() {
loadPixels();
// 因为我们也要访问图像的像素
img.loadPixels();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int loc = x + y*width;
// 函数red()、green()和blue()从像素中提取3个颜色分量。
float r = red(img.pixels[loc]);
float g = green(img.pixels[loc]);
float b = blue(img.pixels[loc]);
//这里是图像处理
//如果我们要改变RGB值,我们可以在这里做,
//在显示窗口中设置像素之前。
//将显示像素设置为图像像素
pixels[loc] = color(r,g,b);
}
}
updatePixels();
}
现在,为了只显示图像,我们当然可以进行简化(例如,不需要嵌套循环,更不用说使用image()函数将允许我们完全跳过所有这些像素工作。)但是,示例15-7提供了一个基本框架来获得红色、绿色,以及基于其空间方向(XY位置)的每个像素的蓝色值;最终,这将允许我们开发更高级的图像处理算法。
在我们继续之前,我应该强调这个示例是有效的,因为显示区域的尺寸与源图像的尺寸相同。如果不是这样,您只需进行两个像素位置计算,一个用于源图像,一个用于显示区域。
int imageLoc = x + y*img.width;
int displayLoc = x + y*width;
我们的第二个图像过滤器 "tint"
我们的第二个图像过滤器,就在几段之前,我们正在享受一个轻松的编码过程,用友好的tint()方法给图像着色并添加alpha透明度。对于基本过滤,这个方法做到了。然而,逐像素方法将允许我们开发自定义算法,以数学方式改变图像的颜色。考虑到明亮的颜色对于它们的红色、绿色和蓝色分量有更高的值。自然地,我们可以通过增加或减少每个像素的颜色分量来改变图像的亮度。在下一个例子中,我们根据鼠标的水平位置动态地增加或减少这些值。(注意,接下来的两个示例仅包括图像处理循环本身,其余代码都是假定的。)
示例:调整图像亮度
for (int x = 0; x < img.width; x++) {
for (int y = 0; y < img.height; y++ ) {
// 计算1D像素位置
int loc = x + y*img.width;
// 从图像中获取R,G,B值
float r = red (img.pixels[loc]);
float g = green (img.pixels[loc]);
float b = blue (img.pixels[loc]);
// 根据鼠标改变亮度
float adjustBrightness = ((float) mouseX / width) * 8.0;
r *= adjustBrightness;
g *= adjustBrightness;
b *= adjustBrightness;
// 将RGB限制在0-255之间
r = constrain(r,0,255);
g = constrain(g,0,255);
b = constrain(b,0,255);
// 创建新颜色并在窗口中设置像素
color c = color(r,g,b);
pixels[loc] = c;
}
}
因为我们是在每个像素的基础上改变图像,所以不需要对所有像素进行同等对待。例如,我们可以根据每个像素与鼠标的距离来改变其亮度。
示例:基于像素位置调整图像亮度
for (int x = 0; x < img.width; x++) {
for (int y = 0; y < img.height; y++ ) {
// 计算1D像素位置
int loc = x + y*img.width;
//从图像中获取R,G,B值
float r = red (img.pixels[loc]);
float g = green (img.pixels[loc]);
float b = blue (img.pixels[loc]);
//计算更改亮度的量
//基于接近鼠标
float distance = dist(x,y,mouseX,mouseY);
float adjustBrightness = (50-distance)/50;
r *= adjustBrightness;
g *= adjustBrightness;
b *= adjustBrightness;
// 将RGB限制在0-255之间
r = constrain(r,0,255);
g = constrain(g,0,255);
b = constrain(b,0,255);
// 创建新颜色并在窗口中设置像素
color c = color(r,g,b);
pixels[loc] = c;
}
}
写入另一个PImage对象的像素
我们的所有图像处理示例都从源图像读取每个像素,并将新像素直接写入处理窗口。但是,将新像素写入目标图像(然后使用image()函数显示)通常更方便。我们将在查看另一个简单的像素操作时演示此技术:阈值。
阈值过滤器仅在两种状态(黑色或白色)中的一种状态下显示图像的每个像素。该状态是根据特定的阈值设置的。如果像素的亮度大于阈值,我们将像素着色为白色,小于,黑色。在下面的代码中,我们使用100的任意阈值。
示例:亮度阈值
PImage source; // 源图像
PImage destination; //目标图像
void setup() {
size(200, 200);
source = loadImage("sunflower.jpg");
// 目标图像创建为与源图像大小相同的空白图像。
destination = createImage(source.width, source.height, RGB);
}
void draw() {
float threshold = 127;
//我们要看两幅图像的像素
source.loadPixels();
destination.loadPixels();
for (int x = 0; x < source.width; x++) {
for (int y = 0; y < source.height; y++ ) {
int loc = x + y*source.width;
// 根据阈值测试亮度
if (brightness(source.pixels[loc]) > threshold) {
destination.pixels[loc] = color(255); // White
} else {
destination.pixels[loc] = color(0); // Black
}
}
}
// 我们改变了目的地的像素
destination.updatePixels();
// 显示目的地
image(destination,0,0);
}
此特定功能不需要作为处理的filter()函数的一部分进行逐像素处理。但是,如果要实现自己的图像处理算法,而不是filter()提供的图像处理算法,那么理解较低级别的代码是至关重要的。
但如果你想做的只是门槛,这里是如何:
//绘制图像
image(img,0,0);
//用阈值效果过滤窗口
//0.5表示阈值为50%亮度
filter(THRESHOLD,0.5);
二级:像素组处理
在前面的例子中,我们看到了源像素和目标像素之间的一对一关系。为了增加图像的亮度,我们从源图像中提取一个像素,增加RGB值,并在输出窗口中显示一个像素。为了执行更高级的图像处理功能,我们必须超越一对一的像素范式,进入像素组处理。
我们先从源图像的两个像素中创建一个新像素—一个像素及其左边的邻居。
如果我们知道像素位于(x,y):
int loc = x + y*img.width;
color pix = img.pixels[loc];
其左邻位于(x-1,y):
int leftLoc = (x-1) + y*img.width;
color leftPix = img.pixels[leftLoc];
然后,我们可以根据像素与其左侧相邻像素之间的差异创建新颜色。
float diff = abs(brightness(pix) - brightness(leftPix));
pixels[loc] = color(diff);
下面是完整的算法:
示例:像素邻域差异(边)
//因为我们在看左邻右舍
//我们跳过第一列
for (int x = 1; x < width; x++) {
for (int y = 0; y < height; y++ ) {
// 像素位置和颜色
int loc = x + y*img.width;
color pix = img.pixels[loc];
// 像素到左边的位置和颜色
int leftLoc = (x-1) + y*img.width;
color leftPix = img.pixels[leftLoc];
// 新颜色是像素和左邻域之间的差异
float diff = abs(brightness(pix) - brightness(leftPix));
pixels[loc] = color(diff);
}
}
这个例子是一个简单的水平边缘检测算法。当像素与相邻像素相差很大时,它们很可能是“边缘”像素。例如,想象一张白纸放在黑色桌面上的照片。那张纸的边缘是颜色最不一样的地方,在那里白色和黑色相遇。
在前面的示例中,我们查看了两个像素来查找边。然而,更复杂的算法通常需要一次查看多个像素。毕竟,每个像素有8个近邻:左上、左上、右上、右下、右下、左下、左下。
这些图像处理算法通常被称为“空间卷积”。该过程使用输入像素及其邻域的加权平均值来计算输出像素。换句话说,新像素是像素区域的函数。可以使用不同大小的相邻区域,例如3x3矩阵、5x5等。
每个像素的不同权重组合会产生不同的效果。例如,我们通过减去相邻像素值并增加中心点像素来“锐化”图像。模糊是通过取所有相邻像素的平均值来实现的。(注意卷积矩阵中的值加起来是1)。
例如:
锐化:
-1 -1 -1
-1 9 -1
-1 -1 -1
模糊:
1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9
下面是一个使用2D数组执行卷积的示例(关于2D数组的回顾,请参见第13章第XX页),以存储3x3矩阵的像素权重。这个例子可能是我们在这本书中遇到的最高级的例子,因为它涉及到很多元素(嵌套循环、二维数组、PImage像素等等)
示例:使用卷积锐化
PImage img;
int w = 80;
//可以进行卷积
//不同矩阵的图像
float[][] matrix = { { -1, -1, -1 },
{ -1, 9, -1 },
{ -1, -1, -1 } };
void setup() {
size(200, 200);
frameRate(30);
img = loadImage("sunflower.jpg");
}
void draw() {
//我们只处理图像的一部分
//所以我们先把整个图像设为背景
image(img,0,0);
//我们要处理的小矩形在哪里
int xstart = constrain(mouseX-w/2,0,img.width);
int ystart = constrain(mouseY-w/2,0,img.height);
int xend = constrain(mouseX+w/2,0,img.width);
int yend = constrain(mouseY+w/2,0,img.height);
int matrixsize = 3;
loadPixels();
//为每个像素开始循环
for (int x = xstart; x < xend; x++) {
for (int y = ystart; y < yend; y++ ) {
//每个像素位置(x,y)被传递到一个称为卷积()的函数中
//返回要显示的新颜色值。
color c = convolution(x,y,matrix,matrixsize,img);
int loc = x + y*img.width;
pixels[loc] = c;
}
}
updatePixels();
stroke(0);
noFill();
rect(xstart,ystart,w,w);
}
color convolution(int x, int y, float[][] matrix, int matrixsize, PImage img) {
float rtotal = 0.0;
float gtotal = 0.0;
float btotal = 0.0;
int offset = matrixsize / 2;
//循环卷积矩阵
for (int i = 0; i < matrixsize; i++){
for (int j= 0; j < matrixsize; j++){
// 我们在测试什么像素
int xloc = x+i-offset;
int yloc = y+j-offset;
int loc = xloc + img.width*yloc;
// 确保我们没有离开像素阵列的边缘
loc = constrain(loc,0,img.pixels.length-1);
//计算卷积
//我们将所有相邻像素乘以卷积矩阵中的值求和。
rtotal += (red(img.pixels[loc]) * matrix[i][j]);
gtotal += (green(img.pixels[loc]) * matrix[i][j]);
btotal += (blue(img.pixels[loc]) * matrix[i][j]);
}
}
//确保RGB在范围内
rtotal = constrain(rtotal,0,255);
gtotal = constrain(gtotal,0,255);
btotal = constrain(btotal,0,255);
// 返回结果颜色
return color(rtotal,gtotal,btotal);
}
形象化
你可能会想:“天哪,这一切都很有趣,但说真的,当我想模糊图像或改变其亮度时,我真的需要编写代码吗?我是说,我不能用Photoshop吗?”事实上,我们在这里所取得的成果仅仅是对Adobe高技能程序员所做工作的初步了解。然而,处理能力是实时交互式图形应用的潜力所在。我们没有必要生活在“像素点”和“像素组”处理的范围内。
下面是两个绘制处理形状的算法示例。我们不再像过去那样随机地或用硬编码值给形状着色,而是从PImage对象内部的像素中选择颜色。图像本身永远不会显示出来;相反,它是一个信息数据库,我们可以利用它来进行大量的创造性追求。
在第一个示例中,对于通过draw()的每个循环,我们使用从源图像中相应位置获取的颜色在屏幕上的随机位置填充一个椭圆。其结果是一个基本的“点石成金”的效果:
例子:“点主义”
PImage img;
int pointillize = 16;
void setup() {
size(200,200);
img = loadImage("sunflower.jpg");
background(0);
smooth();
}
void draw() {
//选取一个随机点
int x = int(random(img.width));
int y = int(random(img.height));
int loc = x + y*img.width;
// 在源图像中查找RGB颜色
loadPixels();
float r = red(img.pixels[loc]);
float g = green(img.pixels[loc]);
float b = blue(img.pixels[loc]);
noStroke();
// 用那个颜色在那个位置画一个椭圆
fill(r,g,b,100);
ellipse(x,y,pointillize,pointillize);
}
在下一个示例中,我们从二维图像中获取数据,并使用第14章中描述的三维转换技术,为三维空间中的每个像素渲染一个矩形。z位置由颜色的亮度决定。较亮的颜色看起来更接近观察者,较暗的颜色则更远离观察者。
示例:映射到三维的二维图像
PImage img; // 源图像
int cellsize = 2; // 网格中每个单元格的尺寸
int cols, rows; // 系统中的列和行数
void setup() {
size(200, 200, P3D);
img = loadImage("sunflower.jpg"); // 加载图像
cols = width/cellsize; // 计算列数
rows = height/cellsize; // 计算行数
}
void draw() {
background(0);
loadPixels();
// 开始循环列
for ( int i = 0; i < cols;i++) {
// 开始行循环
for ( int j = 0; j < rows;j++) {
int x = i*cellsize + cellsize/2; // x坐标
int y = j*cellsize + cellsize/2; // y坐标
int loc = x + y*width; // 像素阵列位置
color c = img.pixels[loc]; // 抓住颜色
// 根据鼠标和像素亮度计算z位置
float z = (mouseX/(float)width) * brightness(img.pixels[loc]) - 100.0;
// 转换到位置,设置填充和笔划,并绘制矩形
pushMatrix();
translate(x,y,z);
fill(c);
noStroke();
rectMode(CENTER);
rect(0,0,cellsize,cellsize);
popMatrix();
}
}
}