web analytics

Processing:视频

1. 本章的学习需要一个外部摄像头(如果你使用PC则还需安装版本7或以上的QuickTime播放机,并在自定义安装时勾选”QuickTime for Java“。另外,你还需安装vdig(数字视频转换机)在Win系统内捕捉视频)的参与配合。具体安装方式请查阅你自己的摄像头使用帮助。

2. 是的,现在你会发现比起PC,Mac真是轻松许多。Mac的本子自带iSight摄像头,预装QuickTime,Mac用户无需进行任何恼人步骤便可轻松进入下一步。

3. 要在p5内使用视频,遵循以下步骤:
◎ 导入video库:

import processing.video.*;

◎ 申明一个捕捉对象:

Capture video; 

◎ 初始化捕捉对象:

video = new Capture(this,320,240,30);

※ this — 自参考指令(self-referential statement),原书唧唧歪歪扯一大堆,暂不考试,记得这里放this就OK。
※ 320 — 摄像头捕捉视频的宽度
※ 240 — 摄像头捕捉视频的高度
※ 30 — 帧速率
◎ 从摄像头读取图像

4. 有两种方式从摄像头读取帧。两种方式遵循的基本原则都是:我们只在当一个新帧准备好被读取时,从摄像头读取一个图像。要查询一个图片是否可用,我们使用available()函数,它以是否有东西存在返回真或假。如果有,函数read()被呼叫并且摄像头的帧被读入内存。我们在draw()反复这么做,总是查询是否有新的图像可用。

    void draw()  {    
         if (video.available())  {    
              video.read();   
          }    
     }

5. 第二种方式,使用”事件(event)“的方式,即某个事件发生的时候便执行。比如鼠标点击或键盘输入。在视频这里,我们可以选择使用函数captureEvent(),使得每当一个捕捉动作发生时被执行,即,来自摄像头的一个新帧可用了。

void captureEvent(Capture video)  {    
   video.read();   
} 

6. 显示视频图像,当然,最简单的部分,你可以将Capture对象看做是一个始终处于变化的PImage,事实上,一个Capture对象可以以与PImage对象完全一致的方式来利用。

image(video,0,0); 

7. 把上边说的加一块:

// Step 1. 载入video库
import processing.video.*;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(320,240);
  
// Step 3. 通过构造器初始化Capture对象 Constructor
// 视频性质:320 x 240, @15 fps
video = new Capture(this,320,240,15);
}

void draw() {
  
  // 产看一个新帧是否已准备完毕
  if (video.available()) {
    // 如果是的话, Step 4. 从摄像头读取图像.
    video.read();
  }
  
 // Step 5. 显示视频图像.
 image(video,0,0);
}

8. 再重复一次,所有能在PIamge身上用的招数,我们全可以用在PImage上。只要我们从那个对象上read(),视频图像就将像我们操作它一样的更新。

// Step 1. 导入video库
import processing.video.*;

Capture video;

void setup() {
  size(320,240);
  video = new Capture(this,320,240,15);
  background(255);
}

void draw() {

  if (video.available()) {
    video.read();
  }

  // 用鼠标位置改变色调
  tint(mouseX,mouseY,255);

  //一个视频图像同样可以像PImage一样被改色及重设大小.
  image(video,0,0,mouseX,mouseY);   
}

9. ”图像“一课内的所有例子都可以在本章使用,以下为稍微改写后那个改变亮度的例子:

// Step 1. 导入video库
import processing.video.*;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(320,240);
  
  // Step 3. 用构造器初始化Capture对象
  // 图像属性:320 x 240, @15 fps
  video = new Capture(this,320,240,15); 
  background(0);
}

void draw() {
  
  // 检查是否有新帧可用
  if (video.available()) {
    // 如果有,读取它.
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  
  for (int x = 0; x < video.width; x++) {
    for (int y = 0; y < video.height; y++) {
      
      // 由2D网格计算出其1D位置
      int loc = x + y*video.width;
      
      // 从图像获取R,G,B值
      float r,g,b;
      r = red (video.pixels[loc]);
      g = green (video.pixels[loc]);
      b = blue (video.pixels[loc]);
      
      // 依距鼠标距离计算改变亮度的总量
      float maxdist = 100;// dist(0,0,width,height);
      float d = dist(x,y,mouseX,mouseY);
      float adjustbrightness = (maxdist-d)/maxdist;
      r *= adjustbrightness;
      g *= adjustbrightness;
      b *= adjustbrightness;
      
      // Constrain RGB to make sure they are within 0-255 color range
      r = constrain(r,0,255);
      g = constrain(g,0,255);
      b = constrain(b,0,255);
      
      // Make a new color and set pixel in the window
      color c = color(r,g,b);
      pixels[loc] = c;
    }
  }
  updatePixels();
}

10. 是的,再来一个,这是小丹出题ww改进版:

// Step 1. 导入video库
import processing.video.*;

int pointillize = 10;

// Step 2. 申明一个Capture对象
Capture video;

void setup() {
  size(200,130);
  
  // Step 3. 用构造器初始化Capture对象
  // 图像属性:320 x 240, @15 fps
  video = new Capture(this,200,130,15); 
  background(0);
}

void draw() {
  
  // 检查是否有新帧可用
  if (video.available()) {
    // 如果有,读取它.
    video.read();
  }
  
loadPixels();
// 与PImage的关键区别就是多了下边这行 
video.loadPixels();
  // 沿y轴每隔5个像素循环所有像素
  for (int y = 0; y < video.height; y+=5) {
    // 沿x轴每隔5个像素循环所有像素
    for (int x = 0; x < video.width+5; x+=5) {
      // 从一个2D网格计算1D位置
      int loc = x + y*video.width;
      // 以原图填色
      stroke(video.pixels[loc]);
      fill(video.pixels[loc]);
      // 画圆
      ellipse(x,y,3,3);
    }
  }
}

在你套上一课例子的时候,我们发现只需将”img“改为”video“,然后加上”video.loadPixels();“这么一行即可。只是注意updatePixels()的使用,有时候你加上它,当摄像头打开p5窗口什么都没有。我在p5官网参考里查到:如果你仅是从像素数组中读取像素,则没有必要呼叫updatePixels(),除非出现变化。

11. 显示录制好的视频大部分遵循与实时视频相同的结构。p5的视频库仅接收QuickTime格式的电影。
◎ 申明一个Movie对象替代Capture对象:

Movie movie;

◎ 初始化Movie对象:

movie = new Movie(this, "xxx.mov");

同样的,电影文件应当储存在你sketch目录里的data文件夹内
◎ 开始电影播放。同样有两个选择,play()播放电影一次,loop()持续循环播放。

movie.loop();

◎ 从电影读取帧。与捕捉完全一致。

     void draw()  {   
          if (movie.available())  {   
               movie.read();   
           }    
      }

或者

     void movieEvent(Movie movie)  {   
          movie.read();   
      }

◎ 显示电影

image(movie,0,0);

12. 全部放一块儿:

import processing.video.*;

// Step 1. 申明Movie对象
Movie movie; 

void setup() {
  size(320,240);
  
  // Step 2. 初始化Movie对象
  // 电影文件应放在data文件夹下
  movie = new Movie(this, "xxx.mov"); 
  
  // Step 3. 开始电影播放
  movie.loop();
}

// Step 4. 从电影读取新帧
void movieEvent(Movie movie) {
  movie.read();
}

void draw() {
  // Step 5. 显示电影.
  image(movie,0,0);
}

是的,运行上边的代码,不出意外的话,你将得到错误:"ArrayIndexOutOfBoundsException: Coordinate out of bounds!" 。然后错误高亮框选在“image(myMovie, 0, 0);”上。哦也,bug!于是我查找p5讨论区,被这个打败的同学还真不少。最后终于发现一个解决之道,即:用“size(160,120,P2D);”替代 “size(160,120);”。这样修改后,mov文件便可正常显示了。按理说P2D应是留空默认引数,至于在这里为何非要特殊申明才能使程序运转正常,小弟就不得而知了。

13. 当然,p5不可能只能做播放电影的事,下边这个例子使用jump()(跳到影片内特定的点)和duration()(返回电影的长度)函数实现了前进后退的功能:

// 如果mouseX是0, 到影片开始   
// 如果mouseX是宽度, 到影片末尾   
// 所有其他的位置,在开始与末尾间徘徊   

import processing.video.*;

Movie movie;

void setup() {
  size(160,120,P2D);
  movie = new Movie(this, "xxx.mov");
}

void draw() {

  // mouseX相对宽度的比率
  float ratio = mouseX / (float) width;

  // jump()函数允许你快速的跳到影片特定的时间点. 
  // duration()以秒为单位返回影片的长度.  
  movie.jump(ratio*movie.duration()); 
  
  // 读取帧
  movie.read(); 
  // 显示帧
  image(movie,0,0); 
}

注意,这里你的size必须严格按照影片真实尺寸写,否则程序会崩溃掉(在本章里p5真是bug频出啊..);还有一种可能是size不能写太大,太大会造成程序崩溃(回头我真实测试一下便知)。

14. 我们现在要做一个玩意,用80×60像素捕捉视频,然后渲染到640×480的窗口内。首先,我们来做一个布满矩形的网格:

// 网格内每一格的大小,依视频和窗口的大小成比例缩放。
// 80 * 8 = 640
// 60 * 8 = 480
int videoScale = 8;

// 我们系统内行和列的数目
int cols, rows;

void setup() {
  size(640,480);
  
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
}

void draw() {
  
  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {
      
      // 在 (x,y) 按比例放大绘制矩形
      int x = i*videoScale;
      int y = j*videoScale;
      fill(255);
      stroke(0);
      // 对于每一行每一列, 一个矩形被绘制于一个 (x,y) 位置,并由videoScale限制大小.
      rect(x,y,videoScale,videoScale); 
    }
  }
}

现在,我们便可捕捉一个80×60的视频了。这很有用,因为相比起来,640×480视频捕捉的速度就很慢。我们仅仅想用我们sketch需要的分辨率来捕捉颜色信息。

import processing.video.*;

// 网格内每一格的大小,依视频和窗口的大小成比例缩放。
int videoScale = 8;
// 我们系统内行和列的数目
int cols, rows;
// 承载变量的Capture对象
Capture video;

void setup() {
  size(640,480);
  
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
  video = new Capture(this,cols,rows,30);
}

void draw() {
  // 从摄像头读取图像
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  
  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {
      
      // 我们在哪?
      int x = i*videoScale;
      int y = j*videoScale;
      // 在pixel数组内寻找合适的颜色
      color c = video.pixels[i + j*video.width];
      fill(c);
      stroke(0);
      rect(x,y,videoScale,videoScale);
    }
  }
}

15. 在下例中,我们只使用黑与白。视频中较亮的部分较大,较暗的则较小。

// 每个源于视频源的像素都基于亮度被绘制为一个矩形

import processing.video.*;

// 每格的大小
int videoScale = 10;
// 我们系统内行和列的数量
int cols, rows;
// Capture对象
Capture video;

void setup() {
  size(640,480);
  // 初始化行和列
  cols = width/videoScale;
  rows = height/videoScale;
  smooth();
  // 构造Capture对象
  video = new Capture(this,cols,rows,15);
}

void draw() {
  if (video.available()) {
    video.read();
  }
  background(0);

  video.loadPixels();

  // 开始循环列
  for (int i = 0; i < cols; i++) {
    // 开始循环行
    for (int j = 0; j < rows; j++) {

      // 我们在哪?
      int x = i*videoScale;
      int y = j*videoScale;

      // 反向x使其镜像图像
      // 为了镜像图像, 列基于如下公式反向:
      // 镜像列 = 宽度 - 列 - 1
      int loc = (video.width - i - 1) + j*video.width;

      // 每个矩形被染白,并基于亮度确定大小
      color c = video.pixels[loc];

      // 一个矩形的大小被计算为一个像素亮度的函数. 
      // 亮的十大的,暗的是小的.
      float sz = (brightness(c)/255.0)*videoScale; 
      rectMode(CENTER);
      fill(255);
      noStroke();
      rect(x + videoScale/2,y + videoScale/2,sz,sz);
    }
  }
}

那个镜像图像是干啥吃的?常规的视频显示是与你的动作反向的,比如你的头向右移动,则视频窗口内显示你的头便向左移动。如果你在向右摇头时希望窗口内的影像随你一同向右,你就需要加上:

// 设i为列,j为行
int loc = (video.width - i - 1) + j*video.width;
color c = video.pixels[loc];
fill(c);

有心的可以自己算算为什么会这样子,没心没肺只想用的,记住这三行即可。

16. 思考软件镜像(software mirrors)常常通过两步。这将同样帮助我们比更明显的将像素映射到网格的形状上想得更远。
第一步:开发一个可以覆盖整个窗口的有趣模式(pattern)
第二步:查找视频像素渲染那个模式
比方说第一步,创建一个在窗口内胡乱书写的随机线。
◎ 以位于萤幕中心的x和y作为起始坐标
◎ 无限重复以下:
— 选取一个新的x和y
— 从新的(x,y)到旧的(x,y)画一条直线
— 保存新的(x,y)

// 两个全局变量
float x;
float y;

void setup() {
  size(320,240);
  smooth();
  background(255);
  // 从中心开始x和y
  x = width/2;
  y = height/2;
}

void draw() {
  // 通过当前(x,y)位置加减一个随机数获取新的x,y位置. 
  // 新位置被限制于窗口像素内.  
  float newx = constrain(x + random(-20,20),0,width);
  float newy = constrain(y + random(-20,20),0,height);
  
  // 由x,y到newx,newy绘制一条线
  stroke(0);
  strokeWeight(4);
  line(x,y,newx,newy);
  
  // 我们将新位置存入(x,y)以无限循环这个程序.
  x = newx; 
  y = newy;
}

ok,现在让我们用视频图像的颜色来替代stroke():

import processing.video.*;

// 两个全局变量
float x;
float y;

// 承载变量的Capture对象
Capture video;

void setup() {
  size(320,240);
  smooth();
  background(255);
  
  // 从中心开始x和y
  x = width/2;
  y = height/2;
  
  // 开始捕捉程序
  // 如果窗口更大 (比如说 800 x 600), 我们可能会要引入一个videoScale变量,因此我们便无需捕捉如此大的一个图像了.
  video = new Capture(this,width,height,15); 
}

void draw() {
  // 从摄像头读取图像
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  
  // 选取一个新的x和y
  float newx = constrain(x + random(-20,20),0,width-1);
  float newy = constrain(y + random(-20,20),0,height-1);
  
  // 找到线的中点
  int midx = int((newx + x) / 2);
  int midy = int((newy + y) / 2);
  
  // 从事品选取颜色, 反向x
  color c = video.pixels[(width-1-midx) + midy*video.width];
  
  // 由x,y到newx,newy绘制一条线
  stroke(c);
  strokeWeight(4);
  line(x,y,newx,newy);
  
  // 将newx, newy存入x,y
  x = newx;
  y = newy;
}

17. 摄像头为我们提供了大量的信息,下边我们将试着将其用作传感器。我们在其他例子里已经看到,一个单独像素的亮度值可由brightness()函数检索,返回一个从0~255的浮点值。下边这行像素返回视频图像内第一个相素的值:

float brightness = brightness(video.pixels[0]);

当然,我们还可以计算总体亮度:

   video.loadPixels(); 
   // 由总体亮度为0开始 
   float totalBrightness = 0; 
   // 求出每个像素的亮度总和 
   for (int i = 0; i < video.pixels.length; i++ ) {  
        color c = video.pixels[i]; 
        totalBrightness += brightness(c); 
    }  

   // 计算平均值
   // 平均亮度=总亮度/总像素
   float averageBrightness = totalBrightness / video.pixels.length;     
   // 以平均亮度显示背景 
   background(averageBrightness);

这是一个基于视频的算法的很好的例子。总之,一个视频图像不仅是一个颜色的集合,而且还是一个具有空间导向的颜色的集合。通过开发搜索像素和识别模式的算法,我们便可开始开发更高级的计算机视觉程序。

18. 跟踪最亮的颜色是一个很好的起步。想象一个昏暗的房间里一个移动的光源。在我们接下来将要学习的技术中,这个灯光将被作为鼠标的替代,以成为一种互动的形式。首先,我们将测试如何贯穿一个图像然后找到最亮像素的x,y位置。

    // 程序之初的记录为0   
    float worldRecord = 0.0;   
    // 哪个像素将赢得奖牌?   
    int xRecordHolder = 0;  
    int yRecordHolder = 0;  
    for (int x = 0; x < video.width; x++) {    
         for (int y = 0; y < video.height; y++ )  {    
              // 当前亮度是什么   
              int loc = x + y*video.width;   
              float currentBrightness = brightness(video.pixels[loc]);   
              if (currentBrightness > worldRecord)  {    
                   // 设置一个新纪录   
                   worldRecord = currentBrightness;   
                   // 这个像素保持记录! 
                   // 当我们发现了新的最亮的像素,我们必须保存下数组中那个像素的(x,y)位置以备之后使用。  
                   xRecordHolder = x;   
                   yRecordHolder = y;   
               }    
          }    
     }

现在我们来做抓取一个特定的颜色,而不是简单的亮度。比如,我们可以寻找视频中最绿或最黄的部分。为了做这类分析,我们需要开发一个比较颜色的方法论。让我们创建两个颜色,c1和c2.

color c1 = color(255,100,50);   
color c2 = color(150,255,0);

颜色们仅能比较它们各自红、绿、蓝的成分,所以我们必须首先分离这些值:

float r1 = red(c1);
float g1 = green(c1);
float b1 = blue(c1);
float r2 = red(c2);
float g2 = green(c2);
float b2 = blue(c2);

ok,现在,对比它们,有一个策略是获取它们差值总数的绝对值:

float diff = abs(r1-r2) + abs(g1-g2) + abs(b1-b2);

还有一个更精确的方法是比较颜色之间的距离,是的,我说真的。我们可以将颜色想象为三维空间中的一个点,用rgb替代xyz。如果两个颜色在这个颜色空间相近,则它们便相似;较远,则它们不同。

float diff = dist(r1,g1,b1,r2,g2,b2);

尽管更精确,但因为dist()函数在其计算中包含了一个平方根,因此它的计算要慢于绝对值的方式。一个解决方法是不用平方根写你自己的颜色距离:

colorDistance = (r1-r 2)*(r1-r2)+(g1-g2)* 
(g1-g2)+(b1-b2)*(b1-b2)

19. 比如说,要找到一个图像最红的像素,也就是找到最接近红色的颜色——(255,0,0)。下例就是一个颜色抓取的展示,用户可以用鼠标自行点取想要捕捉的颜色:

import processing.video.*;

// 捕捉设备的变量
Capture video;

// 一个给我们要寻找的颜色的变量.
color trackColor; 

void setup() {
  size(320,240);
  video = new Capture(this,width,height,15);
  // 从抓红色开始
  trackColor = color(255,0,0);
  smooth();
}

void draw() {
  // 捕捉并显示视频
  if (video.available()) {
    video.read();
  }
  video.loadPixels();
  image(video,0,0);

  // 在我们开始搜索前, 为最接近颜色设置的“世界纪录”被设置为一个很高的值以此容易击败第一个像素。
  float worldRecord = 500; 

  // 最接近颜色的XY坐标
  int closestX = 0;
  int closestY = 0;

  // 开始在整个数组内循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      int loc = x + y*video.width;
      // 当前颜色是什么
      color currentColor = video.pixels[loc];
      float r1 = red(currentColor);
      float g1 = green(currentColor);
      float b1 = blue(currentColor);
      float r2 = red(trackColor);
      float g2 = green(trackColor);
      float b2 = blue(trackColor);

      // 使用欧氏距离(euclidean distance)比较颜色
      float d = dist(r1,g1,b1,r2,g2,b2);

      // 如果当前颜色比“世界纪录”颜色更类似记录的颜色,保存当前位置和当前差值。
      if (d < worldRecord) {
        worldRecord = d;
        closestX = x;
        closestY = y;
      }
    }
  }

  // 我们仅仅在颜色距离小于10的时候才认为颜色被找到了. 
  // 阀值10是任意的,你可以基于你要求的抓取精度来任意给这个值。
  if (worldRecord < 10) { 
    // 在抓取到的像素位置画一个圈
    fill(trackColor);
    strokeWeight(4.0);
    stroke(0);
    ellipse(closestX,closestY,16,16);
  }
}

void mousePressed() {
  // 当鼠标在trackColor变量内点击时,保存那个颜色
  int loc = mouseX + mouseY*video.width;
  trackColor = video.pixels[loc];
}

20. 一个延伸运用,效果很奇妙,代码很长。。,基本做法时,在包含鼠标互动的例子里,用上例中的颜色捕捉替代鼠标。哦呀,今天头晕得很,这里边貌似错综复杂的东西很多,我现在大脑一片空白,去看部傻逼大片,明日继续吧~

背景移除
背景移除

21. ok,失意归来,继续,可我今天感觉也有点困,昨晚没睡好。是的,状况总是频出不穷,借口也总是那么容易找,,今天我们要学习一个技术——背景移除。我们准备这么干:
◎ 记录一个背景图像
◎ 在当前视频帧呢检查每一个像素。如果它与背景图像相应像素的差别很大,那么证明它为前景像素。如果不是,则它为背景像素。仅显示前景像素。
为证明以上算法,我们将以这样来显示它:移除背景图像,并用绿色填充背景。
21.1. 第一步是记忆背景,背景其实是视频的一个快照。因为视频图像总是始终出于变化中,我们必须将视频一帧的一个副本存入一个单独的PImage对象中。

PImage backgroundImage; 

void setup() {   
   backgroundImage = createImage(video.width,video.height,RGB); 
}

当backgroundImage被创建后,它是一副与视频尺寸一致大小的空图。通常这样的形式是没有用的,所以当我们需要记忆一个背景的时候,我们需要从摄像头copy一个图像进入背景图像里。让我们在鼠标按下时这么做:

void mousePressed()  {    
  // 将当前视频帧复制到backgroundImage对象
  // copy()允许你从一个图像到另一个图像复制像素。注意在新像素被复制后应当呼叫updatePixels()
  // 注意copy有五个引数:   
  // 源图像   
  // 被复制源区域的x,y,宽度和高度 
  // 复制目标的x,y,宽度和高度
  backgroundImage.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);   
  backgroundImage.updatePixels(); 
}

21.2. 一旦我们的背景图像被保存,我们可以在当前帧循环其所有像素并用距离计算比较它们与背景。对于任何给定的像素(x,y),我们使用如下代码:

int loc = x + y*video.width;  // Step 1, 1D位置是什么    
color fgColor = video.pixels[loc];  // Step 2, 前景色是什么    
color bgColor = backgroundImage.pixels[loc];  // Step 3, 背景色是什么    

// Step 4, 比较前景与背景色 
float r1 = red(fgColor); float g1 = green(fgColor); float b1 = blue(fgColor);   
float r2 = red(bgColor); float g2 = green(bgColor); float b2 = blue(bgColor);   
float diff  =  dist(r1,g1,b1,r2,g2,b2);   

// Step 5, 前景色相比背景色显得不同吗    
if (diff > threshold)  {    
   // 如果是,显示前景色    
   pixels[loc] = fgColor;   
}  else  {    
   // 如果不是,显示绿色    
   pixels[loc] = color(0,255,0);   
}

21.3. 以上代码假设了一个名为”阀值“的变量。阀值越低,对于一个像素来讲就越容易被作为前景像素。它不需要与背景像素有很大的差异。以下是以阀值作为一个全局变量的例子:

// 点击鼠标以记录当前背景图像
import processing.video.*;

// 捕捉设备的变量
Capture video;

// 保存背景
PImage backgroundImage;

// 差异有多大才能将一个像素作为前景像素
float threshold = 20;

void setup() {
  size(320,240);
  video = new Capture(this, width, height, 30);
  // 创建一个与视频大小一致的的空白图像
  backgroundImage = createImage(video.width,video.height,RGB);
}

void draw() {
  // 捕捉视频
  if (video.available()) {
    video.read();
  }
  
  // 我们正看着视频的像素, 记录的backgroundImage的像素, 同时也进入显示的像素. 
  // 因此我们必须loadPixels()全体!
  loadPixels();
  video.loadPixels(); 
  backgroundImage.loadPixels();
  
  // 开始在每个像素间循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      int loc = x + y*video.width;  // Step 1, 1D位置是什么    
      color fgColor = video.pixels[loc];  // Step 2, 前景色是什么    
      color bgColor = backgroundImage.pixels[loc];  // Step 3, 背景色是什么 
      
      // Step 4, compare the foreground and background color
      float r1 = red(fgColor);
      float g1 = green(fgColor);
      float b1 = blue(fgColor);
      float r2 = red(bgColor);
      float g2 = green(bgColor);
      float b2 = blue(bgColor);
      float diff = dist(r1,g1,b1,r2,g2,b2);
      
      // Step 5, 前景色相比背景色显得不同吗
      if (diff > threshold) {
        // 如果是,显示前景色
        pixels[loc] = fgColor;
      } else {
        // 如果不是,显示绿色
        pixels[loc] = color(0,255,0); // 我们当然可以在绿色之外让背景显示为其他东西!
      }
    }
  }
  updatePixels();
}

void mousePressed() {
  // 将当前视频帧复制到backgroundImage对象
  // copy()允许你从一个图像到另一个图像复制像素。注意在新像素被复制后应当呼叫updatePixels()
  // 注意copy有五个引数:   
  // 源图像   
  // 被复制源区域的x,y,宽度和高度 
  // 复制目标的x,y,宽度和高度
  backgroundImage.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);
  backgroundImage.updatePixels();
}

运行以上代码,你需要先位于摄像头范围之外,然后鼠标点击选取背景,然后你再进去。当然,这方式不可能100%的严丝合缝,而且很多时候还会达不到抹掉背景的效果,试着调整你的位置、背景的复杂度以及阀值。是的,之前也说了,你可以用其他东西替换掉背景,由于这招也不是那么好使,所以有兴趣的可以在这里看如何用一副图换掉背景。

22. 接下来是动作侦测。Daniel说今天将是一个“happy day”。为什么呢?因为动作侦测与背景移除使用的是同一个算法。动作是如何产生的?在一个像素颜色与上一帧有极大变化的时候产生。唯一的不同,与背景移除仅记录一次背景相比,我们需要持续的记录视频的上一帧。以下代码几乎与背景移除的例子相同,但有一个重要的变化——当一个新帧可以使用的时候,视频上一帧持续的被存储下来。

    // 捕捉视频   
    if (video.available())  {    
      // 为动作侦测保存上一帧!!   
      prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height); 
      video.read();   
     }

23. 看例子:

import processing.video.*;
// 捕捉设备变量
Capture video;
// 前一帧
PImage prevFrame;
// 一个像素要如何不同才能被界定为一个“动作”像素
float threshold = 50;

void setup() {
  size(320,240);
  video = new Capture(this, width, height, 30);
  //创建一个与视频大小相同的空图
  prevFrame = createImage(video.width,video.height,RGB);
}

void draw() {
  
  // 捕捉视频
  if (video.available()) {
    // 为动作侦测保存上一帧!!
    prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height); // 在我们读取新帧前, 始终保存上一帧作为对比!
    prevFrame.updatePixels();
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  prevFrame.loadPixels();
  
  // 开始在每一帧间循环
  for (int x = 0; x < video.width; x ++ ) {
    for (int y = 0; y < video.height; y ++ ) {
      
      int loc = x + y*video.width;            // Step 1, 1D位置是什么
      color current = video.pixels[loc];      // Step 2, 当前颜色是什么
      color previous = prevFrame.pixels[loc]; // Step 3, 之前的颜色是什么
      
      // Step 4, 比较严色 (之前的 vs. 现在的)
      float r1 = red(current); float g1 = green(current); float b1 = blue(current);
      float r2 = red(previous); float g2 = green(previous); float b2 = blue(previous);
      float diff = dist(r1,g1,b1,r2,g2,b2);
      
      // Step 5, 颜色的差别有多大?
      // 如果那一点的像素改变了, 那么那个像素就运动了.
      if (diff > threshold) { 
        // 如果动了,显示黑色
        pixels[loc] = color(0);
      } else {
        // 如果没动,显示白色
        pixels[loc] = color(255);
      }
    }
  }
  updatePixels();
}

24. 如果我们要知道在一个空间内动作的总量,该怎么办?还记得上边那个计算平均亮度的例子吗?
平均亮度 = 总亮度/像素总数
同样的,我们也可以这么计算平均动作:
平均动作 = 总动作/像素总数

25. 下例显示了一个根据动作平均量变化颜色和大小的圆。再次注意,你不需要为了分析视频而display(显示)它。

import processing.video.*;

// 捕捉设备变量
Capture video;
// 前一帧
PImage prevFrame;

// 一个像素要如何不同才能被界定为一个“动作”像素
float threshold = 50;

void setup() {
  size(320,240);
  // 使用默认捕捉设备
  video = new Capture(this, width, height, 15);
  // 创建一个与视频大小相同的空图
  prevFrame = createImage(video.width,video.height,RGB);
}

void draw() {
  background(0);
  
  // 你不需要显示以分析它!
  image(video,0,0);
  
  // Capture video
  if (video.available()) {
    // 为动作侦测保存上一帧!!
    prevFrame.copy(video,0,0,video.width,video.height,0,0,video.width,video.height);
    prevFrame.updatePixels();
    video.read();
  }
  
  loadPixels();
  video.loadPixels();
  prevFrame.loadPixels();
  
  // 开始在每个像素间循环
  // 从总数为0开始
  float totalMotion = 0;
  
  // 计算像素的亮度总和
  for (int i = 0; i < video.pixels.length; i ++ ) {
    // Step 2, 当前颜色是什么
    color current = video.pixels[i];    
    // Step 3, 之前颜色是什么
    color previous = prevFrame.pixels[i];
    
    // Step 4, 对比颜色 (之前的 vs. 现在的)
    float r1 = red(current); 
    float g1 = green(current);
    float b1 = blue(current);
    float r2 = red(previous); 
    float g2 = green(previous);
    float b2 = blue(previous);
    
    // 独立像素的运动即其之前和现在颜色的差异.
    float diff = dist(r1,g1,b1,r2,g2,b2);
    // totalMotion(动作总和)即所有颜色差异的总和. 
    totalMotion += diff; 
  }
  
  // averageMotion(动作均值).
  float avgMotion = totalMotion / video.pixels.length; 
  
  // 基于动作均值绘制一个圆形
  smooth();
  stroke(255,0,0);
  fill(0);
  float r = avgMotion*2;
  ellipse(width/2,height/2,r,r);
}

26. 小丹出题:创建一个侦测动作平均位置的sketch。您能搞一个跟伴随你手的甩动的圆吗?官网没答案。。。不过我相信我清理一下头脑应该还是能做出来的,不过现在我们还是暂时继续下边的内容吧。

27. 出于安全考虑,例如使用摄像头这样的事请,在一个web applet中常常是禁用的,如果你不得不这么干的话,请参照这里

分享给你的网络

ww
ww

9 Comments

  1. 请问,“背景移除”这段code里面,我明明是把您的那段code直接粘贴到processing里面的,可是它无法运行,显示的错误是“expecting EOF,found it”。

  2. super tut !!!!! 写的很好啊,很感谢!!!这一类的中文教程还真缺啊,给作者加油! 另外请问作者是计算机学习背景还是艺术学习背景呢?我是做新媒体艺术的,希望和您这样的人多交流

  3. 打开摄像头不是需要video.start()么 没有添加这条语句还可以打开摄像头么

  4. 除了把rgb换成hsl 还怎么才能精确的捕捉到颜色呢 拿着东西移动的时候颜色就不知道该捕捉谁了

  5. 你好,我练习了教程的最后一个案例,但是我的processing必须要写video.start()才能使用,而且写了这句代码后就没办法让摄像头只在后台运行了,为什么?难道是版本问题?

Leave a Reply

Your email address will not be published. Required fields are marked *