视频
Daniel Shiffman
现场视频
既然您已经探索了处理中的静态图像,您就可以继续移动图像,特别是从实时相机 (以及后来录制的电影)。首先,我将介绍导入视频库和使用捕获类显示实时视频的基本步骤。
步骤 1.导入处理视频库。
尽管视频库是由处理基金会开发和维护的,但由于其大小,仍然必须通过贡献管理器单独下载。视频和声音库需要通过库管理器下载。从草图菜单中的 “导入库.” 子菜单中选择 “添加库.”。
安装库后,下一步是在代码中导入库。这是通过选择菜单选项 “草图” → “导入库” → “视频”,或者通过键入以下代码行 (该代码应位于草图的最顶部) 来完成的:
import processing.video.*;
使用 “导入库” 菜单选项只能自动将该行插入代码,因此手动键入完全等效。
步骤 2.声明捕获对象。
您最近已经了解了如何从处理语言 (如 PShape 和 PImage) 中内置的类创建对象。应该注意的是,这两个类都是 processing.core 库的一部分,因此不需要导入语句。Processing.video 库内部有两个有用的类 -- 实时视频的捕获和录制视频的电影。在这一步中,我将声明一个捕获对象。
Capture video;
步骤 3.初始化捕获对象。
捕获对象 “视频” 就像任何其他对象一样 -- 要构造一个对象,你需要使用新的运算符,后跟构造函数。对于捕获对象,此代码通常出现在 setup() 中。
video = new Capture();
上面的代码行缺少构造函数的适当参数。记住,这不是你自己写的课程,所以如果不咨询在线参考,就无法知道括号之间需要什么。
引用将显示有几种方法来调用捕获构造函数。调用构造函数的典型方法是使用三个参数:
void setup() {
video = new Capture(this, 320, 240);
}
让我们浏览一下捕获构造函数中使用的参数。
- this: 如果你对这意味着什么感到困惑,你并不孤单。从技术上讲,这是指出现 this 这个词的类的实例。不幸的是,这样的定义很可能会引起头部旋转。更好的思考方式是一种自我参照的陈述。毕竟,如果你需要在你自己的代码中引用你的处理程序呢?你可能会试着说 “我” 或 “我”。嗯,这些词在 Java 中不可用,所以你可以这样说。你把它传递到捕获对象的原因是你告诉它: “嘿,听着,我想做视频捕捉,当相机有新图像时,我想让你提醒这个草图。"
- 320: 幸运的是,第一个论点,这是唯一令人困惑的论点。320 是指摄像机拍摄的视频的宽度。
- 240: 视频的高度。
然而,在某些情况下,上述情况是行不通的。例如,如果您的计算机上连接了多个摄像头,该怎么办?如何选择要捕获的对象?此外,在一些罕见的情况下,您可能还需要指定相机的帧速率。对于这些情况,处理将通过 Capture.list() 为您提供所有可能的相机配置列表。您可以在消息控制台中显示这些内容,例如,通过说:
printArray(Capture.list());
您可以使用这些配置的文本创建捕获对象。例如,在带有内置摄像头的 Mac 上,这可能看起来像:
video = new Capture(this, "name=FaceTime HD Camera (Built-in),size=320x240,fps=30");
Capture.list() 实际上为您提供了一个数组,因此您也可以简单地引用所需配置的索引。
video = new Capture(this, Capture.list()[0]);
步骤 4.开始捕获过程。
一旦相机准备好了,就由你来告诉处理开始捕捉图像。
void setup() {
video = new Capture(this, 320, 240);
video.start();
}
几乎在每种情况下,您都希望在 setup() 中开始捕获。然而,start() 是它自己的方法,你可以选择,比如说,直到其他时间 (比如按下按钮等) 才开始捕获。)
步骤 5.从相机读取图像。
从相机读取帧有两种策略。我将简要地看这两个,并在本章的其余例子中选择一个。然而,这两种策略都在相同的基本原则下运行: 我只想在可以读取新帧时从相机读取图像。
为了检查图像是否可用,您使用可用函数 (),该函数根据是否存在某些内容返回 true 或 false。如果它在那里,则调用函数 read(),并将来自相机的帧读入内存。您可以在 draw() 循环中一遍又一遍地执行此操作,始终检查是否可以自由读取新图像。
void draw() {
if (video.available()) {
video.read();
}
}
第二种策略,即 “事件” 方法,需要一个函数,该函数在某个事件 (在这种情况下是相机事件) 发生时执行。每当按下鼠标时,都会执行 mousePressed() 函数。对于视频,您可以选择实现函数 captureEvent(),该函数在捕获事件发生时随时调用,也就是说,相机可以使用新帧。这些事件functions (mousePressed(), keyPressed(), captureEvent(), etc.) 有时被称为 “回调”。顺便说一句,如果你密切关注,这就是适合的地方。捕获对象视频知道通过调用 captureEvent() 来通知此草图,因为您在创建捕获对象视频时向它传递了对此草图的引用。
CaptureEvent () 是一个函数,因此需要生活在自己的块中,在 setup() 和 draw() 之外。
void captureEvent(Capture video) {
video.read();
}
你可能会注意到一些关于 captureEvent() 的奇怪的事情。它在定义中包含类型捕获的参数。这对你来说可能是多余的; 毕竟,在这个例子中,我已经有了一个全局变量视频。然而,在你可能有多个捕获设备的情况下,两者都可以使用相同的事件函数,视频库将确保将正确的捕获对象传递给 captureEvent()。
总而言之,每当有要读取的东西时,我想调用函数 read(),我可以通过使用 draw() 中的 available() 手动检查来做到这一点或者允许回调为你处理它-captureEvent()。这允许草图通过从主动画循环中分离出从相机中读取的逻辑来更有效地操作。
步骤 6.显示视频图像。
毫无疑问,这是最简单的部分。您可以将捕获对象视为随时间变化的 PImage,事实上,捕获对象可以以与 PImage 对象相同的方式使用。
image(video, 0, 0);
所有这些都放在下面的代码中:
//步骤 1.导入视频库。
import processing.video.*;
//步骤 2.声明捕获对象。
Capture video;
//步骤 5.当有新图像可用时,从相机读取!
void captureEvent(Capture video) {
video.read();
}
void setup() {
size(320, 240);
// 步骤 3.初始化捕获对象。
video = new Capture(this, 320, 240);
//步骤 4.开始捕获过程。
video.start();
}
//步骤 6.显示图像。
void draw() {
image(video, 0, 0);
}
同样,你可以用 PImage 做任何事情 (调整大小、着色、移动等)。) 你可以用捕获对象做。只要您从该对象中读取 (),视频图像将在您操作时更新。请参见以下示例:
import processing.video.*;
Capture video;
void setup() {
size(320, 240);
video = new Capture(this, 320, 240);
video.start();
}
void captureEvent(Capture video) {
video.read();
}
void draw() {
background(255);
tint(mouseX, mouseY, 255);
translate(width/2, height/2);
imageMode(CENTER);
rotate(PI/4);
image(video, 0, 0, mouseX, mouseY);
}
请注意,视频图像可以像 PImage 一样着色。它也可以像 PImage 一样移动、旋转和调整大小。以下是视频图像的 “调整亮度” 示例:
//步骤 1。导入视频库
import processing.video.*;
//步骤 2。声明捕获对象
Capture video;
void setup() {
size(320, 240);
//步骤 3.通过构造函数初始化捕获对象
video = new Capture(this, 320, 240);
video.start();
}
// 新帧可用时的事件
void captureEvent(Capture video) {
// 步骤 4.从相机读取图像。
video.read();
}
void draw() {
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;
// 从像素中获取红色,绿色,蓝色值
float r = red (video.pixels[loc]);
float g = green(video.pixels[loc]);
float b = blue (video.pixels[loc]);
//计算基于接近鼠标来改变亮度的数量
float d = dist(x, y, mouseX, mouseY);
float adjustbrightness = map(d, 0, 100, 4, 0);
r *= adjustbrightness;
g *= adjustbrightness;
b *= adjustbrightness;
// 约束 RGB 以确保它们在 0-255 颜色范围内
r = constrain(r, 0, 255);
g = constrain(g, 0, 255);
b = constrain(b, 0, 255);
// 创建新颜色并在窗口中设置像素
color c = color(r, g, b);
pixels[loc] = c;
}
}
updatePixels();
}
录制的视频
显示录制的视频遵循与实时视频相同的结构。Processing 的视频库接受大多数视频文件格式; 有关详细信息,请访问电影参考。
步骤 1.声明影片对象,而不是捕获对象。
Movie movie;
步骤 2.初始化电影对象。
movie = new Movie(this, "testmovie.mov");
唯一必要的参数是这个和电影的文件名,用引号括起来。影片文件应存储在草图的数据目录中。
步骤 3.开始播放电影。
有两个选项,play(),播放电影一次,或 loop(),连续循环。
movie.loop();
步骤 4.从电影中读取帧。
同样,这与捕获相同。您可以检查新帧是否可用,或者使用回调函数。
void draw() {
if (movie.available()) {
movie.read();
}
}
或者:
void movieEvent(Movie movie) {
movie.read();
}
步骤 5.显示影片。
image(movie, 0, 0);
以下代码一起显示了该程序:
import processing.video.*;
//步骤 1。声明电影对象。
Movie movie;
void setup() {
size(320, 240);
// 步骤 2.初始化电影对象。文件 “testmovie.mo v” 应位于数据文件夹中。
movie = new Movie(this, "testmovie.mov");
//步骤 3.开始播放电影。只玩一次 play() 可以用来代替。
movie.loop();
}
// 步骤 4.从电影中读取新帧。
void movieEvent(Movie movie) {
movie.read();
}
//步骤 5.显示电影。
void draw() {
image(movie, 0, 0);
}
尽管处理绝不是显示和操作录制视频的最复杂的环境,但视频库中有一些更高级的功能。有一些函数用于获取视频的持续时间 (以秒为单位的长度),用于加速和减慢,以及跳转到视频中的特定点 (等等)。如果您发现性能缓慢且视频播放不稳定,我建议您尝试 P2D 或 P3D 渲染器。
下面是一个使用 jump() (跳转到视频中的特定点) 和 duration() (以秒为单位返回电影长度) 的示例。在此示例中,如果 mouseX 等于 0,则视频跳到开头。如果 mouseX 等于宽度,它会跳到最后。任何其他值都介于两者之间。Jump () 函数允许您立即跳到视频中的某个时间点。Duration () 以秒为单位返回电影的总长度。
import processing.video.*;
Movie movie;
void setup() {
size(200, 200);
background(0);
movie = new Movie(this, "testmovie.mov");
}
void movieEvent(Movie movie) {
movie.read();
}
void draw() {
// 鼠标 X 与宽度之比
float ratio = mouseX / (float) width;
movie.jump(ratio * movie.duration());
image(movie, 0, 0);
}
软件镜像
随着小型摄像机连接到越来越多的个人计算机上,开发实时处理图像的软件变得越来越受欢迎。这些类型的应用有时被称为 “镜像”,因为它们提供了观众图像的数字反射。Processing 广泛的图形函数库及其从相机实时捕获的能力使其成为原型制作和软件镜像实验的绝佳环境。
您可以将基本的图像处理技术应用于视频图像,逐个读取和替换像素。更进一步,您可以读取像素并将颜色应用于屏幕上绘制的形状。
我将从一个以 80 × 60 像素捕获视频并在 640 × 480 窗口上渲染的示例开始。对于视频中的每个像素,我将绘制一个八像素宽八像素高的矩形。
让我们首先编写显示矩形网格的程序。在以下示例中,videoScale 变量存储窗口的像素大小与网格大小的比率,对于每一列和每一行,在 (x,y) 处绘制一个矩形位置按视频尺度缩放和调整大小。
// 网格中每个单元格的大小,窗口大小与视频大小的比率
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);
rect(x, y, videoScale, videoScale);
}
}
}
知道我想要八像素宽八像素高的正方形,我可以计算列数为宽度除以八,行数为高度除以八。
- 640/8 = 80 列
- 480/8 = 60 行
我现在可以捕捉到 80 × 60 的视频图像。这很有用,因为与 80 × 60 相比,从相机捕捉 640 × 480 的视频可能会很慢。我只想以草图所需的分辨率捕获颜色信息。
对于第 i 列和第 j 行的每个正方形,我在视频图像中查找像素 (I,j) 的颜色,并相应地对其进行着色。请参阅以下粗体中新零件的示例:
import processing.video.*;
//网格中每个单元格的大小,窗口大小与视频大小的比率
int videoScale = 8;
// 系统中的列数和行数
int cols, rows;
//保持捕获对象的变量
Capture video;
void setup() {
size(640, 480);
//初始化列和行
cols = width/videoScale;
rows = height/videoScale;
background(0);
video = new Capture(this, cols, rows);
video.start()
}
// 从相机读取图像
void captureEvent(Capture video) {
video.read();
}
void draw() {
video.loadPixels();
// 列的开始循环
for (int i = 0; i < cols; i++) {
// 行的开始循环
for (int j = 0; j < rows; j++) {
// 你在哪里,像素方面?
int x = i*videoScale;
int y = j*videoScale;
color c = video.pixels[i + j*video.width];
fill(c);
stroke(0);
rect(x, y, videoScale, videoScale);
}
}
}
如您所见,扩展简单的网格系统以包含视频中的颜色只需要添加一些内容。我必须声明并初始化捕获对象,从中读取,并从像素数组中提取颜色。
也可以应用较少的像素颜色到网格中形状的文字映射。在以下示例中,仅使用黑色和白色。视频中出现较亮像素的方块较大,较暗像素的方块较小。
//视频源中的每个像素绘制为
//尺寸基于亮度的矩形。
import processing.video.*;
// 网格中每个单元的大小
int videoScale = 10;
//系统中的列数和行数
int cols, rows;
// 捕获设备的变量
Capture video;
void setup() {
size(640, 480);
// 初始化列和行
cols = width / videoScale;
rows = height / videoScale;
// 构造捕获对象
video = new Capture(this, cols, rows);
video.start();
}
void captureEvent(Capture video) {
video.read();
}
void draw() {
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;
//反转列以使图像恢复正常。
int loc = (video.width - i - 1) + j * video.width;
color c = video.pixels[loc];
//矩形的大小是像素亮度的函数。
//亮像素是一个大矩形,暗像素是一个小矩形。
float sz = (brightness(c)/255) * videoScale;
rectMode(CENTER);
fill(255);
noStroke();
rect(x + videoScale/2, y + videoScale/2, sz, sz);
}
}
}
考虑分两个步骤开发软件镜像通常很有用。这也将帮助你超越像素到网格上形状的更明显的映射。
步骤 1.开发一个覆盖整个窗口的有趣模式。
步骤 2.使用视频的像素作为查找表,为该模式着色。
比如说,对于第一步,我编写了一个程序,在窗口周围随意涂鸦一行。这是我的算法,用伪代码写的。
- 从屏幕中心的 (x,y) 位置开始。
- 永远重复以下内容:
—选择一个新的 (x,y),留在窗口内。
—从旧 (x,y) 到新 (x,y) 画一条线。
—保存新的 (x,y)。
// 两个全局变量
float x;
float y;
void setup() {
size(320, 240);
background(255);
// 在中心开始 x 和 y
x = width/2;
y = height/2;
}
void draw() {
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 = newx;
y = newy;
}
现在我已经完成了图案生成草图,我可以根据视频图像更改笔画 () 来设置颜色。再次注意以下代码中以粗体添加的新代码行:
import processing.video.*;
//两个全局变量
float x;
float y;
//保持捕获对象的变量。
Capture video;
void setup() {
size(320, 240);
background(255);
// 在中心开始 x 和 y
x = width/2;
y = height/2;
//启动捕获过程
video = new Capture(this, width, height);
video.start();
}
void draw() {
video.loadPixels();
float newx = constrain(x + random(-20, 20), 0, width);
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);
// 在 (x,y) 中保存 (newx,newy)
x = newx;
y = newy;
}
void captureEvent(Capture video) {
// 从相机读取图像
video.read();
}