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()才能使用,而且写了这句代码后就没办法让摄像头只在后台运行了,为什么?难道是版本问题?
应该是版本问题,我做演示的版本距今很早了,参考这个链接,不知道是否对你有帮助