图片和纹理
仅仅使用基本的几何元素来创建丰富的可视化效果通常是不够的。图像是帮助增加装饰,风格,甚至照片现实主义到一个互动场景的积木。在本章中,我们将介绍可以在图像上执行的基本操作:
加载和绘制图像
旋转图像
色彩调制
透明度
创建和修改图像
使用ofTexture进行内存优化
图像变形和视频映射
光栅和矢量图像
在计算机图形学和计算机视觉中,图像是一种用途广泛的二维图像。有两类图像:光栅图像和矢量图像。
光栅图像是由图像元素(称为像素)组成的矩形阵列,对于表示来自数码相机的照片来说,它们是很自然的。现代计算机屏幕是像素的物理阵列,因此屏幕是显示光栅图像的自然设备。
矢量图像由许多图形元素(如直线、圆形和曲线)组成,它们很自然地用于表示精确的绘图(如卡通和图形)。矢量图像可以放大,没有任何质量损失,并增加了存储器的大小,因此,他们用于参数化绘图。
可以处理光栅和矢量图像。在这一章中,我们将只处理光栅图像。为了处理矢量图像,
示例/插件/svgexample示例。
让我们考虑从文件中加载图像的两个基本操作
在屏幕上画出来。
加载和绘制图像
要加载和绘制图像,需要声明图像对象,从文件中加载图像,并在testApp::draw()函数中添加一个绘图函数调用。执行以下步骤:
1.将图像声明为ofImage对象:
ofImage image;
最好的方法是在testApp.h文件中的testApp类声明中声明图像。为了简单起见,有时我们将在testApp.cpp文件上面声明它们。
2.使用loadImage函数从文件中加载图像:
image.loadImage( fileName );
这里,fileName是一个指定文件名的字符串值;例如,sunflower.png。通常,图像应该位于应用程序的bin/data文件夹中。如果要使用其他文件夹中的图像,可以使用绝对路径;例如,在Windows中使用image.loadImage(“c:myimage.png”)。
3.使用testApp::Draw()函数中的image.Draw(x,y)函数绘制图像。在这里,x和y是指定屏幕上图像左上角的浮点值。
让我们在一个项目中实现这些步骤。它只是在屏幕上绘制一个图像。该项目基于openFrameworks的emptyExample示例。用示例复制文件夹并重命名它。然后将图像sunflower.png放入项目的bin/data文件夹中。现在,用下面的代码替换testApp.cpp文件的开头:
#include "testApp.h"
ofImage image; //Declare image object
void testApp::setup(){
//Load image file
image.loadImage("sunflower.png");
}
void testApp::update(){
}
void testApp::draw(){
//Set up gray background
ofBackground(128, 128, 128);
//Draw image with top left corner x=100, y=50 pixels
image.draw( 100, 50 );
}
运行该项目;您将看到下面在屏幕截图中显示的图像。
如您所见,我们使用了PNG格式的图像。此外,openFrameworks允许我们以JPG、BMP和TIFF文件格式加载和保存图像。其中,PNG是最有用的,因为它保持了原始图像的高质量,可以保持透明度,具有小文件尺寸,解码速度非常快。Jpg适合拍摄平滑逼真的图片,比如照片。这种格式可以减少可见图像的质量,并不与透明度工作,但有更小的文件大小的情况下,真实的照片。Bmp和TIFF以未压缩的形式存储图像。它们有利于保存和处理图像而不会丢失质量。它们很少用于交互式应用程序,因为它们的文件大小太大,而且从这些文件中加载图像的速度很慢。
您不仅可以加载图像,还可以将它们保存到PNG、JPG、BMP或TIFF文件中。为此,请使用image.saveImage()方法。参见下面的例子:
image.saveImage( "test.png" );
可以使用draw()方法的重载版本:image.draw(x,y,w,h)在屏幕上移动、缩放和拉伸图像。它绘制图像对象,另外指定像素的宽度w和高度h。
另外,还有image.draw()方法的重载版本,它允许我们简化代码:
1.image.draw( p ) -使用point类型的点p绘制图像。
2.image.draw( rect ) – 使用矩形rect类型绘制图像ofRectangle。
要检索以像素为单位的原始图像大小,可以使用其width和height字段image.width和image.height,其类型为int。以下是使用这些方法的例子:
3.绘制一个大小为其50%的图像,左上角为(0,0):
image.draw( 0, 0, image.width*0.5, image.height*0.5 );
4.绘制宽度等于300像素和高度成比例的图像:
image.draw( 0, 0, 300, 300.0*image.height/image.width );
5.绘制任意比例的图像;例如,宽度100,高度200:
image.draw( 0, 0, 100, 200 );
6.可以使用宽度或高度的负值翻转图像。例如,对于垂直翻转,使用以下代码:
image.draw( 0, image.height, image.width, -image.height);
注意:而不是在图像的参数中写入大的公式。Draw(x,y,w,h)方法,您可以使用ofTranslate(x,y)和ofScale(scaleX,scaleY)方法移动和缩放坐标系,该方法用于绘制屏幕上的所有内容。(参见第2章的坐标系变换部分,2D绘图以了解细节。)您可以在需要的连续性中调用ofTranslate()和ofScale()来获得所需的转换。如果您对坐标转换不是很熟悉,那么看起来就会很难。但是,相信我,它使您的代码更加干净、易于阅读和维护。另外,请参阅下一节了解详细信息。
旋转图像
image.draw()方法没有参数来旋转任意角度的图像。为了达到这个效果,我们需要使用坐标系/坐标系转换,这些转换将在坐标系/值转换中详细描述第二章二维绘图部分。
我们需要执行以下步骤来绘制旋转图像:
1.使用ofPushMatrix()存储当前变换矩阵。
2.使用ofRotate()应用旋转变换更改矩阵。
3.使用image.Draw()绘制图像。
4.使用ofPopMatrix()恢复原来的变换矩阵。
下面的代码演示了这些步骤。它绘制围绕当前坐标中心(0,0)旋转10度的图像。
void testApp::draw(){
ofPushMatrix(); //Store the transformation matrix
ofRotate( 10.0 ); //Applying rotation on 10 degrees
image.draw( 0, 0 ); //Draw image
ofPopMatrix(); //Restore the transformation
}
有时候,我们会想要旋转一个图像围绕其中心,而不是左上角。为了达到这个目的,我们需要将坐标中心平移到想要的旋转中心,旋转坐标系,最后绘制一个经过平移的图像,使图像的中心位于坐标中心。下面的代码通过绘制一个随时间慢慢旋转的图像来说明这一点:
void testApp::draw(){
ofPushMatrix ();
//Shift center of coordinate system ( 0,0 ) to the desired
//point, which will be rotation center
ofTranslate( 500, 400 );
//Rotate coordinate system, 10 degrees per second
ofRotate( 10.0 * ofGetElapsedTimef() );
//Draw image in a way that its center on the screen coincide
//with ( 0,0 )
image.draw( -image.width/2, -image.height/2 );
ofPopMatrix();
}
此外,还有一种更优雅的方式来绘制以特定点为中心的图像。与使用image.draw(-image.width/2,-image.height/2)不同,我们可以更改图像的定位点,即在绘制图像时用作原点的点。可以通过调用以下函数来实现:
image.setAnchorPercent( 0.5, 0.5 );
前面的方法将锚点设置为50%,即图像大小的50%,这是图像的中心。然后调用image.draw(0,0)方法,该方法将绘制以(0,0)为中心的图像。若要将锚重置为默认状态,请调用image.setAnchorPercent(0,0)或image.resetAnchor()。
您还可以通过使用image.setAnchorPoint(x,y)函数在像素坐标(x,y)中指定锚点来设置锚点。
使用ofTranslate()、ofScale()和ofRotate()是试验参数化绘图的好方法。下面是一个创建图片拼贴的例子:
注意:这是例子04-images/02-imagespiral。
void testApp::draw(){
//Set up white background
ofBackground( 255, 255, 255 );
for (int i=0; i<20; i++) {
ofPushMatrix();
//Translate system coordinates to screen center
ofTranslate( ofGetWidth() / 2, ofGetHeight() / 2 );
//Rotate coordinate system on i * 15 degrees
ofRotate( i * 15 );
//Go right on 50 * i * 10 pixels
//in rotated coordinate system
ofTranslate( 50 + i * 10, 0 );
//Scale coordinate system for decreasing drawing
//image size
float scl = 1.0 - i * 0.8 / 20.0;
ofScale( scl, scl );
//scl decreases with i, so the images Chapter 4
//became gradually smaller
//Draw image
image.draw( -100, -100, 200, 200 );
ofPopMatrix();
}
}
运行这个项目,你会看到一个由图片组成的螺旋,如下截图所示:
色彩调制
通过将每个像素的颜色分量乘以某个固定的数字,可以很好地改变绘图图像的总体颜色。它是通过使用ofSetColor()函数来实现的。在image之前调用ofSetColor(r,g,b)或ofSetColor(r,g,b,a)意味着每个图像像素的红色、绿色、蓝色和alpha分量将分别乘以r'=r/255.0、g'=g/255.0、b'=b/255.0和a'=a/255.0。
请注意,ofSetColor中的参数r、g、b和a位于0到255之间,因此r'、g'、b'和a'位于范围[0。.1].因此,通过使用这种调制,您可以减少或保留像素的颜色分量,但不能增加它们。
对于绘制图像时使用颜色组件的任意操作,请使用片段着色器(参见第8章,使用着色器)。此外,您还可以更改图像本身的所有像素。这种方法是好的和适当的,但工作缓慢时,它改变的形象。有关详细信息,请参阅创建和修改图像部分。
以下是一些例子:
1.用不变的颜色绘制图像:
ofSetColor( 255, 255, 255 );
image.draw( 0, 0, 200, 100 );
2.绘制具有一半颜色值的图像:
ofSetColor( 128, 128, 128);
image.draw( 250, 0, 200, 100 );
3.只绘制图像的红色通道
ofSetColor( 255, 0, 0 );
image.draw( 150, 0, 200, 100 );
你会看到下面截图中的图片:
您可以注意到,颜色调制的结果类似于Adobe Photoshop或Gimp等照片编辑器中的色调校正。
记住,调用ofSetColor()会影响调用之后绘制的所有图像。因此,如果您需要绘制没有调制的图像,在绘制图像之前调用ofSetColor(255,255,255)是一个好主意。
您可以看到,在这些示例中,我们没有演示alpha通道的用法。这是一个非常重要的透明度问题,我们现在将详细讨论这个问题。
透明度
使用前面章节中描述的方法,我们可以构建图像的重叠拼贴,改变它们的大小、方向和颜色。直到现在,这样的拼贴画都是由图像组成的,看起来像彩色矩形。但是我们通常希望有非矩形图像拼贴画,如下面的截图所示:
在前面的截图中,拼贴是由许多向日葵图像组成的,不是长方形,而是相当复杂的曲线形状。直接对这些形状建模是一项困难且耗费内存的任务。在光栅图形中使用的一个更优雅的解决方案是使用alpha通道。在这种技术中,我们仍然使用矩形图像,但考虑到像素不仅有颜色组件,而且还有一个额外的alpha组件,控制像素的不透明度。最小alpha值(0)意味着像素是绝对透明的;也就是说,用户是不可见的。最大alpha值(255)表示该像素是不透明的。您可以使用首选的图像编辑器(如adobephotoshop或Gimp)准备具有透明像素的图像。在编辑器中,用魔术棒工具或橡皮擦工具移除背景像素,并以PNG格式保存文件。在保存时,选择24位或32位PNG格式,但不要选择8位PNG格式,因为它的面板有限,不适合我们的目的。
注意:请注意,JPG文件并不保持透明性。
删除背景的结果如下图所示:
不仅有绝对透明和不透明的像素(alpha0和255),还有介于两者之间的所有像素(alpha值从1到254)。如何处理这些像素?这种带有透明度的重叠颜色的过程称为混合。默认情况下,将新color(r,g,b,a)与屏幕像素的旧color(r,g,b,a)混合使用以下公式:
1.R' = (1-a/255) · R + a/255 · r
2.G' = (1-a/255) · G + a/255 · g
3.B' = (1-a/255) · B + a/255 · b
4.A' = (1-a/255) · A + a/255 · a
您可以看到,如果a等于255,(r',g',b',a')等于(r,g,b,a);也就是说,屏幕的像素颜色被一种新颜色取代。如果a等于0,(r',g',b',a')等于(r,g,b,a),也就是说,一个新的像素不影响屏幕,因此是不可见的。
与这样的配方混合称为α混合。非常适合普通拼贴。但是还有其他的模式可以通过使用ofEnableBlendMode()函数进行切换。
例如,添加模式可以通过以下方式启用,该模式只对颜色进行汇总下面这句话:
ofEnableBlendMode( OF_BLENDMODE_ADD );
注意:在测试此模式时,不要使用白色作为背景色。因为向白色中添加颜色会导致白色再次出现!所以,如果你设置一个白色的背景,产生的图片将永远是纯白色的。
若要返回alpha混合模式,请调用以下函数:
ofEnableBlendMode( OF_BLENDMODE_ALPHA );
请参阅openFrameworks示例中的其他内置混合模式示例,位于examples/graphics/blendingexample。
注意:应该注意的是,内置的混合模式是有用的和简单的,但固定的,因此,有限的。实现特殊、参数化和非平稳混合模式的最灵活的工具是片段着色器(参见第8章,使用着色器)。
默认情况下,混合是在openFrameworks中启用的,因此alpha通道用于绘制。如果需要禁用混合并将所有像素视为不透明,请调用ofDisableAlphaBlending()函数。要再次启用混合,请调用ofDisableAlphaBlending() 函数。
注意:禁用混合通常用于绘制FBO(参见第二章二维绘图中使用FBO作为屏外绘图部分中FBO的细节)。原因是为了消除不想要的二次混合,当FBO包含透明像素时会发生这种情况。
当启用alpha混合时,可以通过调用小于255的ofSetColor(r,g,b,a)将整个图像绘制为半透明图像。例如,下面的代码绘制一个半透明的图像:
ofSetColor( 255, 255, 255, 128 );
image.draw( 0, 0 );
下面的代码演示了如何使用带有alpha分量的alpha通道和颜色调制来处理透明性。它基于openFrameworks的emptyExample项目。在运行它之前,将sunflower-transp.png文件复制到项目的bin/data文件夹中。
注意:这是04-images/03-imagetransp的例子。
#include "testApp.h"
ofImage image; //Declare image object
void testApp::setup(){
image.loadImage("sunflower-transp.png");
}
void testApp::update(){
}
void testApp::draw(){
//Set up white background
ofBackground(255, 255, 255);
//Draw two images without color modulation
//(but using alpha channel by default)
ofSetColor( 255, 255, 255, 255 );
image.draw( 100, 0 );
image.draw( 250, 0 );
//Draw half-transparent image
ofSetColor( 255, 255, 255, 128 );
image.draw( 400, 0 );
}
在运行该项目时,您将看到两个不透明的向日葵图像和一个半透明的向日葵图像。所有图片都去掉了背景像素。
使用带有阿尔法通道的图像是创建卡通风格的交互式装置的强大技术。例如,可以在下面的屏幕截图中看到我们的互动装置Kuklon(IgorSodazotDenisPerevalov,2011)的图像。这个装置展示了一个虚构的世界。一个有趣的洋娃娃重复观众的动作生活在那里。
下面这张截图中最大的图片是这个场景的结果,其他的图片是这个场景的组成部分:
创建和修改图像
在前面的章节中,我们考虑了从文件中绘制图像的不同方法。在本节中,我们将了解如何通过直接指定图像的像素来生成新图像或更改现有图像。
光栅图像表示为内存中的像素数组。如果我们有一个宽度为w像素、高度为h像素的图像,它就用n=w*h像素来表示。通常,图像的水平行依次位于内存中:第一行的w像素,第二行的w像素,以此类推到h行。
根据图像类型的不同,图像的像素可以容纳不同数量的信息。在openFrameworks中,使用以下类型:
1.Of_Image_Color_Alpha类型表示具有透明度的彩色图像。在这里,每个像素由4个字节表示,分别包含红色、绿色、蓝色和alpha颜色组件,值从0到255。
2.Of_Image_Color类型表示没有透明度的彩色图像。在这里,每个像素由3个字节表示,包含红色、绿色和蓝色组件。这种图像是在不需要透明像素时使用的。例如,这种类型的openFrameworks中表示的来自相机的JPG文件和图像。
3.Of_Image_GrayScale类型表示一个灰度图像。这里的每个像素由1个字节表示,并且只包含颜色的一个组件。大多数情况下,这样的图像用于表示面具。在大多数情况下,我们使用彩色图像,但如果您的项目需要大量的蒙版或半色调图像使用灰度类型,因为它占用较少的内存。
注意:在本书中,我们主要讨论image类的图像,其中每个像素组件由1个字节表示,整数值从0到255(类型为无符号字符)。但是,在某些情况下,需要更多的准确性。这种情况发生在使用内容逐步擦除的缓冲区或使用图像作为高度映射时。为此,openFrameworks有一个图像类ofFloatImage。该类的方法与ofImage相同,但每个像素组件保存一个浮点值。有关如何使用它的示例,请参见examples/graphics/floatingpointimagexample。
另外,还有ofShortImage类,它使用0到65535范围内的整数值,即无符号短类型。这样的图像最适合表示来自深度相机的数据,像素与场景物体的距离以毫米为单位。
更多关于使用这些图像类型的细节见第9章OpenCV计算机视觉和第10章使用深度摄像机。
创建图像
要通过代码创建图像,我们需要创建一个像素数组,然后使用image.setFromPixels(data,w,h,type)方法将其推送到图像中。这里的数据是像素数组,w是图像宽度,h是图像高度,类型是图像类型(Of_Image_Color_Alpha,Of_Image_Color,or Of_Image_Grayscale)。
数据应该是无符号字符类型的数组。如果我们创建一个有宽度w和高度h像素的四通道图像,那么数组大小将为w*h*4字节。
对于给定从0到w-1的x和从0到h-1的y,我们有位于data[index]、data[index+1]、data[index+2]和data[index+3]中的像素(x,y)的红色、绿色、蓝色和alpha分量,其中index等于4*(x+w*y)。
在以下示例中,图像是在每个testApp::update()函数调用中生成的,并且会随着时间的推移而演变。
注意:这是04-images/04-colorwaves的例子。
#include "testApp.h"
ofImage image; //Declare image object
void testApp::setup(){
}
void testApp::update(){
//Creating image
int w = 512; //Image width
int h = 512; //Image height
//Allocate array for filling pixels data
unsigned char *data = new unsigned char[w * h * 4];
//Fill array for each pixel (x,y)
for (int y=0; y<h; y++) {
for (int x=0; x<w; x++) {
//Compute preliminary values,
//needed for our pixel color calculation:
//1. Time from application start
float time = ofGetElapsedTimef();
//2. Level of hyperbola value of x and y with
//center in w/2, h/2
float v = ( x - w/2 ) * ( y - h/2 );
//3. Combining v with time for motion effect
float u= v * 0.00025 + time;
//Here 0.00025 was chosen empirically
//4. Compute color components as periodical
//functions of u, and stretched to [0..255]
int red = ofMap( sin( u ), -1, 1, 0, 255 );
int green = ofMap( sin( u * 2 ), -1, 1, 0, 255 );
int blue = 255 - green;
int alpha = 255; //Just constant for simplicity
//Fill array components for pixel (x, y):
int index = 4 * ( x + w * y );
data[ index ] = red;
data[ index + 1 ] = green;
data[ index + 2 ] = blue;
data[ index + 3 ] = alpha;
}
}
//Load array to image
image.setFromPixels( data, w, h, OF_IMAGE_COLOR_ALPHA );
//Array is not needed anymore, so clear memory
delete[] data;
}
void testApp::draw(){
ofBackground(255, 255, 255); //Set up white background
ofSetColor( 255, 255, 255 ); //Set color for image drawing
image.draw( 0, 0 ); //Draw image
}
注意,对于时间测量,我们使用ofGetElapsedTimef()函数,它返回的浮点数等于应用程序启动后的秒数。
此外,我们使用ofMap()函数将sin(...)(位于[-1,1]中)的结果映射为interval[0,255]。详见第一章openFrameworks基础中的基本实用函数部分。
在运行上面的代码之后,你会看到一个带有移动颜色波动的动画图像,如下图所示:
修改图片
您可以修改现有的图像,而不是无中生有地创建图像。为此,请使用image.getPixels()函数,该函数返回图像的像素数组。更改此数组后,调用image.update()以应用对图像的更改。实际上,image.update()将更改后的图像加载到视频内存中,以便在屏幕上绘图;有关详细信息,请参阅使用ofTexture进行内存优化一节。
注意:这是04-images/05-imagemandify的例子。
在下面的示例中,我们读取和修改向日葵图像的像素并在屏幕上绘制它。我们只修改一次映像,在testApp::setup()中。在代码中,我们不知道哪种类型具有sunflower.png图像文件,Of_Image_Color或Of_Image_Color_Alpha。
出于这个原因,我们通过计算图像像素组件, (int components)的数量制作了一个通用代码,它等于image.bpp/8。在这里,image.bpp字段保存每个值的位,并表示为每个图像像素分配的位数。它可以是8、24或32,分别对应于OF_IMAGE_GRAYSCALE、OF_IMAGE_COLOR或OF_IMAGE_COLOR_ALPHA。所以,除以值8,我们得到像素分量1,3,或4的个数。在本例中,我们使用了一个彩色图像文件,因此组件将等于3或4(不是1)。
注意:在这个例子中,使用一些组件是很方便的。有时,直接检查图像类型更方便。图像类型保存在字段image.type中,并获取OF_IMAGE_GRAYSCALE、OF_IMAGE_COLOR和OF_IMAGE_COLOR_ALPHA。
在严肃的项目中,总是检查给定图像的颜色组件的类型或数量。使用不正确的类型假设执行图像修改会导致依赖于不正确的像素数组大小的计算。它可能会导致内存错误或者导致图像损坏。
代码如下:
#include "testApp.h"
ofImage image; //Declare image object
void testApp::setup(){
image.loadImage( "sunflower.png" ); //Load image
//Modifying image
//Getting pointer to pixel array of image
unsigned char *data = image.getPixels();
//Calculate number of pixel components
int components = image.bpp / 8;
//Modify pixel array
for (int y=0; y<image.height; y++) {
for (int x=0; x<image.width; x++) {
//Read pixel (x,y) color components
int index = components * (x + image.width * y);
int red = data[ index ];
int green = data[ index + 1 ];
int blue = data[ index + 2 ];
//Calculate periodical modulation
float u = abs(sin( x * 0.1 ) * sin( y * 0.1 ) );
//Set red component modulated by u
data[ index ] = red * u;
//Set green value as inverted original red
data[ index + 1 ] = (255 - red);
//Invert blue component
data[ index + 2 ] = (255 - blue);
//If there is alpha component or not,
//we don't touch it anyway
}
}
//Calling image.update() to apply changes
image.update();
}
void testApp::draw(){
ofBackground( 255, 255, 255 );
ofSetColor( 255, 255, 255 );
image.draw( 0, 0 ); //Draw image
}
在运行该项目时,您将看到向日葵图像带有非线性修改的颜色。
前面使用image.getPixels()操纵图像像素的方法很快,但有时不是很方便,因为你需要单独处理每个像素的颜色分量。所以,让我们考虑更方便的函数,它们使用color类型对像素的颜色进行操作。
使用单个像素的颜色
存在在不知道图像类型的情况下获取和设置图像像素颜色的功能:
1.image.getColor(x,y)函数读取图像像素(x,y)的颜色。它返回ofColor类型的对象,包括字段r、g、b、a、对应的红、绿、蓝和alpha颜色组件(详见第2章“2D绘图”中的“颜色”部分)。
2.image.setColor(x,y,color)函数将像素(x,y)的颜色设置为颜色值,其中color具有ofColor类型。在使用image.setColor()更改像素的颜色之后,需要调用image.update()函数以使更改生效。
请注意,使用这些函数的代码的总体性能可能略低于使用
这些函数的代码
Image.getPixels() and image.setFromPixels()。
让我们考虑一个使用这些函数处理几何图像畸变的例子
一个简单的几何畸变例子
这个例子通过正弦波改变图像的水平线,从而扭曲了图像的几何形状,而正弦波也会随着时间改变。为了实现这一点,我们保持原始图像不变,并使用它在testApp::update()函数中构建扭曲的图像image2。
注意:这是例子04-images/06-horizontaldistortion。
#include "testApp.h"
ofImage image; //Original image
ofImage image2; //Modified image
//--------------------------------------------------------------
void testApp::setup(){
image.loadImage( "sunflower.png" ); //Load image
image2.clone( image ); //Copy image to image2
}
void testApp::update(){
float time = ofGetElapsedTimef();
//Build image2 using image
for (int y=0; y<image.height; y++) {
for (int x=0; x<image.width; x++) {
//Use y and time for computing shifted x1
float amp = sin( y * 0.03 );
int x1 = x + sin( time * 2.0 ) * amp * 50.0;
//Clamp x1 to range [0, image.width-1]
x1 = ofClamp( x1, 0, image.width - 1 );
//Set image2(x, y) equal to image(x1, y)
ofColor color = image.getColor( x1, y );
image2.setColor( x, y, color );
}
}
image2.update();
}
void testApp::draw(){
ofBackground(255, 255, 255);
ofSetColor( 255, 255, 255 );
image2.draw( 0, 0 );
}
注意,在testApp::setup()函数中,我们使用image2.clone(image)函数,它将image复制到image2。在给定的示例中,需要使用它来分配image2。
当你运行上面的代码时,你会看到一个项目,在这个项目中你会看到一个挥动的向日葵图像,如下图所示:
注意:学习如何使用着色器在第8章使用着色器的一个简单的几何失真例子中实现类似的畸变。
我们即将结束对图像修改方法的讨论。现在,让我们考虑一下用于调整大小、裁剪和旋转图像的有用函数。
操作整个图像的功能
有许多函数执行全局图像处理。具体如下:
1.image.resize( newW, newH ) – 将图像调整到新的大小,newW × newH
2.image.crop( x, y, w, h ) – 将图像裁剪为左上角(x,y)大小为w×h的子图像
3. image.rotate90( times ) – 将图像顺时针旋转90倍
4.image.mirror( vertical, horizontal ) – 镜像图像,其中垂直和水平是bool值
5.image2.clone( image ) –将图像复制到image2(我们在前面的示例中使用了此函数)
下面我们将讨论CPU使用的普通存储器中的图像与显卡使用的视频存储器之间的关系。这对于理解和优化图像处理具有重要意义。
使用ofTexture进行内存优化
计算机中有两种类型的内存——中央处理器(CPU)使用的随机存取存储器(RAM)和视频卡使用的视频存储器,即图形处理器存储器(GPU)。Ram用于进行计算,视频存储器用于在屏幕上绘制图形。
注意:一个典型的GPU包含数百个计算核心,其数量每年都在增加。这就是为什么新的gpu比只有1到16个核的cpu拥有更强的计算能力。今天,几乎所有的计算都可以在GPU上使用着色器、OpenCL或NVIDIACUDA等技术进行。因此,CPU不再是计算机中最重要的单位。例如,可视化开发平台deriventtouchDesigner(仅适用于Windows)几乎完全在GPU上进行处理。
虽然在gpu中编程是一个非常强大的工具,但是与CPU编程相比有点棘手。而且,调试gpu还不是那么方便。因此,在这本书中,我们仍然主要考虑CPU编程和只触摸GPU编程时,谈论着色器在第8章,使用着色器。
计算机的体系结构假设所有的图像、矢量图形和3d对象将被描绘在屏幕上,应该首先加载到视频内存中。视频存储器中的图像称为纹理。默认情况下,openFrameworks的类ofImage包含两个相同的图像。这些是RAM中可由image.getPixels()访问的像素数组及其克隆,即可由image.getTextureReference()访问的视频内存中的纹理。
因此,当您更改图像的像素数组时,需要调用image.update()以便对相应的纹理应用更改。
你可能会问,为什么需要这样的双重代表?是的,确实,可以放弃纹理(使用image.setUseTexture(false)函数)并直接在屏幕上呈现像素数组。但无论如何,这个操作需要将像素数组加载到视频内存中,这是一个快速但却耗时的操作。所以,如果我们没有改变图像或希望在屏幕上多次绘制它,它最好有一个纹理。
您也可以放弃像素数组。像素数组只是将图像写入磁盘的工具,也是使用cpu更改图像的方便方法。所以,如果你不想改变你的图像,最好只使用纹理,不要在内存中有像素数组。要做到这一点,使用纹理而不是图片。如果你正在使用ofTexture,你的图像将仅仅位于视频内存中,不会占用任何内存。因此您可以获得内存优化,这对于大型项目至关重要。
在代码中使用纹理非常类似于使用ofImage的实例。以下是用于处理纹理的函数:
1.ofLoadImage( texture, fileName ) – 从图像文件中加载纹理
2.ofSaveImage( texture, fileName ) – 保存纹理到图像文件。
3.texture.draw( x, y ) or texture.draw( x, y, w, h ) – 绘制纹理
4.texture.loadData( data, w, h, format ) –从以下内容创建纹理
像素阵列数据,其中格式分别为4、3或1通道图像的GL_RGBA、GL_RGB或GL_LUMINANCE。
5. texture.getWidth()和texture.getHeight()用于获取纹理维度。
下面是使用ofTexture绘制图像的例子:
#include "testApp.h"
ofTexture texture; //Declare texture
void testApp::setup(){
//Load texture from file
ofLoadImage( texture, "sunflower.png" );
}
void testApp::update(){
}
void testApp::draw(){
ofBackground(255, 255, 255); //Set background color
ofSetColor( 255, 255, 255 );
texture.draw( 0, 0 ); //Draw texture
}
我们已经讨论了纹理的基础知识,现在将看到如何使用它来进行图像变形,以及它在视频映射中的应用。
图像变形和视频映射
到目前为止,我们绘制的图像是任意形状的刚性矩形。但是也有可能画出一个像是橡胶做的图像。为了达到这个效果,图像被分解成一个由许多三角形或四边形(四边形)组成的网格。每个三角形或四边形在其顶点上呈现一个任意的位置,同时保持网格中的邻接关系。这给图像增添了橡胶般的效果。
注意:另一种几何上实现畸变的方法是直接像素修改图像,这在前面已经讨论过了。但是对于大图像来说,这样的方法往往效果太慢。
使用着色器可以更快地绘制任意修改的图像(参见第8章“使用着色器创建视频特效”一节)。
这样的方法可以用来实现视频映射技术,也被称为投影映射。这项技术涉及投影仪的使用,投影图像的非平面和对象,如雕塑,建筑物和定制的建设,如多面体。在这种技术中,一个或多个投影仪的安装方式是所需的对象的表面被投影仪的光照亮。然后,计算机生成并发送图像给投影仪,以绘制纯色,纹理,甚至在这个表面上运动的物体。通常,它是通过渲染普通图像,然后在对象的表面上扭曲这些实现的。当投影仪和物体被物理安装和固定时,整经参数在舞台上手动或自动调整。
下面的屏幕截图显示了一个使用一个投影仪将视频映射到真实头部雕塑上的例子(IgorSodazot,2010年):
你可以提到图像的哪些边缘最适合雕塑的边缘。这是通过在视频编辑器中手动移动网格顶点来交互地扭曲图像的边缘来实现的。
注意:最流行和最先进的视频映射工具是MadMapper。您可以使用ofxSyphon插件(通过虹吸协议执行图像传输)将图像从openFrameworks发送到MadMapper。详细信息请参阅附录a,与插件一起工作。
在这里,我们考虑扭曲和视频映射的最简单示例;即扭曲矩形图像。该模型将被映射到相对于投影仪在3D中旋转的平坦矩形表面上。
该示例允许您任意移动屏幕上显示的图像的角。要选择图像的四个角之一,请按1、2、3或4键。要移动所选角,请按任意光标键。角落的枚举如下:
代码的基本函数是texture.draw(p[0],p[1],p[2],p[3]),它将纹理扭曲到选定的角点。
注意:这是04-images/07-videomapping的例子。
#include "testApp.h"
ofTexture texture;
ofPoint p[4]; //Corners
int ind = 0; //Index of selected corner, 0..3
void testApp::setup(){
//Load texture image
ofLoadImage( texture, "sunflower.png" );
//Set up initial corners
p[0].x = 100; p[0].y = 100;
p[1].x = 300; p[1].y = 100;
p[2].x = 300; p[2].y = 300;
p[3].x = 100; p[3].y = 300;
}
void testApp::update(){
}
void testApp::draw(){
ofBackground( 255, 255, 255 );
ofSetColor( 255, 255, 255 );
//Draw texture by specifying its target corners points
texture.draw( p[0], p[1], p[2], p[3] );
}
//Process keys
void testApp::keyPressed(int key){
//Select corner to edit by keys 1,2,3,4
if ( key == '1' ) { ind = 0; }
if ( key == '2' ) { ind = 1; }
if ( key == '3' ) { ind = 2; }
if ( key == '4' ) { ind = 3; }
//Move selected corner by cursor keys
if ( key == OF_KEY_LEFT ) { p[ ind ].x -= 10; }
if ( key == OF_KEY_RIGHT ) { p[ ind ].x += 10; }
if ( key == OF_KEY_UP ) { p[ ind ].y -= 10; }
if ( key == OF_KEY_DOWN ) { p[ ind ].y += 10; }
}
运行该项目,您将看到向日葵图像。然后按1、2、3或4来选择一个角并用光标键移动这个角。如果你有一个投影仪,你可以得到向日葵图像到任何矩形表面的视频映射;例如,在纸箱的一侧。
如果你经常转弯,畸变就会很高。你可以看到沿着对角线翘曲不是很好,因为会有一些不必要的额外失真。原因是示例中的texture.draw()方法通过绘制两个三角形来执行翘曲。所以这里的映射网格由两个带有共同(对角线)边的三角形组成。为了得到更平滑的结果,我们需要构造一个由至少50个三角形组成的更复杂的网格,并在角点移动时重新计算其顶点。它可以使用双线性插值和使用ofMesh类绘制网格,但是它超出了本章的范围。参见第7章中的使用ofMesh部分,3d绘图,了解有关网格绘图的细节。
使用图像进行内部计算
在这一章中,我们主要把图像看作是视觉场景的组成部分。在最后一节中,我们将看到如何以另一种方式使用图像,作为内部计算的数据源,而不是直接显示在屏幕上。这种用法的主要例子是面具和调色板,我们现在将讨论它们。
作为面具的图像
彩色照相机和深度照相机提供的颜色和深度图像代表了他们捕获的场景(例如,人在照相机前)。这些图像可以用逐像素的方法处理,也可以用计算机视觉库处理
Opencv.结果往往是一个二进制图像,这被称为面具,其中包含表示背景的黑色像素和表示人体轮廓的白色像素。这个掩码可以应用于控制物理和改变交互式安装的任何参数。用户从来不会看到面具图像本身,而只是感知它对交互场景行为的影响。看看使用深度相机获得的人体侧影面具的例子,微软Kinect:
有关从相机获取彩色图像及其处理的详细信息,请参阅第5章视频处理和第9章OpenCV计算机视觉。有关从深度相机获取和处理数据的详细信息,请参阅第10章,使用深度相机。
作为调色板的图像
图像可以用作调色板,用于设置项目中笔刷或粒子的颜色。这意味着你在图像中选择一些点p,然后根据一些柏林噪声的固定规则慢慢移动它。每次移动之后,您只需从图像中p的当前位置读取像素颜色,并使用它用画笔绘制或粒子颜色绘制。我们经常使用这样的技术,为不同的对象创建许多调色板,也为我们的项目获得可控的着色,这确保了可预测的行为。
在我们的互动装置和舞蹈表演中使用的调色板的一个例子抽象墙(由Kuflex制作,KseniaLyashenko制作,在微软当代文化车库中心展出,莫斯科,2013年)
如下图所示:
这个装置根据人体的运动作出反应,画出飞行的粒子从调色板中选择颜色。
在安装中更改粒子颜色的方法如下:当创建粒子时,它在调色板中的位置随机设置在调色板图像的底部。在粒子的生命周期中,它在调色板中的位置上升,所以粒子的颜色慢慢改变。
注意:改变粒子在调色板中位置的另一种常用方法是使用珀林噪声。详情请参阅附录b使用柏林噪音。
摘要
在本章中,我们学习了如何从文件中加载图像;在屏幕上使用不同大小、颜色和透明度渲染图像;创建新图像;以及修改现有图像。我们还触及了图像变形和视频映射的基本知识。
下一步是处理视频,我们将在下一章讨论。