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中常常是禁用的,如果你不得不这么干的话,请参照这里。
请问,“背景移除”这段code里面,我明明是把您的那段code直接粘贴到processing里面的,可是它无法运行,显示的错误是“expecting EOF,found it”。
出问题的是哪一行代码?(p5 会用背景色显示)
super tut !!!!! 写的很好啊,很感谢!!!这一类的中文教程还真缺啊,给作者加油! 另外请问作者是计算机学习背景还是艺术学习背景呢?我是做新媒体艺术的,希望和您这样的人多交流
谢谢。关于我,请看本站 about 页面。
打开摄像头不是需要video.start()么 没有添加这条语句还可以打开摄像头么
@yi, 可以
除了把rgb换成hsl 还怎么才能精确的捕捉到颜色呢 拿着东西移动的时候颜色就不知道该捕捉谁了
你好,我练习了教程的最后一个案例,但是我的processing必须要写video.start()才能使用,而且写了这句代码后就没办法让摄像头只在后台运行了,为什么?难道是版本问题?
应该是版本问题,我做演示的版本距今很早了,参考这个链接,不知道是否对你有帮助