内容


用 Processing 进行数据可视化,第 2 部分

使用接口、对象、图像和应用程序的中间数据可视化

系列内容:

此内容是该系列 # 部分中的第 # 部分: 用 Processing 进行数据可视化,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:用 Processing 进行数据可视化,第 2 部分

敬请期待该系列的后续内容。

在本系列的 第 1 部分 中,您了解了 Processing 作为可视化语言和环境有多么强大。本期继续探究 Processing,首先回顾其用户交互功能。

键盘和鼠标

Processing 不仅创建一种可视化数据的简单方法,而且支持来自鼠标和键盘的用户输入。Processing 通过一组函数和回调实现这一点,在获得用户输入时通知 Processing 程序。

键盘事件

Processing 提供一小组键盘函数,在某个键被按下或释放时通知 Processing 应用程序。您还可以进一步解析该输入,在事件中将特殊字符呈现给用户。

可使用 keyPressed 函数来表示一个按键操作。您可以在您的应用程序中定义该函数,且当按键操作发生时该函数被调用。然后使用回调函数中一个名为 key 的特殊变量来识别用户按下的那个键。类似地,当释放一个键时,您可以使用 keyReleased 函数来捕捉这一事件。注意,两个函数都产生相同的信息,但是允许您定义何时触发您的操作。

清单 1 显示 keyPressedkeyReleased 函数。不管在哪个函数中,程序都可以解析两种用户按键:未编码的 ASCII 字符和需要编码的非 ASCII 字符(比如箭头键)。对于编码的字符,Processing 将 key 变量设置为 CODED 令牌,以表明必须检查另一个名为 keyCode 的特殊变量。因此,如果 key 不是 CODEDkey 变量包含按键。如果 keyCODEDkeyCode 变量包含实际字符:UPDOWNLEFTRIGHTALTCONTROLSHIFT

清单 1. keyPressedkeyReleased 回调
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 变量(LEFTCENTERRIGHT)识别特定鼠标按钮。当释放鼠标按钮时,mouseReleased 回调函数被调用。mouseClicked(您可以使用的另一个回调函数)可同时调用 mousePressedmouseReleased。每次鼠标移动但未点击鼠标按钮时mouseMoved 函数被调用。最后,当鼠标移动且点击鼠标按钮时 mouseDragged 函数被调用。

另外有大量特殊变量可供使用。首先,mouseXmouseY 变量包含鼠标的当前位置。您可以使用 pmouseXpmouseY 变量(从前一帧)捕获鼠标的前一位置。您可以使用 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 空间中具有坐标(xy)和直径的一个圆。您可以使用 xy 坐标以及值为 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 中的 spreadshow)最后,添加 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 实例的 widthheight 属性)。在 draw 函数内,通过对 image 的一个调用显示图像。image 函数要求图像显示在显示窗口,包括图像左上角的 xy 坐标。(注意,这里也可以指定图像的宽度和高度。)最后,执行特定滤波模式 — 在本例中是 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. 滤波操作示例
照片合成显示 BLUR、GRAY 和 INVERT 等各种滤波的结果
照片合成显示 BLUR、GRAY 和 INVERT 等各种滤波的结果

这里没有显示其他可能的滤波操作,包括:

  • OPAQUE— 将 alpha 通道设置为不透明
  • ERODE— 基于提供的质量参数减少明亮区域
  • DILATE— 基于提供的质量参数增加命令区域

参考资料 部分提供有关其他 filter 模式的信息。

图像融合

图像可以融合,对于每幅图像(或图像区域)一次发生一个像素。该功能模拟 Adobe® Illustrator® 和 Photoshop® 中的一些功能。

清单 6 展示对两幅图像的 ADD 融合操作(如图 3 所示)。如清单中所示,使用 loadImage 加载两幅图像,然后使用 ADD 模式将 img2img1 融合。在 blend 调用中,指定要与目标图像(img1)融合的源图像(img2)。接下来的 4 个参数是源图像、xy 坐标,以及宽度和高度。紧接的参数是目标图像的左上角坐标和目标图像的宽度和高度。最后定义模式参数。在本例中,您请求一个 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(不设上限)、SUBTRACTDARKEST(采用最暗的像素)、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)创建一个颜色类型实例。您可以使用 redgreenblue 函数将该变量进一步分解为单个颜色。在本例中,您在显示窗口中将图像的红色部分对分,然后使用 color 函数将像素加载回来,该函数采用单个颜色并重新构建像素。清单 7 的前后对比如图 4 所示。

图 4. 对像素的手动图像处理
合成图展示原始海鸥图以及稀释红色后的蓝色图片
合成图展示原始海鸥图以及稀释红色后的蓝色图片

粒子群

我们看一下展示 Processing 功能的一个应用程序 — 特别是 OOP。该例来自数值优化和机器学习。

Particle swarms 是自然激发的一个优化技术。它使用大量候选解决方案(粒子),其移动由搜索空间中找到的最佳解决方案(粒子局部最佳解决方案和全局最佳解决方案)所引导。粒子群优化(PSO)很简单,且提供对搜索空间的有趣的可视化表示,使其成为探究数据可视化语言的理想之选(参见 参考资料 了解更多信息)。粒子群跨 2-D 空间移动,寻求全局最优效果。

PSO 实现

PSO 的 Processing 实现由两种类组成。第一种类是 Particle 类,它实现单个粒子。按照 PSO,每个粒子维护其当前位置、速度、当前和最佳适用性,以及最佳粒子解决方案(参见清单 8)。Particle 类提供大量方法来支持 PSO,包括一个构造函数(随机地将粒子放在搜索空间)、一个计算适用性的函数(在本例中是 sombrero 函数,其中适用性是 z)、一个 update 函数(基于当前速度向量移动粒子)以及在搜索空间中显示粒子的一个 show 函数。还有其他三个 helper 函数,将粒子的元素呈现给用户(适用性以及 xy 位置)。

清单 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 数组(在粒子群构造函数中创建和初始化)、当前的全局最佳解决方案(xy 坐标)以及两个学习因子。学习因子度量粒子是否应围绕局部最佳解决方案(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 功能,并开发使用网络进行数据收集的一个可视化应用程序。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=628149
ArticleTitle=用 Processing 进行数据可视化,第 2 部分: 使用接口、对象、图像和应用程序的中间数据可视化
publish-date=02212011