用 Processing 进行数据可视化,第 2 部分
使用接口、对象、图像和应用程序的中间数据可视化
系列内容:
此内容是该系列 # 部分中的第 # 部分: 用 Processing 进行数据可视化,第 2 部分
此内容是该系列的一部分:用 Processing 进行数据可视化,第 2 部分
敬请期待该系列的后续内容。
在本系列的 第 1 部分 中,您了解了 Processing 作为可视化语言和环境有多么强大。本期继续探究 Processing,首先回顾其用户交互功能。
键盘和鼠标
Processing 不仅创建一种可视化数据的简单方法,而且支持来自鼠标和键盘的用户输入。Processing 通过一组函数和回调实现这一点,在获得用户输入时通知 Processing 程序。
键盘事件
Processing 提供一小组键盘函数,在某个键被按下或释放时通知 Processing 应用程序。您还可以进一步解析该输入,在事件中将特殊字符呈现给用户。
可使用 keyPressed
函数来表示一个按键操作。您可以在您的应用程序中定义该函数,且当按键操作发生时该函数被调用。然后使用回调函数中一个名为 key
的特殊变量来识别用户按下的那个键。类似地,当释放一个键时,您可以使用 keyReleased
函数来捕捉这一事件。注意,两个函数都产生相同的信息,但是允许您定义何时触发您的操作。
清单 1 显示 keyPressed
和 keyReleased
函数。不管在哪个函数中,程序都可以解析两种用户按键:未编码的 ASCII 字符和需要编码的非 ASCII 字符(比如箭头键)。对于编码的字符,Processing 将 key
变量设置为 CODED
令牌,以表明必须检查另一个名为 keyCode
的特殊变量。因此,如果 key
不是 CODED
,key
变量包含按键。如果 key
是
CODED
,keyCode
变量包含实际字符:UP
、DOWN
、LEFT
、RIGHT
、ALT
、CONTROL
或 SHIFT
。
清单 1. keyPressed
和 keyReleased
回调
void keyPressed() { if (key == CODED) { if (keyCode == DOWN) println("Key pressed: Down arrow"); if (keyCode == SHIFT) println("Key pressed: Shift key"); } else { println("Key pressed: " + key ); } } void keyReleased() { if (key == CODED) { if (keyCode == DOWN) println("Key released: Down arrow"); if (keyCode == SHIFT) println("Key released: Shift key"); } else { println("Key released: " + key ); } }
Processing 函数内还有一个特殊的 keyPressed
变量可供使用。keyPressed
函数返回一个布尔值,表示有键被按下(True)或无键被按下(False)。您可以使用该变量管理 Processing 应用程序内的键事件(典型回调结构之外)。
鼠标事件
鼠标事件遵循与键盘事件类似的一种结构,区别就是有多个函数支持发生的各种基于鼠标的事件。对于鼠标事件,可以定义 4 个基本回调:
mousePressed
mouseReleased
mouseMoved
mouseDragged
当用户单击一个鼠标按钮时,mousePressed
回调函数被调用。在回调函数内,您可以通过 mouseButton
变量(LEFT
、CENTER
、RIGHT
)识别特定鼠标按钮。当释放鼠标按钮时,mouseReleased
回调函数被调用。mouseClicked
(您可以使用的另一个回调函数)可同时调用 mousePressed
和 mouseReleased
。每次鼠标移动但未点击鼠标按钮时mouseMoved
函数被调用。最后,当鼠标移动且点击鼠标按钮时 mouseDragged
函数被调用。
另外有大量特殊变量可供使用。首先,mouseX
和 mouseY
变量包含鼠标的当前位置。您可以使用 pmouseX
和 pmouseY
变量(从前一帧)捕获鼠标的前一位置。您可以使用 Processing 应用程序内的 mouseClicked
变量来检测当前是否点击了鼠标按钮。将该变量与 mouseButton
结合使用来识别当前点击的按钮。清单 2 显示这些回调和特殊变量。
清单 2. 演示基本的鼠标事件回调
int curx, cury; void setup() { size(100, 100); curx = cury = 0; } void mousePressed() { println( "Mouse Pressed at " + mouseX + " " + mouseY ); if (mousePressed && (mouseButton == LEFT)) { curx = mouseX; cury = mouseY; } } void mouseReleased() { println( "Mouse Released at " + mouseX + " " + mouseY ); } void mouseMoved() { println( "Mouse moved, now at " + mouseX + " " + mouseY ); } void mouseDragged() { println( "Mouse moved from " + curx + " " + cury + " to " + mouseX + " " + mouseY ); }
这些鼠标和键盘事件为构建 UI 提供基础。在将对象置于显示屏上时,您可以使用鼠标事件函数来识别是否有鼠标被按下(即当按下鼠标按钮时对象区域内的像素是否是鼠标光标)。
面向对象的编程
Processing 允许使用面向对象编程(OOP)技术来使 Processing 应用程序更易于开发和维护。Processing 本身是面向对象的,但允许开发忽略对象概念的应用程序。OOP 很重要,因为它提供信息隐藏、模块化和封装等优势。
与其他面向对象的语言一样,Processing 使用 Class
的概念来定义对象模板。通过类定义的对象维护一组数据和可在该数据上执行的相关操作。我们首先看看简单类的开发,然后扩展该示例,包含该类的多个对象。
Processing 中的类定义一组数据和应用于该数据的函数(或方法)。本例中的数据是在 2-D 空间中具有坐标(x 和 y)和直径的一个圆。您可以使用 x 和 y 坐标以及值为 1 的直径来初始化圆,在本例中仅指出圆正在使用中。您可以使用 init
函数提供该信息。随着时间的推移,圆变大。如果直径大于 0(表示它是使用的对象),那么您应当通过递增其直径来递增圆的大小。这个递增是由 spread
函数提供的。最后,show
函数在显示屏中展示圆。只要直径大于 0(有效圆),您就可以使用 ellipse
函数创建圆。一旦圆增长到一定大小,您可以将其直径设置为 0 来取消它。这个类样例如清单 3 所示。
清单 3. 扩散水滴的类样例
class Drop { int x, y; // Coordinate (center of circle) int diameter; // Diameter of circle (unused == 0). void init( int ix, int iy ) { x = ix; y = iy; diameter = 1; } void spread() { if (diameter > 0) diameter += 1; } void show() { if (diameter > 0) { ellipse( x, y, diameter, diameter ); if (diameter > 500) diameter = 0; } } }
现在我们来看一下如何使用 Drop
类来构建一些利用用户输入的图形。清单 4 给出使用 Drop
类的应用程序。第一步是创建一个水滴数组(称为 drops
)。之后进行几个定义(drops
数组中的水滴数和工作索引)。在 setup
函数中,您可以创建显示窗口并初始化 drops
数组(所有直径为 0 或未使用)。draw
函数相当简单,因为水滴的核心功能在类本身内部(清单 3 中的 spread
和 show
)最后,添加 UI 部分,该部分允许用户定义水滴从何处开始。mousePressed
回调函数通过当前鼠标位置(目前有一个直径且是使用过的)初始化水滴,然后递增当前水滴索引。
清单 4. 构建多个用户定义水滴的应用程序
Drop[] drops; int numDrops = 30; int curDrop = 0; void setup() { size(400, 400); ellipseMode(CENTER); smooth(); drops = new Drop[numDrops]; for (int i = 0 ; i < numDrops ; i++) { drops[i] = new Drop(); drops[i].diameter = 0; } } void draw() { background(0); for (int i = 0 ; i < numDrops ; i++) { drops[i].spread(); drops[i].show(); } } void mousePressed() { drops[curDrop].init( mouseX, mouseY ); if (++curDrop == numDrops) curDrop = 0; }
您可以在图 1 中从 清单 3 和 清单 4 中看到应用程序输出。鼠标被点击了很多次,产生扩散的水滴,如图所示。
图 1. 清单 3 和清单 4 中应用程序的显示窗口

图像处理
Processing 提供有用且有趣的功能来进行图像处理。本节探究图像滤波、融合和对使用像素的用户定义图像处理的支持。
图像滤波
Processing 通过 filter
函数提供预装的图像处理功能。该函数向显示窗口直接应用一种滤波模式。清单 5 显示一个简单的 Processing 应用程序内滤波的使用,关联图 2 了解各种类型的输出。注意,在清单 5 中,您仅为 BLUR
执行滤波,但图 2 中显示了其他可能性(还有对应的代码)。
由于 filter
操作显示窗口的内容,您仅需提供滤波模式(要执行的滤波操作类型)和质量(即滤波模式的参数)。清单 5 首先声明 PImage
类型,这是存储图像的数据类型。接下来,在 setup
函数内,将您的特定图像加载到 PImage
数据类型(img1
)中。加载图像之后,您就知道了图像大小,然后据此设置窗口大小(使用 PImage
实例的 width
和 height
属性)。在 draw
函数内,通过对 image
的一个调用显示图像。image
函数要求图像显示在显示窗口,包括图像左上角的 x 和 y 坐标。(注意,这里也可以指定图像的宽度和高度。)最后,执行特定滤波模式 — 在本例中是 BLUR
模式。参见图 2 了解其他滤波选项,将结果与原始图像(下面图 5 中也有提供)进行对比。
清单 5. 一个简单的滤波应用程序
PImage img1; void setup() { img1 = loadImage("alaska1.png"); size(img1.width, img1.height); smooth(); } void draw() { image(img1, 0, 0); filter(BLUR, 2); }
如图 2 所示,Processing 提供一些封装的图像处理操作,常见于图像操作应用程序中。但是您也可以逐像素地操作图像。
图 2. 滤波操作示例

这里没有显示其他可能的滤波操作,包括:
OPAQUE
— 将 alpha 通道设置为不透明ERODE
— 基于提供的质量参数减少明亮区域DILATE
— 基于提供的质量参数增加命令区域
参考资料 部分提供有关其他 filter
模式的信息。
图像融合
图像可以融合,对于每幅图像(或图像区域)一次发生一个像素。该功能模拟 Adobe® Illustrator® 和 Photoshop® 中的一些功能。
清单 6 展示对两幅图像的 ADD
融合操作(如图 3 所示)。如清单中所示,使用 loadImage
加载两幅图像,然后使用 ADD
模式将 img2
与 img1
融合。在 blend
调用中,指定要与目标图像(img1
)融合的源图像(img2
)。接下来的 4 个参数是源图像、x 和 y 坐标,以及宽度和高度。紧接的参数是目标图像的左上角坐标和目标图像的宽度和高度。最后定义模式参数。在本例中,您请求一个 ADD
混合,它实现操作 dest_img_pixel +=
src_img_pixel*factor (maxed at 255)
。
清单 6. 融合图像
void setup() { size(237, 178); smooth(); } void draw() { PImage img1 = loadImage("alaska1.png"); PImage img2 = loadImage("alaska2.png"); img1.blend( img2, 0, 0, 237, 178, 0, 0, 237, 178, ADD ); image(img1, 0, 0); }
其他操作包括 BLEND
(不设上限)、SUBTRACT
、DARKEST
(采用最暗的像素)、LIGHTEST
(采用最浅的像素)、MULTIPLY
(暗化图像),等等。参考资料 部分提供其他混合模式信息的链接。
图 3. 混合操作的图像输出

像素阵列
最后的图像处理技术采用更手工的方法。在该模式下,您可以逐个操作每个像素。显示窗口由 color
类型的一个 1-D 阵列组成。显示图像后(如清单 7 中的 background
函数所示),您可以通过 loadPixels
函数在显示窗口中访问 pixels
阵列中的像素。loadPixels
函数将显示窗口加载到 pixels
阵列,同时 updatePixels
函数从 pixels
阵列更新显示窗口。
清单 7. 像素图的图像处理
void setup() { size(237, 178); smooth(); } void draw() { PImage img = loadImage("alaska2.png"); background(img); loadPixels(); for (int i = 0 ; i < img.width*img.height ; i++) { color p = pixels[i]; float r = red(p)/2; float g = green(p); float b = blue(p); pixels[i] = color(r, g, b); } updatePixels(); }
虽然显示窗口在像素阵列中,您可以通过各种方式操作它。清单 7 显示如何修改显示,即首先从每个像素(变量 p
)创建一个颜色类型实例。您可以使用 red
、green
和 blue
函数将该变量进一步分解为单个颜色。在本例中,您在显示窗口中将图像的红色部分对分,然后使用 color
函数将像素加载回来,该函数采用单个颜色并重新构建像素。清单 7 的前后对比如图 4 所示。
图 4. 对像素的手动图像处理

粒子群
我们看一下展示 Processing 功能的一个应用程序 — 特别是 OOP。该例来自数值优化和机器学习。
Particle swarms 是自然激发的一个优化技术。它使用大量候选解决方案(粒子),其移动由搜索空间中找到的最佳解决方案(粒子局部最佳解决方案和全局最佳解决方案)所引导。粒子群优化(PSO)很简单,且提供对搜索空间的有趣的可视化表示,使其成为探究数据可视化语言的理想之选(参见 参考资料 了解更多信息)。粒子群跨 2-D 空间移动,寻求全局最优效果。
PSO 实现
PSO 的 Processing 实现由两种类组成。第一种类是 Particle
类,它实现单个粒子。按照 PSO,每个粒子维护其当前位置、速度、当前和最佳适用性,以及最佳粒子解决方案(参见清单 8)。Particle
类提供大量方法来支持 PSO,包括一个构造函数(随机地将粒子放在搜索空间)、一个计算适用性的函数(在本例中是 sombrero
函数,其中适用性是 z
)、一个 update
函数(基于当前速度向量移动粒子)以及在搜索空间中显示粒子的一个 show
函数。还有其他三个 helper 函数,将粒子的元素呈现给用户(适用性以及 x 和 y 位置)。
清单 8. PSO 的 Particle
类
class Particle { float locX, locY; float velX = 0.0, velY = 0.0; float fitness = 0.0; float bestFitness = -10.0; float pbestX = 0.0, pbestY = 0.0; // Best particle solution float vMax = 10.0; // Max velocity float dt = 0.1; // Used to constrain changes to each particle Particle() { locX = random( dimension ); locY = random( dimension ); } void calculateFitness() { // Clip the particles if ((locX < 0) || (locX > dimension) || (locY < 0) || (locY > dimension)) fitness = 0; else { // Calculate fitness based on the sombrero function. float x = locX - (dimension / 2); float y = locY - (dimension / 2); float r = sqrt( (x*x) + (y*y) ); fitness = (sin(r)/r); } // Maintain the best particle solution if (fitness > bestFitness) { pbestX = locX; pbestY = locY; bestFitness = fitness; } } void update( float gbestX, float gbestY, float c1, float c2 ) { // Calculate particle.x velocity and new location velX = velX + (c1 * random(1) * (gbestX - locX)) + (c2 * random(1) * (pbestX - locX)); if (velX > vMax) velX = vMax; if (velX < -vMax) velX = -vMax; locX = locX + velX*dt; // Calculate particle.y velocity and new location velY = velY + (c1 * random(1) * (gbestY - locY)) + (c2 * random(1) * (pbestY - locY)); if (velY > vMax) velY = vMax; if (velY < -vMax) velY = -vMax; locY = locY + velY*dt; } void show() { point( (int)locX, (int)locY); } float pFitness() { return fitness; } float xLocation() { return locX; } float yLocation() { return locY; } }
接下来是粒子群的类(参见清单 9),该类维护一个 Particles
数组(在粒子群构造函数中创建和初始化)、当前的全局最佳解决方案(x 和 y 坐标)以及两个学习因子。学习因子度量粒子是否应围绕局部最佳解决方案(c2
)或全局最佳解决方案(c1
)成群移动(搜索)。每个因子指示将最佳解决方案应用于粒子的影响。
run
方法执行一步 PSO 模拟。首先,它计算每个粒子在粒子群中的适用性。然后查找全局最佳解决方案。有了这个信息之后,它调用 update
来移动粒子,调用 show
来展示粒子群中的粒子。
清单 9. PSO 的粒子群类
class Swarm { float gbestX = 0.0, gbestY = 0.0; // Global best solution float c1 = 0.1, c2 = 2.0; // Learning factors Particle swarm[]; Swarm() { swarm = new Particle[numParticles]; for (int i = 0 ; i < numParticles ; i++) { swarm[i] = new Particle(); } } void run() { // Calculate each particle's fitness for (int i = 0 ; i < numParticles ; i++) { swarm[i].calculateFitness(); } findGlobalBest(); // Update each particle and display it. for (int i = 0 ; i < numParticles ; i++) { swarm[i].update( gbestX, gbestY, c1, c2 ); swarm[i].show(); } } void findGlobalBest() { float fitness = -10.0; for (int i = 0 ; i < numParticles ; i++) { if (swarm[i].pFitness() > fitness) { gbestX = swarm[i].xLocation(); gbestY = swarm[i].yLocation(); fitness = swarm[i].pFitness(); } } } void showGlobalBest() { println("Best Particle Result: " + gbestX + " " + gbestY); } }
最后,使用这里定义的类的用户应用程序如清单 10 所示。这个应用程序为 PSO 定义一些可配置项,比如粒子数量、显示窗口的大小(dimension
)以及粒子群本身。setup
函数准备窗口和颜色,draw
执行对粒子群的调用,每 10 次迭代发出一次全局解决方案。由于该模拟使用 sombrero
函数实现优化,最佳效果是显示的中心。
清单 10. 驱动 PSO 的应用程序
// Particle Swarm Optimization int numParticles = 200; int iteration = 0; float dimension = 500; Swarm mySwarm = new Swarm(); void setup() { background(255); fill(0); size( int(dimension), int(dimension)); smooth(); } void draw() { background(255); // remove for trails mySwarm.run(); if ((iteration++ % 10) == 0) mySwarm.showGlobalBest(); }
下面两幅图展示 Processing 中 PSO 模拟的输出。图 5 显示 PSO 的时滞,而图 6 显示带有轨迹的 PSO,识别粒子的最优路径。
图 5. PSO 模拟的时滞

从图 6 中所示的轨迹可以很容易地看到粒子路径,因为它们向中心处的最优解决方案聚合。您可以看到一些路径中的闭环,这表示粒子在继续向全局最优效果移动之前云集了其最佳本地解决方案。
图 6. PSO 模拟的轨迹

应用程序转换
回顾 第 1 部分,Processing 代码在执行之前被转换为 Java 语言,使得将一个 Processing 应用程序转化为一个 Java applet 或应用程序变得很容易。要执行该转换,单击 Processing Development Environment (PDE) 中的 File,然后单击 Export 导出一个 applet,或单击 Export Application 导出一个 Java 应用程序。然后 Sketchbook 目录将包含代码和该操作的相关文件。清单 11 展示导出的 applet(applet 子目录)、导出的应用程序(出于特定目标的三个应用程序目录)以及源本身(pso.pde)。
清单 11. 导出后的 Processing Sketchbook 子目录
mtj@ubuntu:~/sketchbook/pso$ ls applet application.linux application.macosx application.windows pso.pde
在 applet 子目录中,您可以找到 Processing 源码、转换后的 Java 源码、JAR 和 index.html 文件样例,来查看结果。
前景
第二期探究了鼠标和键盘事件上下文中的 UI,探讨了 Processing 的 OOP 方法,并探究了大量其他 Processing 应用程序。下一期和最后一期将探究 Processing 的 3-D 功能,并开发使用网络进行数据收集的一个可视化应用程序。
相关主题
- 要了解有关 Processing 的更多信息,下载最新版本,并查找有趣的示例和教程,查看 Processing.org。开发了应用程序之后,一定要在 OpenProcessing.org 上共享它。
- 请务必阅读了解 filter 模式和 blend 选项。
- PSO 是一个相对较新的优化技术,使用粒子群来解决优化问题。您可以在 SwarmIntelligence.org 上进一步了解 PSO 和其他技术,比如白蚁群和蚁群。
- 访问 developerWorks Open source 专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用,以及我们 最受欢迎的文章和教程。
- 随时关注 developerWorks 技术活动和网络广播。
- 访问 developerWorks Open source 专区获得丰富的 how-to 信息、工具和项目更新以及最受欢迎的文章和教程,帮助您用开放源码技术进行开发,并将它们与 IBM 产品结合使用。
- 下载 IBM 产品评估试用版软件 或 IBM SOA Sandbox for People,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。