原文:Pro Processing for Images and Computer Vision with OpenCV
六、理解运动
在上一章中,你学习了如何理解一帧图像中的内容。在这一章中,您将开始了解多帧数字视频或实时网络摄像头流中的运动。作为一个简单的解释,只要两个连续帧之间有差异,就可以识别运动。在计算机视觉中,你试图使用各种方法来理解这些差异,以便理解运动方向和前景背景分离等现象。在这一章的开始,我将介绍数字艺术家在处理动态图像时使用的现有方法。我将涉及的主题如下:
- 运动图像的效果
- 帧差分
- 背景去除
- 光流
- 运动历史
运动图像的效果
在 20 世纪 90 年代,多媒体设计者主要使用软件导演来创建通过 CD-ROM 平台交付的交互式内容。当时的数字视频资料主要由预先录制的内容组成。然而,Director 能够通过附加组件或插件来扩展其功能。丹尼尔·罗津开发的 TrackThemColors 就是其中之一。extras 使导演能够捕捉和分析从网络摄像头捕捉的数字图像。大约在 1999 年,约翰·梅达的反应式图书系列《镜子镜子》也使用视频输入作为交互功能。此外,Josh Nimoy 的米隆图书馆(由 WebCamXtra 和 JMyron 组成)提供了从 Director、Java 和 Processing 访问网络摄像头的功能。该图书馆以米隆·克鲁格的名字命名,他是一位伟大的美国计算机研究人员和艺术家,在 20 世纪 70 年代用实时视频流创建了增强现实应用的早期形式。另一个参考是伟大的英国摄影师埃德沃德·迈布里奇的定格运动研究,他展示了一系列静态照片来说明连续的运动,如一匹马在奔跑。
通过在处理中使用video
库,您有了一组一致的函数来处理运动图像。媒体艺术家和设计师一直在探索在处理运动图像时产生创造性视觉效果的方法。以下部分将实现处理中的一些常见效果,以说明这些效果背后的创造性概念。我将介绍以下内容:
- 马赛克效应
- 狭缝扫描摄影
- 滚动效果
- 三维可视化
马赛克效应
第一个练习Chapter06_01
,是你在第三章中完成的马赛克效果的修改版本。您将为每个单元格创建原始图像的缩小版本,而不是为网格中的每个单元格使用单一的纯色,在本例中,为每个单元格创建实时网络摄像机视频流。这种效果已经在很多数字艺术和广告材料中使用。以下是节目来源:
// Mosaic effect
import processing.video.*;
final int CELLS = 40;
Capture cap;
PImage img;
int idx;
int rows, cols;
void setup() {
size(960, 720);
background(0);
cap = new Capture(this, 640, 480);
cap.start();
rows = CELLS;
cols = CELLS;
img = createImage(width/cols, height/rows, ARGB);
idx = 0;
}
void draw() {
if (!cap.available())
return;
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
int px = idx % cols;
int py = idx / cols;
int ix = px*cap.width/cols;
int iy = py*cap.height/rows;
color col = cap.pixels[iy*cap.width+ix];
tint(col);
image(img, px*img.width, py*img.height);
idx++;
idx %= (rows*cols);
}
在draw()
函数中,程序的每一帧都会将网络摄像头视频图像的快照复制到一个更小的叫做img
的PImage
中。它将从左到右、从上到下遍历整个屏幕,将最新的帧粘贴到网格的每个单元格中。在粘贴img
之前,它使用tint()
函数来改变颜色,从单元格的左上角反映颜色信息。结果,最终显示将类似于实时图像,而每个单元是时间上的独立帧。图 6-1 显示了显示屏的样本。
图 6-1。
Mosaic with live camera input
狭缝扫描效应
狭缝扫描是一种摄影技术,一次只曝光图像的一条狭缝。对于数字图像处理,您可以修改它,使其一次只包含一行像素。在下一个练习Chapter06_02
中,您将从网络摄像机直播流的每一帧中仅复制一行垂直像素。这是从运动图像生成静止图像的常用技术。Golan Levin 在 http://www.flong.com/texts/lists/slit_scan/
提供狭缝扫描艺术品的综合信息目录。以下清单是练习的来源:
// Slit-scan effect
import processing.video.*;
Capture cap;
PImage img;
int idx, mid;
void setup() {
size(1280, 480);
background(0);
cap = new Capture(this, width/2, height);
cap.start();
img = createImage(1, cap.height, ARGB);
idx = 0;
mid = cap.width/2;
}
void draw() {
if (!cap.available())
return;
cap.read();
img.copy(cap, mid, 0, 1, cap.height,
0, 0, img.width, img.height);
image(img, idx, 0);
idx++;
idx %= width;
}
程序很简单。在draw()
功能中,在捕获视频的中心取一条垂直的像素线,并将其复制到一个水平移动的位置,由idx
表示。在这种情况下,屏幕上的每条垂直线代表一个单独的时间点,从左向右移动。图 6-2 显示了结果图像。
图 6-2。
Slit-scan effect with Processing
滚动效果
同样,回到 20 世纪 90 年代,英国多媒体艺术团体 Antirom ( http://www.antirom.com/
)让电影的滚动效果变得流行起来。在 Flash 时代,日本设计师 Yugop Nakamura 也大量试验了滚动条作为界面元素。这背后的想法很简单。首先,您构建一个由多个图像组成的长条,类似于模拟电影胶片。这些图像通常是连续运动的快照。然后,您可以通过水平或垂直的滚动运动来制作影片动画。当滚动速度达到一定的阈值时,电影胶片中的每个单元似乎都在自己制作动画,产生类似于早期电影的效果。在下面的练习中,您将实现一个处理版本,Chapter06_03
:
// Scrolling effect
import processing.video.*;
// Processing modes for the draw() function
public enum Mode {
WAITING, RECORDING, PLAYING
}
final int FPS = 24;
Capture cap;
Mode mode;
PShape [] shp;
PImage [] img;
PShape strip;
int dispW, dispH;
int recFrame;
float px, vx;
void setup() {
size(800, 600, P3D);
background(0);
cap = new Capture(this, 640, 480);
cap.start();
// Frame size of the film strip
dispW = 160;
dispH = 120;
// Position and velocity of the film strip
px = 0;
vx = 0;
prepareShape();
mode = Mode.WAITING;
recFrame = 0;
frameRate(FPS);
noStroke();
fill(255);
}
void prepareShape() {
// Film strip shape
strip = createShape(GROUP);
// Keep 24 frames in the PImage array
img = new PImage[FPS];
int extra = ceil(width/dispW);
// Keep 5 more frames to compensate for the
// continuous scrolling effect
shp = new PShape[FPS+extra];
for (int i=0; i<FPS; i++) {
img[i] = createImage(dispW, dispH, ARGB);
shp[i] = createShape(RECT, 0, 0, dispW, dispH);
shp[i].setStroke(false);
shp[i].setFill(color(255));
shp[i].setTexture(img[i]);
shp[i].translate(i*img[i].width, 0);
strip.addChild(shp[i]);
}
// The 5 extra frames are the same as the
// first 5 ones.
for (int i=FPS; i<shp.length; i++) {
shp[i] = createShape(RECT, 0, 0, dispW, dispH);
shp[i].setStroke(false);
shp[i].setFill(color(255));
int j = i % img.length;
shp[i].setTexture(img[j]);
shp[i].translate(i*img[j].width, 0);
strip.addChild(shp[i]);
}
}
void
draw() {
switch (mode) {
case WAITING:
waitFrame();
break;
case RECORDING:
recordFrame();
break;
case PLAYING:
playFrame();
break;
}
}
void waitFrame() {
// Display to live webcam image while waiting
if (!cap.available())
return;
cap.read();
background(0);
image(cap, (width-cap.width)/2, (height-cap.height)/2);
}
void
recordFrame() {
// Record each frame into the PImage array
if (!cap.available())
return;
if (recFrame >= FPS) {
mode = Mode.PLAYING;
recFrame = 0;
println("Finish recording");
return;
}
cap.read();
img[recFrame].copy(cap, 0, 0, cap.width, cap.height,
0, 0, img[recFrame].width, img[recFrame].height);
int sw = 80;
int sh = 60;
int tx = recFrame % (width/sw);
int ty = recFrame / (width/sw);
image(img[recFrame], tx*sw, ty*sh, sw, sh);
recFrame++;
}
void playFrame() {
background(0);
// Compute the scrolling speed
vx = (width/2 - mouseX)*0.6;
px += vx;
// Check for 2 boundary conditions
if (px < (width-strip.getWidth())) {
px = width - strip.getWidth() - px;
} else if (px > 0) {
px = px - strip.getWidth() + width;
}
shape(strip, px, 250);
}
void mousePressed() {
// Press mouse button to record
if (mode != Mode.RECORDING) {
mode = Mode.RECORDING;
recFrame = 0;
background(0);
println("Start recording");
}
}
程序有三种状态,由enum
类型mode
表示。第一个是WAITING
状态,屏幕上显示实时网络摄像头。一旦用户按下鼠标按钮,程序进入RECORDING
状态。在这种状态下,它将 24 帧记录到名为img
的PImage
数组中。用户还可以在那一秒钟内获得屏幕上 24 个小框架布局的反馈。录制完成后,它会进入PLAYING
状态,显示一个长的水平连续画面。它将根据鼠标位置向左或向右滚动。用户也可以通过向左或向右移动鼠标来改变滚动速度。要创建连续循环滚动的幻像,需要在原来的 24 帧的末尾再添加 5 帧。这五帧构成了显示屏的宽度(800 像素)。当电影胶片滚动超出其边界时,您只需将胶片的另一端放在屏幕窗口内,如playFrame()
功能所示。整个电影胶片保存在由shp
阵列中的 29 帧组成的strip PShape
中。图 6-3 显示了一个示例截图供参考。
图 6-3。
Scrolling effect of filmstrip
三维可视化
你可以进一步将你的实验扩展到三维空间。在下一个练习Chapter06_04
中,您将在处理显示窗口中显示 24 个帧的集合。该程序将在一个由 24 个图片帧组成的半透明块中同时可视化 24 个连续帧,在三维空间中缓慢旋转。
// 3D effect
import processing.video.*;
final int FPS = 24;
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
PImage [] img;
PShape [] shp;
int idx;
float angle;
int dispW, dispH;
void setup() {
size(800, 600, P3D);
cap = new Capture(this, CAPW, CAPH, FPS);
cap.start();
idx = 0;
angle = 0;
frameRate(FPS);
// Keep the 24 frames in each img array member
img = new PImage[FPS];
// Keep the 24 images in a separate PShape
shp = new PShape[FPS];
dispW = cap.width;
dispH = cap.height;
for (int i=0; i<FPS; i++) {
img[i] = createImage(dispW, dispH, ARGB);
shp[i] = createShape(RECT, 0, 0, dispW, dispH);
shp[i].setStroke(false);
shp[i].setFill(color(255, 255, 255, 80));
shp[i].setTint(color(255, 255, 255, 80));
shp[i].setTexture(img[i]);
}
}
void draw() {
if (!cap.available())
return;
background(0);
lights();
cap.read();
// Copy the latest capture image into the
// array member with index - idx
img[idx].copy(cap, 0, 0, cap.width, cap.height,
0, 0, img[idx].width, img[idx].height);
pushMatrix();
translate(width/2, height/2, -480);
rotateY(radians(angle));
translate(-dispW/2, -dispH/2, -480);
displayAll();
popMatrix();
// Loop through the array with the idx
idx++;
idx %= FPS;
angle += 0.5;
angle %= 360;
text(nf(round(frameRate), 2), 10, 20);
}
void
displayAll() {
// Always display the first frame of
// index - idx
pushMatrix();
int i = idx - FPS + 1;
if (i < 0)
i += FPS;
for (int j=0; j<FPS; j++) {
shape(shp[i], 0, 0);
i++;
i %= FPS;
translate(0, 0, 40);
}
popMatrix();
}
每个矩形图像帧对应于一秒钟内 24 帧中的一帧。最上面的总是最新的帧。实际上,你可以看到运动一个接一个地向下传播到其他帧。由于框架是半透明的,当运动向下下沉时,您可以透过它们看到。我在我的作品《时间运动》第一部分中使用了这种效果。有了这个效果,电影中的跳跃剪辑将变成一个平滑的过渡。诀窍在于displayAll()
函数。变量idx
代表最新的帧。然后,将从以下语句中计算最早的帧,并由于负值而进行额外的调整:
int i = idx – FPS + 1;
之后的for
循环将以正确的顺序显示每一帧。为了在一秒钟内保存所有的 24 帧,你使用两个数组,img
和shp
。数组img
将每个视频帧存储为一个PImage
,它将作为纹理映射到数组shp
的每个成员之上,如PShape
。draw()
功能管理整个图框块的旋转,如图 6-4 所示。
图 6-4。
Video frames in 3D
帧差分
现在,您已经看到了许多处理运动图像中的帧的例子,您可以继续了解计算机视觉中如何检测运动。基本原理是,只有当两个图像帧发生变化时,您才能实现运动。通过比较两个帧,您可以简单地知道这两个帧之间发生了什么类型的运动。比较两帧的方法是在处理中使用第三章中的blend()
功能。在下一个练习Chapter06_05
中,您将实现实时网络摄像头流和静态图像之间的帧差:
// Difference between video and background
import processing.video.*;
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
PImage back, img, diff;
int dispW, dispH;
void setup() {
size(800, 600);
cap = new Capture(this, CAPW, CAPH);
cap.start();
dispW = width/2;
dispH = height/2;
back = createImage(dispW, dispH, ARGB);
img = createImage(dispW, dispH, ARGB);
diff = createImage(dispW, dispH, ARGB);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
// Get the difference image.
diff.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
diff.filter(GRAY);
diff.blend(back, 0, 0, back.width, back.height,
0, 0, diff.width, diff.height, DIFFERENCE);
// Obtain the threshold binary image.
img.copy(diff, 0, 0, diff.width, diff.height,
0, 0, img.width, img.height);
img.filter(THRESHOLD, 0.4);
image(cap, 0, 0, dispW, dispH);
image(back, dispW, 0, dispW, dispH);
image(diff, 0, dispH, dispW, dispH);
image(img, dispW, dispH, dispW, dispH);
text(nf(round(frameRate), 2), 10, 20);
}
void mousePressed() {
// Update the background image.
back.copy(cap, 0, 0, cap.width, cap.height,
0, 0, back.width, back.height);
back.filter(GRAY);
}
在这个程序中,你可以按下鼠标键从网络摄像头直播流中录制一个静态图像,并将其作为背景帧存储在名为back
的PImage
变量中。在每一帧中,在draw()
函数中,它使用blend()
函数将当前帧与背景进行比较,并将差异存储在PImage
变量diff
中。进一步应用阈值滤波器来生成称为img
的二进制PImage
。在处理显示窗口中,左上角显示当前视频帧,右上角显示背景图像,左下角显示差异图像,右下角显示阈值二进制图像。在阈值图像中,白色区域表示运动发生的位置。图 6-5 显示了一个示例截图供参考。
图 6-5。
Frame difference between live video and background
对于无法获得静态背景图像的应用,您可以考虑比较两个连续的帧来获得差异。下面的练习Chapter06_06
演示了获取两帧之间的差异的纯处理实现:
// Difference between consecutive frames
import processing.video.*;
final int CNT = 2;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
// Keep two frames to use alternately with
// array indices (prev, curr).
PImage [] img;
int prev, curr;
// Display image size
int dispW, dispH;
void setup() {
size(800, 600);
dispW = width/2;
dispH = height/2;
cap = new Capture(this, CAPW, CAPH);
cap.start();
img = new PImage[CNT];
for (int i=0; i<img.length; i++) {
img[i] = createImage(dispW, dispH, ARGB);
}
prev = 0;
curr = 1;
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
// Copy video image to current frame.
img[curr].copy(cap, 0, 0, cap.width, cap.height,
0, 0, img[curr].width, img[curr].height);
// Display current and previous frames.
image(img[curr], 0, 0, dispW, dispH);
image(img[prev], dispW, 0, dispW, dispH);
PImage tmp = createImage(dispW, dispH, ARGB);
arrayCopy(img[curr].pixels, tmp.pixels);
tmp.updatePixels();
// Create the difference image.
tmp.blend(img[prev], 0, 0, img[prev].width, img[prev].height,
0, 0, tmp.width, tmp.height, DIFFERENCE);
tmp.filter(GRAY);
image(tmp, 0, dispH, dispW, dispH);
// Convert the difference image to binary.
tmp.filter(THRESHOLD, 0.3);
image(tmp, dispW, dispH, dispW, dispH);
text(nf(round(frameRate), 2), 10, 20);
// Swap the two array indices.
int temp = prev;
prev = curr;
curr = temp;
}
程序保持一个PImage
缓冲数组img
,通过交换两个指针索引prev
和curr
来维护视频流中的前一帧和当前帧。其余的代码与前一个程序类似。它使用blend()
函数检索DIFFERENCE
图像,使用THRESHOLD
过滤器提取黑白二值图像。图 6-6 显示了该程序的示例截图。
图 6-6。
Difference between two frames in Processing
有了黑白差异图像,下一步就是从中获取有意义的信息。在第五章中,你学习了如何从黑色背景下的白色区域获取轮廓信息。在下一个练习Chapter06_07
中,您将使用相同的技术找出从黑白图像中识别出的轮廓的边界框。这个程序将使用 OpenCV。记得将带有 OpenCV 库的code
文件夹和CVImage
类定义添加到加工草图文件夹。
// Difference between 2 consecutive frames
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
final int CNT = 2;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
// Minimum bounding box area
final float MINAREA = 200.0;
Capture cap;
// Previous and current frames in Mat format
Mat [] frames;
int prev, curr;
CVImage img;
// Display size
int dispW, dispH;
void setup() {
size(800, 600);
dispW = width/2;
dispH = height/2;
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
img = new CVImage(dispW, dispH);
frames = new Mat[CNT];
for (int i=0; i<CNT; i++) {
frames[i] = new Mat(img.height, img.width,
CvType.CV_8UC1, Scalar.all(0));
}
prev = 0;
curr = 1;
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
PImage tmp0 = createImage(dispW, dispH, ARGB);
tmp0.copy(cap, 0, 0, cap.width, cap.height,
0, 0, tmp0.width, tmp0.height);
// Display current frame.
image(tmp0, 0, 0);
img.copyTo(tmp0);
frames[curr] = img.getGrey();
CVImage out = new CVImage(dispW, dispH);
out.copyTo(frames[prev]);
// Display previous frame.
image(out, dispW, 0, dispW, dispH);
Mat tmp1 = new Mat();
Mat tmp2 = new Mat();
// Difference between previous and current frames
Core.absdiff(frames[prev], frames[curr], tmp1);
Imgproc.threshold(tmp1, tmp2, 90, 255, Imgproc.THRESH_BINARY);
out.copyTo(tmp2);
// Display threshold difference image.
image(out, 0, dispH, dispW, dispH);
// Obtain contours of the difference binary image
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp2, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
Iterator<MatOfPoint> it = contours.iterator();
pushStyle();
fill(255, 180);
noStroke();
while
(it.hasNext()) {
MatOfPoint cont = it.next();
// Draw each bounding box
Rect rct = Imgproc.boundingRect(cont);
float area = (float)(rct.width * rct.height);
if (area < MINAREA)
continue;
rect((float)rct.x+dispW, (float)rct.y+dispH,
(float)rct.width, (float)rct.height);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
int temp = prev;
prev = curr;
curr = temp;
hierarchy.release();
tmp1.release();
tmp2.release();
}
该程序与前一个类似,只是您使用名为frames
的 OpenCV Mat
实例来存储前一帧和当前帧。您还可以使用Core.absdiff()
函数来计算差异图像,并使用Imgproc.threshold()
来生成黑白二值图像。当你在contours
数据结构中循环时,你首先计算边界框面积来过滤那些面积较小的轮廓。剩下的,你在显示窗口的右下角显示矩形,如图 6-7 所示。
图 6-7。
Simple tracking with frame differencing
背景去除
在前面的帧差分练习中,如果你观察足够长的时间,静态背景将保持黑色。只有前景中的运动物体是白色的。OpenCV 中的背景去除或背景减除是指将前景运动物体从静态背景图像中分离出来。您不需要像练习Chapter06_05
中那样提供静态背景图像。在 OpenCV 的video
模块中,BackgroundSubtractor
类将从一系列输入图像中学习,通过在当前帧和背景模型(包含场景的静态背景)之间执行减法来生成前景遮罩。下一个练习Chapter06_08
说明了背景减除的基本操作:
// Background subtraction
import processing.video.*;
import org.opencv.video.*;
import org.opencv.video.Video;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
CVImage img;
PImage back;
// OpenCV background subtractor
BackgroundSubtractorMOG2 bkg;
// Foreground mask
Mat fgMask;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
img = new CVImage(cap.width, cap.height);
bkg = Video.createBackgroundSubtractorMOG2();
fgMask = new Mat();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat capFrame = img.getBGRA();
bkg.apply(capFrame, fgMask);
CVImage out = new CVImage(fgMask.cols(), fgMask.rows());
out.copyTo(fgMask);
image(cap, 0, 0);
// Display the foreground mask
image(out, cap.width, 0);
text(nf(round(frameRate), 2), 10, 20);
capFrame.release();
}
该程序使用 Zoran Zivkovic 的基于高斯混合的背景/前景分割算法。类定义在 OpenCV 的video
模块中。注意使用额外的import
语句来包含类定义。这个类实例是由Video.createBackgroundSubtractorMOG2()
函数创建的。要使用该对象,您需要将视频帧和前景蒙版Mat
、fgMask
传递给draw()
函数中每一帧的apply()
函数。BackgroundSubtractor
对象bkg
将从每一帧中学习静态背景应该是什么,并生成前景蒙版。前景蒙版fgMask
是黑白图像,其中黑色区域是背景,白色区域是前景对象。程序会在左侧显示原始视频帧,在右侧显示前景遮罩,如图 6-8 所示。
图 6-8。
Background subtraction in OpenCV
使用前景蒙版,您可以将其与视频帧结合,以从背景中检索前景图像。下面的练习Chapter06_09
,将使用这种方法实现效果,类似于视频制作中的色度键:
// Background subtraction
import processing.video.*;
import org.opencv.video.*;
import org.opencv.video.Video;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
CVImage img;
PImage back;
BackgroundSubtractorKNN bkg;
Mat fgMask;
int dispW, dispH;
void setup() {
size(800, 600);
dispW = width/2;
dispH = height/2;
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
img = new CVImage(dispW, dispH);
bkg = Video.createBackgroundSubtractorKNN();
fgMask = new Mat();
// Background image
back = loadImage("background.png");
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
PImage tmp = createImage(dispW, dispH, ARGB);
// Resize the capture image
tmp.copy(cap, 0, 0, cap.width, cap.height,
0, 0, tmp.width, tmp.height);
img.copyTo(tmp);
Mat capFrame = img.getBGRA();
bkg.apply(capFrame, fgMask);
// Combine the video frame and foreground
// mask to obtain the foreground image.
Mat fgImage = new Mat();
capFrame.copyTo(fgImage, fgMask);
CVImage out = new CVImage(fgMask.cols(), fgMask.rows());
// Display the original video capture image.
image(tmp, 0, 0);
// Display the static background image.
image(back, dispW, 0);
out.copyTo(fgMask);
// Display the foreground mask.
image(out, 0, dispH);
out.copyTo(fgImage);
// Display the foreground image on top of
// the static background.
image(back, dispW, dispH);
image(out, dispW, dispH);
text(nf(round(frameRate), 2), 10, 20);
capFrame.release();
fgImage.release();
}
在这个程序中,您将显示四幅图像。左上方的是实时视频流。右上角的是静态背景图像,存储在名为back
的PImage
实例中。左下角的是前景蒙版,如前一个练习所示。右下角的是显示在背景图像上面的前景图像。您还可以尝试另一种背景减除方法,即 K 近邻背景减除法BackgroundSubtractorKNN
。当图像中的前景像素较少时,这种方法更有效。在draw()
函数中,程序定义了一个名为fgImage
的新变量来存储前景图像。你用前景蒙版fgMask
将当前视频图像capFrame
复制到fgImage
。
capFrame.copyTo(fgImage, fgMask);
在这种情况下,只有蒙版中的白色区域会被复制。图 6-9 显示了整体结果图像。
图 6-9。
Background subtraction and foreground extraction
除了前景图像,OpenCV BackgroundSubtractor
还可以用getBackgroundImage()
函数检索背景图像。下一个练习Chapter06_10
将演示它的用法。
// Background subtraction
import processing.video.*;
import org.opencv.video.*;
import org.opencv.video.Video;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
CVImage img;
PImage back;
BackgroundSubtractorKNN bkg;
// Foreground mask object
Mat fgMask;
int dispW, dispH;
void setup() {
size(800, 600);
dispW = width/2;
dispH = height/2;
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
img = new CVImage(dispW, dispH);
bkg = Video.createBackgroundSubtractorKNN();
fgMask = new Mat();
// Background image
back = loadImage("background.png");
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
PImage tmp = createImage(dispW, dispH, ARGB);
// Resize the capture image
tmp.copy(cap, 0, 0, cap.width, cap.height,
0, 0, tmp.width, tmp.height);
img.copyTo(tmp);
Mat capFrame = img.getBGR();
bkg.apply(capFrame, fgMask);
// Background image object
Mat bkImage = new Mat();
bkg.getBackgroundImage(bkImage);
CVImage out = new CVImage(fgMask.cols(), fgMask.rows());
// Display the original video capture image.
image(tmp, 0, 0);
out.copyTo(bkImage);
// Display the background image.
image(out, dispW, 0);
out.copyTo(fgMask);
// Display the foreground mask.
image(out, 0, dispH);
// Obtain the foreground image with the PImage
// mask method.
tmp.mask(out);
// Display the forground image on top of
// the static background.
image(back, dispW, dispH);
image(tmp, dispW, dispH);
text(nf(round(frameRate), 2), 10, 20);
capFrame.release();
}
在draw()
函数中,您定义了一个名为bkImage
的新的Mat
变量,并使用getBackgroundImage(bkImage)
方法将背景图像矩阵传递给bkImage
变量。该程序还解释了使用处理PImage
类的mask()
方法执行屏蔽操作的另一种方式。图 6-10 显示了一个示例截图。
图 6-10。
Background image retrieval
光流
OpenCV 有另一种方法来找出运动图像中的运动细节:video
模块中的光流特征。简单来说,光流就是对像素如何在两个连续帧间移动的分析,如图 6-11 所示。
图 6-11。
Optical flow
从第 2 帧开始,您可以逐个扫描每个像素,并尝试将其与第 1 帧中的像素匹配,围绕原始邻域。如果找到匹配,就可以声明第 1 帧中的像素流到第 2 帧中的新位置。您为该像素确定的箭头将是光流信息。要使用光流,您可以假设以下情况:运动对象的像素强度在连续帧之间变化不大,相邻像素具有相似的运动,并且该对象不会移动得太快。
在 OpenCV 实现中,有两种类型的光流分析:稀疏和密集。在这一章中,你将首先研究稠密光流。稀疏光流涉及特征点识别,这是下一章的主题。通常,密集光流是对图像中每个单个像素的光流信息的计算。它是资源密集型的。通常,您会减小视频帧的大小来增强性能。第一个光流练习(第 06_11 章)将基于 Gunnar Farneback 在 2003 年发表的论文“基于多项式展开的两帧运动估计”实现密集光流算法。
// Dense optical flow
import processing.video.*;
import org.opencv.video.*;
import org.opencv.video.Video;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
CVImage img;
float scaling;
int w, h;
Mat last;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
scaling = 10;
w = floor(CAPW/scaling);
h = floor(CAPH/scaling);
img = new CVImage(w, h);
last = new Mat(h, w, CvType.CV_8UC1);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
Mat flow = new Mat(last.size(), CvType.CV_32FC2);
Video.calcOpticalFlowFarneback(last, grey, flow,
0.5, 3, 10, 2, 7, 1.5, Video.OPTFLOW_FARNEBACK_GAUSSIAN);
grey.copyTo(last);
drawFlow(flow);
image(cap, 0, 0);
grey.release();
flow.release();
text(nf(round(frameRate), 2), 10, 20);
}
void drawFlow(Mat f) {
// Draw the flow data.
pushStyle();
noFill();
stroke(255);
for (int y=0; y<f.rows(); y++) {
float py = y*scaling;
for (int x=0; x<f.cols(); x++) {
double [] pt = f.get(y, x);
float dx = (float)pt[0];
float dy = (float)pt[1];
// Skip areas with no flow.
if (dx == 0 && dy == 0)
continue;
float px = x*scaling;
dx *= scaling;
dy *= scaling;
line(px+cap.width, py, px+cap.width+dx, py+dy);
}
}
popStyle();
}
图 6-12 显示了结果截图。
图 6-12。
Farneback dense optical flow
您可以从视频捕获帧中检索颜色信息,并用原始颜色给线条着色,而不是用白色绘制流线。在这种情况下,您可以轻松地生成网络摄像头实时图像的交互式渲染,如图 6-13 所示。
图 6-13。
Dense optical flow in color
在这个版本Chapter06_12
中,您需要做的唯一更改是在drawFlow()
函数中。不是在for
循环外使用stroke(255)
函数,而是计算像素颜色并将其分配给stroke()
函数。您已经在前面的章节中使用了这种技术。
void drawFlow(Mat f) {
// Draw the flow data.
pushStyle();
noFill();
for (int y=0; y<f.rows(); y++) {
int py = (int)constrain(y*scaling, 0, cap.height-1);
for (int x=0; x<f.cols(); x++) {
double [] pt = f.get(y, x);
float dx = (float)pt[0];
float dy = (float)pt[1];
// Skip areas with no flow.
if (dx == 0 && dy == 0)
continue;
int px = (int)constrain(x*scaling, 0, cap.width-1);
color col = cap.pixels[py*cap.width+px];
stroke(col);
dx *= scaling;
dy *= scaling;
line(px+cap.width, py, px+cap.width+dx, py+dy);
}
}
popStyle();
}
除了使用光流信息渲染网络摄像头图像之外,您还可以将其用于交互设计。例如,您可以在显示屏上定义一个虚拟热点,以及来自网络摄像头的实时图像。当你在虚拟热点上挥手时,你可以为程序触发一个事件,比如回放一个简短的声音剪辑。在交互设计中设计这样的空鼓套件或钢琴是相当常见的。下面的练习Chapter06_13
将使用光流信息实现这样一个虚拟热点。为了简化程序,您将定义一个额外的类Region
,来封装代码以实现热点。以下是Region
的定义:
import java.awt.Rectangle;
import java.lang.reflect.Method;
// The class to define the hotspot.
class Region {
// Threshold value to trigger the callback function.
final float FLOW_THRESH = 20;
Rectangle rct; // area of the hotspot
Rectangle screen; // area of the live capture
float scaling; // scaling factor for optical flow size
PVector flowInfo; // flow information within the hotspot
PApplet parent;
Method func; // callback function
boolean touched;
public Region(PApplet p, Rectangle r, Rectangle s, float f) {
parent = p;
// Register the callback function named regionTriggered.
try {
func = p.getClass().getMethod("regionTriggered",
new Class[]{this.getClass()});
}
catch
(Exception e) {
println(e.getMessage());
}
screen = s;
rct = (Rectangle)screen.createIntersection(r);
scaling = f;
flowInfo = new PVector(0, 0);
touched = false;
}
void update(Mat f) {
Rect sr = new Rect(floor(rct.x/scaling), floor(rct.y/scaling),
floor(rct.width/scaling), floor(rct.height/scaling));
// Obtain the submatrix - region of interest.
Mat flow = f.submat(sr);
flowInfo.set(0, 0);
// Accumulate the optical flow vectors.
for (int y=0; y<flow.rows(); y++) {
for (int x=0; x<flow.cols(); x++) {
double [] vec = flow.get(y, x);
PVector item = new PVector((float)vec[0], (float)vec[1]);
flowInfo.add(item);
}
}
flow.release();
// When the magnitude of total flow is larger than a
// threshold, trigger the callback.
if (flowInfo.mag()>FLOW_THRESH) {
touched = true;
try {
func.invoke(parent, this);
}
catch (Exception e) {
println(e.getMessage());
}
} else {
touched = false;
}
}
void
drawBox() {
// Draw the hotspot rectangle.
pushStyle();
if (touched) {
stroke(255, 200, 0);
fill(0, 100, 255, 160);
} else {
stroke(160);
noFill();
}
rect((float)(rct.x+screen.x), (float)(rct.y+screen.y),
(float)rct.width, (float)rct.height);
popStyle();
}
void drawFlow(Mat f, PVector o) {
// Visualize flow inside the region on
// the right hand side screen.
Rect sr = new Rect(floor(rct.x/scaling), floor(rct.y/scaling),
floor(rct.width/scaling), floor(rct.height/scaling));
Mat flow = f.submat(sr);
pushStyle();
noFill();
stroke(255);
for (int y=0; y<flow.rows(); y++) {
float y1 = y*scaling+rct.y + o.y;
for (int x=0; x<flow.cols(); x++) {
double [] vec = flow.get(y, x);
float x1 = x*scaling+rct.x + o.x;
float dx = (float)(vec[0]*scaling);
float dy = (float)(vec[1]*scaling);
line(x1, y1, x1+dx, y1+dy);
}
}
popStyle();
flow.release();
}
float getFlowMag() {
// Get the flow vector magnitude.
return flowInfo.mag();
}
void writeMsg(PVector o, String m) {
// Display message on screen.
int px = round(o.x + rct.x);
int py = round(o.y + rct.y);
text(m, px, py-10);
}
}
在Region
的类定义中,你使用一个叫做rct
的 Java Rectangle
来定义热点区域。另一个Rectangle
是视频捕捉窗口,叫做screen
。您使用 Java Rectangle
而不是 OpenCV Rect
,因为它为您提供了一种额外的方法来计算两个矩形之间的交集,以免rct
的定义在screen
之外,如以下语句所示:
rct = (Rectangle)screen.createIntersection(r);
在Region
的构造函数中,你也使用 Java Method
类从主程序注册方法regionTriggered
。在update()
方法中,你从参数f
中得到光流矩阵。由于您按照scaling
中给出的数量对视频采集图像进行了缩减采样,因此为了计算光流,您还需要按照相同的数量对Region
矩形进行缩减采样。之后,使用Region
矩形和以下语句计算原始光流矩阵中的子矩阵:
Mat flow = f.submat(sr);
在两个嵌套的for
循环中,您将所有的流向量累积到变量flowInfo
中。如果它的大小大于一个阈值,你可以断定有什么东西在摄像机前面移动,从而调用主程序中的回调函数regionTriggered
。其他方法很简单。他们只是画出矩形和流线。
对于主程序,您已经定义了两个测试热点。在draw()
函数中,在计算光流信息之后,循环通过regions
数组来更新和绘制必要的信息。作为回调函数,您定义了一个名为regionTriggered
的函数。引起触发的热点将作为一个Region
对象实例传递给回调。它首先检索区域内所有流向量的大小,然后调用方法writeMsg()
在区域顶部显示数字。
// Interaction design with optical flow
import processing.video.*;
import org.opencv.video.*;
import org.opencv.video.Video;
import java.awt.Rectangle;
// Capture size
final int CAPW = 640;
final int CAPH = 480;
Capture cap;
CVImage img;
float scaling;
int w, h;
Mat last;
Region [] regions;
// Flag to indicate if it is the first frame.
boolean first;
// Offset to the right hand side display.
PVector offset;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, CAPW, CAPH);
cap.start();
scaling = 20;
w = floor(CAPW/scaling);
h = floor(CAPH/scaling);
img = new CVImage(w, h);
last = new Mat(h, w, CvType.CV_8UC1);
Rectangle screen = new Rectangle(0, 0, cap.width, cap.height);
// Define 2 hotspots
.
regions = new Region[2];
regions[0] = new Region(this, new Rectangle(100, 100, 100, 100),
screen, scaling);
regions[1] = new Region(this, new Rectangle(500, 200, 100, 100),
screen, scaling);
first = true;
offset = new PVector(cap.width, 0);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
if (first) {
grey.copyTo(last);
first = false;
return
;
}
Mat flow = new Mat(last.size(), CvType.CV_32FC2);
Video.calcOpticalFlowFarneback(last, grey, flow,
0.5, 3, 10, 2, 7, 1.5, Video.OPTFLOW_FARNEBACK_GAUSSIAN);
grey.copyTo(last);
image(cap, 0, 0);
drawFlow(flow);
// Update the hotspots with the flow matrix.
// Draw the hotspot rectangle.
// Draw also the flow on the right hand side display.
for (Region rg : regions) {
rg.update(flow);
rg.drawBox();
rg.drawFlow(flow, offset);
}
grey.release();
flow.release();
text(nf(round(frameRate), 2), 10, 20);
}
void drawFlow(Mat f) {
// Draw the flow data.
pushStyle();
noFill();
stroke(255);
for (int y=0; y<f.rows(); y++) {
int py = (int)constrain(y*scaling, 0, cap.height-1);
for (int x=0; x<f.cols(); x++) {
double [] pt = f.get(y, x);
float dx = (float)pt[0];
float dy = (float)pt[1];
// Skip areas with no flow.
if (dx == 0 && dy == 0)
continue;
int px = (int)constrain(x*scaling, 0, cap.width-1);
dx *= scaling;
dy *= scaling;
line(px, py, px+dx, py+dy);
}
}
popStyle();
}
void
regionTriggered(Region r) {
// Callback function from the Region class.
// It displays the flow magnitude number on
// top of the hotspot rectangle.
int mag = round(r.getFlowMag());
r.writeMsg(offset, nf(mag, 3));
}
图 6-14 显示了一个示例截图供参考。请注意,其中一个热点是通过在网络摄像头前挥手激活的。它用半透明颜色填充,并且在显示器的右侧显示光流幅度值。
图 6-14。
Virtual hotspots with optical flow interaction
运动历史
在光流分析中,注意该函数仅使用两帧来计算流信息。OpenCV 提供了其他函数,可以累积更多的帧来详细分析运动历史。然而,从 3.0 版本开始,这些函数不再是 OpenCV 的标准发行版。它现在分布在 https://github.com/opencv/opencv_contrib
的opencv_contrib
库的额外模块中。这就是为什么在第一章中你用额外的模块optflow
构建了 OpenCV 库。以下是与运动历史相关的功能:
calcGlobalOrientation
calcMotionGradient
segmentMotion
updateMotionHistory
下一个练习Chapter06_14
,基于opencv_contrib
分布中的motempl.cpp
样本。因为它有点复杂,所以您将一步一步地构建它。我将回顾本章前一节中介绍的比较两个连续帧以创建阈值差图像的技术。
// Display threshold difference image.
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
final int CNT = 2;
Capture cap;
CVImage img;
Mat [] buf;
Mat silh;
int last;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
last = 0;
// Two frames buffer for comparison
buf = new Mat[CNT];
for (int i=0; i<CNT; i++) {
buf[i] = Mat.zeros(cap.height, cap.width,
CvType.CV_8UC1);
}
// Threshold difference image
silh = new Mat(cap.height, cap.width, CvType.CV_8UC1,
Scalar.all(0));
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
grey.copyTo(buf[last]);
int idx1, idx2;
idx1 = last;
idx2 = (last + 1) % buf.length;
last = idx2;
silh = buf[idx2];
// Create the threshold difference image between two frames.
Core.absdiff(buf[idx1], buf[idx2], silh);
Imgproc.threshold(silh, silh, 30, 255, Imgproc.THRESH_BINARY);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(silh);
image(img, 0, 0);
image(out, cap.width, 0);
text(nf(round(frameRate), 2), 10, 20);
grey.release();
}
该程序使用一个名为buf
的Mat
数组来维护来自网络摄像头的两个连续帧。基本上,它利用Core.absdiff()
和Imgproc.threshold()
函数来计算draw()
函数中每一帧的阈值差图像。图 6-15 显示了一个示例截图。
图 6-15。
Threshold difference image
结果就像你在图 6-6 中所做的处理。由于阈值差图像仅包含两帧的信息,下一步Chapter06_15
是累积这些图像中的一些以构建运动历史图像。
// Display motion history image.
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.optflow.Optflow;
import java.lang.System;
final int CNT = 2;
// Motion history duration is 5 seconds.
final double MHI_DURATION = 5;
Capture cap;
CVImage img;
Mat [] buf;
Mat mhi, silh, mask;
int last;
double time0;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
last = 0;
// Maintain two buffer frames.
buf = new Mat[CNT];
for (int i=0; i<CNT; i++) {
buf[i] = Mat.zeros(cap.height, cap.width,
CvType.CV_8UC1);
}
// Initialize the threshold difference image.
silh = new Mat(cap.height, cap.width, CvType.CV_8UC1,
Scalar.all(0));
// Initialize motion history image.
mhi = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
mask = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
// Store timestamp when program starts to run.
time0 = System.nanoTime();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
grey.copyTo(buf[last]);
int idx1, idx2;
idx1 = last;
idx2 = (last + 1) % buf.length;
last = idx2;
silh = buf[idx2];
// Get current timestamp in seconds.
double timestamp = (System.nanoTime() - time0)/1e9;
// Create binary threshold image from two frames.
Core.absdiff(buf[idx1], buf[idx2], silh);
Imgproc.threshold(silh, silh, 30, 255, Imgproc.THRESH_BINARY);
// Update motion history image from the threshold.
Optflow.updateMotionHistory(silh, mhi, timestamp, MHI_DURATION);
mhi.convertTo(mask, CvType.CV_8UC1,
255.0/MHI_DURATION,
(MHI_DURATION - timestamp)*255.0/MHI_DURATION);
// Display the greyscale motion history image.
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(mask);
image(img, 0, 0);
image(out, cap.width, 0);
text(nf(round(frameRate), 2), 10, 20);
grey.release();
}
获得剪影的阈值差异图像silh
后,使用 OpenCV 额外模块optflow
,通过功能Optflow.updateMotionHistory()
创建运动历史图像。第一个参数是输入轮廓图像。第二个参数是输出运动历史图像。第三个参数是以秒为单位的当前时间戳。最后一个参数是您想要保持的运动细节的最大持续时间(以秒为单位),在本例中是 5 秒。运动历史图像mhi
然后被转换回 8 位,称为mask
,用于显示。亮的区域是最近的运动,没有更多的运动时会褪成黑色。图 6-16 显示了一个示例截图。
图 6-16。
Motion history image
下一步Chapter06_16
,将进一步分析运动历史图像,找出运动梯度。即像素在帧之间移动的方向。光流模块提供另一个功能calcMotionGradient()
,计算运动历史图像中每个像素的运动方向。
// Display global motion direction
.
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.optflow.Optflow;
import java.lang.System;
final int CNT = 2;
// Define motion history duration.
final double MHI_DURATION = 5;
final double MAX_TIME_DELTA = 0.5;
final double MIN_TIME_DELTA = 0.05;
Capture cap;
CVImage img;
Mat [] buf;
Mat mhi, mask, orient, silh;
int last;
double time0;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
last = 0;
// Image buffer with two frames.
buf = new Mat[CNT];
for (int i=0; i<CNT; i++) {
buf[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
}
// Motion history image
mhi = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
// Threshold difference image
silh = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
mask = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
orient = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
// Program start time
time0 = System.nanoTime();
smooth();
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
grey.copyTo(buf[last]);
int idx1, idx2;
idx1 = last;
idx2 = (last + 1) % buf.length;
last = idx2;
silh = buf[idx2];
// Get current time in seconds.
double timestamp = (System.nanoTime() - time0)/1e9;
// Compute difference with threshold.
Core.absdiff(buf[idx1], buf[idx2], silh);
Imgproc.threshold(silh, silh, 30, 255, Imgproc.THRESH_BINARY);
// Update motion history image.
Optflow.updateMotionHistory(silh, mhi, timestamp, MHI_DURATION);
mhi.convertTo(mask, CvType.CV_8UC1,
255.0/MHI_DURATION,
(MHI_DURATION - timestamp)*255.0/MHI_DURATION);
// Display motion history image in 8bit greyscale.
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(mask);
image(img, 0, 0);
image(out, cap.width, 0);
// Compute overall motion gradient.
Optflow.calcMotionGradient(mhi, mask, orient,
MAX_TIME_DELTA, MIN_TIME_DELTA, 3);
// Calculate motion direction of whole frame.
double angle = Optflow.calcGlobalOrientation(orient, mask,
mhi, timestamp, MHI_DURATION);
// Skip cases with too little motion.
double count = Core.norm(silh, Core.NORM_L1);
if (count > (cap.width*cap.height*0.1)) {
pushStyle();
noFill();
stroke(255, 0, 0);
float radius = min(cap.width, cap.height)/2.0;
ellipse(cap.width/2+cap.width, cap.height/2, radius*2, radius*2);
stroke(0, 0, 255);
// Draw the main direction of motion.
line(cap.width/2+cap.width, cap.height/2,
cap.width/2+cap.width+radius*cos(radians((float)angle)),
cap.height/2+radius*sin(radians((float)angle)));
popStyle();
}
fill(0);
text(nf(round(frameRate), 2), 10, 20);
grey.release();
}
在draw()
函数中,该语句获取运动历史图像mhi
,并产生两个输出图像。
Optflow.calcMotionGradient(mhi, mask, orient, MAX_TIME_DELTA, MIN_TIME_DELTA, 3);
第一个,mask
,指示哪些像素具有有效的运动梯度信息。第二个是orient
,显示每个像素的运动方向角,单位为度。注意,名为mask
的输出Mat
将覆盖前面步骤中的原始内容。下一条语句根据上一条语句的结果计算平均运动方向:
double angle = Optflow.calcGlobalOrientation(orient, mask, mhi, timestamp, MHI_DURATION);
它将返回以度为单位的运动角度,值从 0 到 360。当屏幕上运动太少时,程序也会跳过这些情况。最后,程序会画一个大圆,并从圆心向检测到的运动方向画一条直线。图 6-17 显示了一个带有指向运动方向的蓝线的示例截图。
图 6-17。
Global motion direction
一旦你有了全局运动方向,你就可以用它来进行手势交互。下一个练习Chapter06_17
演示了从变量angle
获得的运动方向的简单用法:
// Gestural interaction demo
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.optflow.Optflow;
import java.lang.System;
final int CNT = 2;
// Define motion history duration.
final double MHI_DURATION = 3;
final double MAX_TIME_DELTA = 0.5;
final double MIN_TIME_DELTA = 0.05;
Capture cap;
CVImage img;
Mat [] buf;
Mat mhi, mask, orient, silh;
int last;
double time0;
float rot, vel, drag;
void setup() {
// Three dimensional scene
size(640, 480, P3D);
background(0);
// Disable depth test.
hint(DISABLE_DEPTH_TEST);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width, height);
cap.start();
img = new CVImage(cap.width, cap.height);
last = 0;
// Image buffer with two frames.
buf = new Mat[CNT];
for (int i=0; i<CNT; i++) {
buf[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
}
// Motion
history image
mhi = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
// Threshold difference image
silh = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
mask = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
orient = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
// Program start time
time0 = System.nanoTime();
smooth();
// Rotation of the cube in Y direction
rot = 0;
// Rotation velocity
vel = 0;
// Damping force
drag = 0.9;
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
grey.copyTo(buf[last]);
int idx1, idx2;
idx1 = last;
idx2 = (last + 1) % buf.length;
last = idx2;
silh = buf[idx2];
// Get current time in seconds.
double timestamp = (System.nanoTime() - time0)/1e9;
// Compute difference with threshold.
Core.absdiff(buf[idx1], buf[idx2], silh);
Imgproc.threshold(silh, silh, 30, 255, Imgproc.THRESH_BINARY);
// Update motion history image.
Optflow.updateMotionHistory(silh, mhi, timestamp, MHI_DURATION);
mhi.convertTo(mask, CvType.CV_8UC1,
255.0/MHI_DURATION,
(MHI_DURATION - timestamp)*255.0/MHI_DURATION);
// Display motion history image in 8bit greyscale.
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(mask);
image(img, 0, 0);
// Compute overall motion gradient.
Optflow.calcMotionGradient(mhi, mask, orient,
MAX_TIME_DELTA, MIN_TIME_DELTA, 3);
// Calculate motion direction of whole frame.
double angle = Optflow.calcGlobalOrientation(orient, mask,
mhi, timestamp, MHI_DURATION);
// Skip cases with too little motion.
double count = Core.norm(silh, Core.NORM_L1);
if (count > (cap.width*cap.height*0.1)) {
// Moving to the right
if (angle < 10 || (360 - angle) < 10) {
vel -= 0.02;
// Moving to the left
} else if (abs((float)angle-180) < 20) {
vel += 0.02;
}
}
// Slow down the velocity
vel *= drag;
// Update the rotation angle
rot += vel;
fill(0);
text(nf(round(frameRate), 2), 10, 20);
// Draw the cube.
pushMatrix();
pushStyle();
fill(255, 80);
stroke(255);
translate(cap.width/2, cap.height/2, 0);
rotateY(rot);
box(200);
popStyle();
popMatrix();
grey.release();
}
程序的结构保持不变。您可以在屏幕中央添加一个带有半透明立方体的 3D 场景。当你在摄像头前水平移动时,你沿着 y 轴旋转立方体。你把这个运动当作一个加速力来改变旋转的速度。图 6-18 为程序截图。
图 6-18。
Gestural interaction with motion direction
除了检索全局运动方向之外,您还可以分割运动梯度图像以识别各个运动区域。下一个练习Chapter06_18
将展示如何使用函数segmentMotion()
将整体运动信息分割成单独的区域:
// Motion history with motion segment
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.optflow.Optflow;
import java.lang.System;
import java.util.ArrayList;
final int CNT = 2;
// Minimum region area to display
final float MIN_AREA = 300;
// Motion history duration
final double MHI_DURATION = 3;
final double MAX_TIME_DELTA = 0.5;
final double MIN_TIME_DELTA = 0.05;
Capture cap;
CVImage img;
Mat [] buf;
Mat mhi, mask, orient, segMask, silh;
int last;
double time0, timestamp;
void
setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
last = 0;
buf = new Mat[CNT];
for (int i=0; i<CNT; i++) {
buf[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
}
// Motion history image
mhi = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
mask = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
orient = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
segMask = Mat.zeros(cap.height, cap.width, CvType.CV_32FC1);
// Threshold difference image
silh = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
// Program start time
time0 = System.nanoTime();
timestamp = 0;
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
grey.copyTo(buf[last]);
int idx1, idx2;
idx1 = last;
idx2 = (last + 1) % buf.length;
last = idx2;
silh = buf[idx2];
double timestamp = (System.nanoTime() - time0)/1e9;
// Create threshold difference image.
Core.absdiff(buf[idx1], buf[idx2], silh);
Imgproc.threshold(silh, silh, 30, 255, Imgproc.THRESH_BINARY);
// Update motion history image.
Optflow.updateMotionHistory(silh, mhi, timestamp, MHI_DURATION);
// Convert motion
history to 8bit image.
mhi.convertTo(mask, CvType.CV_8UC1,
255.0/MHI_DURATION,
(MHI_DURATION - timestamp)*255.0/MHI_DURATION);
// Display motion history image in greyscale.
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(mask);
// Calculate overall motion gradient.
Optflow.calcMotionGradient(mhi, mask, orient,
MAX_TIME_DELTA, MIN_TIME_DELTA, 3);
// Segment general motion into different regions.
MatOfRect regions = new MatOfRect();
Optflow.segmentMotion(mhi, segMask, regions,
timestamp, MAX_TIME_DELTA);
image(img, 0, 0);
image(out, cap.width, 0);
// Plot individual motion areas.
plotMotion(regions.toArray());
pushStyle();
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
regions.release();
}
void plotMotion(Rect [] rs) {
pushStyle();
fill(0, 0, 255, 80);
stroke(255, 255, 0);
for (Rect r : rs) {
// Skip regions of small area.
float area = r.width*r.height;
if (area < MIN_AREA)
continue;
// Obtain submatrices from motion images.
Mat silh_roi = silh.submat(r);
Mat mhi_roi = mhi.submat(r);
Mat orient_roi = orient.submat(r);
Mat mask_roi = mask.submat(r);
// Calculate motion direction of that region.
double angle = Optflow.calcGlobalOrientation(orient_roi,
mask_roi, mhi_roi, timestamp, MHI_DURATION);
// Skip regions with little motion.
double count = Core.norm(silh_roi, Core.NORM_L1);
if (count < (r.width*r.height*0.05))
continue;
PVector center = new PVector(r.x + r.width/2,
r.y + r.height/2);
float radius = min(r.width, r.height)/2.0;
ellipse(center.x, center.y, radius*2, radius*2);
line(center.x, center.y,
center.x+radius*cos(radians((float)angle)),
center.y+radius*sin(radians((float)angle)));
silh_roi.release();
mhi_roi.release();
orient_roi.release();
mask_roi.release();
}
popStyle();
}
完成计算运动梯度图像的语句后,使用以下语句分割运动信息:
Optflow.segmentMotion(mhi, segMask, regions, timestamp, MAX_TIME_DELTA);
主要输入是运动历史图像mhi
。在这种情况下,您没有段掩码。第二个参数segMask
只是一个空图像。操作的结果将存储在MatOfRect
变量regions
中。你写了函数plotMotion()
来遍历从regions
开始的每一个Rect
。在函数中,它会跳过面积太小而无法使用的区域。您使用相同的calcGlobalOrientation()
功能找出运动方向。唯一的区别是您使用子矩阵作为每个图像mhi
、orient
和mask
的感兴趣区域。其余部分与您在练习Chapter06_16
中所做的相同。图 6-19 显示了一个示例截图以供参考。
图 6-19。
Segment motion demonstration
图像左侧的每个圆圈是运动片段区域。圆的大小由区域的宽度和高度的较短边来定义。圆内的直线从圆心指向运动方向。
结论
在本章中,您研究了如何创造性地使用运动来生成视觉效果。此外,您还学习了如何从一系列帧中识别运动,以及如何将这些信息用于手势交互的界面设计。在下一章中,您将继续学习运动,首先识别感兴趣的点,然后跨图像帧跟踪它们以了解更多关于运动的信息。
七、特征检测和匹配
本章用更复杂的跟踪方法继续上一章的运动探索。在上一章中,您比较和分析了帧之间的整个图像,以识别运动信息。结果,从这些方法跟踪的运动细节是通用的,没有利用图像中的特定结构元素。在本章中,您将首先研究如何定位感兴趣的检测点。它们的通用术语是特征点。然后,您将尝试跟踪这些特征点如何在帧之间移动。这些功能主要在 OpenCV 的features2d
模块中提供。除了特征点,您还将探索如何使用objdetect
模块检测面部特征和人物。以下是本章涵盖的主题:
- 角点检测
- 稀疏光流
- 特征检测
- 特征匹配
- 人脸检测
- 人物检测
角点检测
在前面的章节中,您已经了解到,在imgproc
模块中,Canny()
功能可以有效地检测数字图像中的边缘。在本章中,您将进一步检测数字图像中的角点。这个概念就像边缘检测。如图 7-1 所示,角点是那些在不同方向上颜色发生显著变化的像素。
图 7-1。
Corner detection
第一个练习Chapter07_01
,演示了由 Chris Harris 和 Mike Stephens 在 1988 年创建的 Harris 角点检测方法。为了加快执行速度,在本练习中,您将按比例因子 10 缩小原始网络摄像头图像。检测到角点后,将结果矩阵归一化为 8 位分辨率,并遍历它以识别值高于阈值的角点像素。
// Harris corner detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
// Threshold value for a corner
final int THRESH = 140;
Capture cap;
CVImage img;
// Scale down the image for detection.
float scaling;
int w, h;
void setup() {
size(640, 480);
background(0);
scaling = 10;
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width, height);
cap.start();
w = floor(cap.width/scaling);
h = floor(cap.height/scaling);
img = new CVImage(w, h);
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
// Output matrix of corner information
Mat corners = Mat.zeros(grey.size(), CvType.CV_32FC1);
Imgproc.cornerHarris(grey, corners, 2, 3, 0.04,
Core.BORDER_DEFAULT);
// Normalize the corner information matrix.
Mat cor_norm = Mat.zeros(grey.size(), CvType.CV_8UC1);
Core.normalize(corners, cor_norm, 0, 255,
Core.NORM_MINMAX, CvType.CV_8UC1);
image(cap, 0, 0);
pushStyle();
noFill();
stroke(255, 0, 0);
strokeWeight(2);
// Draw each corner with value greater than threshold.
for (int y=0; y<cor_norm.rows(); y++) {
for (int x=0; x<cor_norm.cols(); x++) {
if (cor_norm.get(y, x)[0] < THRESH)
continue;
ellipse(x*scaling, y*scaling, 10, 10);
}
}
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
corners.release();
cor_norm.release();
}
主要功能是来自imgproc
模块的cornerHarris()
功能。第一个参数是输入灰度图像,grey
。第二个参数是输出矩阵corners
,它表示每个像素成为角点的可能性。其余参数的技术解释超出了本书的范围。有兴趣可以在 http://docs.opencv.org/3.1.0/d4/d7d/tutorial_harris_detector.html
找到 OpenCV 官方教程。第三个参数是用于计算梯度(像素强度的变化)的 2×2 邻域大小。第四个参数是 Sobel 导数的 3×3 孔径大小,如 OpenCV 文档中的 http://docs.opencv.org/3.1.0/d2/d2c/tutorial_sobel_derivatives.html
所示。第五个参数是 Harris 检测器参数,如前面提到的 Harris 检测器教程所示,最后一个参数是边框类型指示器。图 7-2 显示了程序的运行示例。
图 7-2。
Harris corner detection
稀疏光流
您在第六章中学习了如何使用密集光流功能。在本节中,我将解释如何使用稀疏光流进行运动检测。在密集光流中,您检查并跟踪缩减像素采样图像中的所有像素,而在稀疏光流中,您只检查选定数量的像素。这些是您感兴趣跟踪的点,称为特征点。一般来说,它们是角点。以下是您需要遵循的步骤:
- 识别特征点。
- 提高分的准确性。
- 计算这些点的光流。
- 可视化流程信息。
识别特征点
下一个练习Chapter07_02
,将使用时剑波和卡洛·托马西在 1994 年开发的函数goodFeaturesToTrack()
。该函数返回数字图像中最突出的角点。
// Feature points detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
MatOfPoint corners = new MatOfPoint();
// Identify the good feature points.
Imgproc.goodFeaturesToTrack(grey, corners,
100, 0.01, 10);
Point [] points = corners.toArray();
pushStyle();
noStroke();
// Draw each feature point according to its
// original color of the pixel.
for (Point p : points) {
int x = (int)constrain((float)p.x, 0, cap.width-1);
int y = (int)constrain((float)p.y, 0, cap.height-1);
color c = cap.pixels[y*cap.width+x];
fill(c);
ellipse(x+cap.width, y, 10, 10);
}
image(img, 0, 0);
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
corners.release();
}
在draw()
函数中,获得灰度图像后,将它传递给goodFeaturesToTrack()
函数。它将返回名为corners
的MatOfPoint
变量中的特征点信息。剩余的三个参数是检测的点的最大数量、检测的质量水平和每个特征点之间的最小距离。将corners
变量转换成名为points
的Point
数组后,循环遍历它,用从原始视频捕获图像中提取的颜色将每个角绘制成一个圆。图 7-3 显示了该程序的示例截图。
图 7-3。
Good features to track
提高准确性
获得特征点列表后,可以使用 OpenCV 函数来提高点的位置精度。即使您正在处理像素位于整数位置的数字图像,拐角也可能出现在两个相邻像素之间的位置。也就是在子像素位置。以下练习Chapter07_03
探究了此函数cornerSubPix()
,以提高角点位置的准确性:
// Feature points detection with subpixel accuracy
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
Capture cap;
CVImage img;
TermCriteria term;
int w, h;
float xRatio, yRatio;
void setup() {
size(800, 600);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
w = 640;
h = 480;
xRatio = (float)width/w;
yRatio = (float)height/h;
cap = new Capture(this, w, h);
cap.start();
img = new CVImage(cap.width, cap.height);
term = new TermCriteria(TermCriteria.COUNT | TermCriteria.EPS,
20, 0.03);
smooth();
}
void
draw() {
if (!cap.available())
return;
background(200);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
MatOfPoint corners = new MatOfPoint();
// Detect the initial feature points.
Imgproc.goodFeaturesToTrack(grey, corners,
100, 0.01, 10);
MatOfPoint2f c2 = new MatOfPoint2f(corners.toArray());
Imgproc.cornerSubPix(grey, c2,
new Size(5, 5),
new Size(-1, -1), term);
Point [] points = corners.toArray();
pushStyle();
noFill();
stroke(100);
// Display the original points.
for (Point p : points) {
ellipse((float)p.x*xRatio, (float)p.y*yRatio, 20, 20);
}
points = c2.toArray();
stroke(0);
// Display the more accurate points.
for (Point p : points) {
ellipse((float)p.x*xRatio, (float)p.y*yRatio, 20, 20);
}
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
corners.release();
c2.release();
}
在程序中,您使用较大的草图画布尺寸和较小的视频捕获尺寸来显示旧(像素级)和新(子像素级)角位置之间的差异。在draw()
函数中,在goodFeaturesToTrack()
函数之后,你得到一个名为corners
的MatOfPoint
变量中的特征点列表。新函数cornerSubPix()
将使用相同的输入,即grey
图像和corners
矩阵。角点将用作输入和输出,以亚像素精度存储新的特征点。为了提高精度,输入角必须采用新的浮点格式MatOfPoint2f
。对于cornerSubPix()
函数,第三个参数Size(5, 5)
是搜索窗口大小的一半。第四个是Size(-1, -1)
,是搜索窗口中没有搜索的区域的一半大小。负值表示没有这样的区域。最后一个term
,是迭代过程的终止标准。它确定迭代过程,例如cornerSubPix()
何时结束,或者达到最大计数 20,或者达到 0.03 像素的期望精度。在本例中,您在setup()
函数中将其指定为最大计数 20,所需精度为 0.03 像素。图 7-4 是运行程序的截图。灰色圆圈表示像素级拐角,而黑色圆圈表示子像素级拐角。
图 7-4。
Subpixel accuracy feature points
计算光流
当你有了特征点的准确位置后,下一个程序Chapter07_04
将会跟踪这些特征点的流向。主要函数是 OpenCV 的video
模块中的calcOpticalFlowPyrLK()
。它是基于 Jean-Yves Bouguet 在 2000 年发表的论文“Lucas Kanade 特征跟踪器的金字塔式实现”的实现。
// Sparse optical flow
import processing.video.*;
import org.opencv.core.*;
import org.opencv.video.Video;
import org.opencv.imgproc.Imgproc;
final int CNT = 2;
// Threshold to recalculate the feature points
final int MIN_PTS = 20;
// Number of points to track
final int TRACK_PTS = 150;
Capture
cap;
CVImage img;
TermCriteria term;
// Keep the old and new frames in greyscale.
Mat [] grey;
// Keep the old and new feature points.
MatOfPoint2f [] points;
// Keep the last index of the buffer.
int last;
// First run of the program
boolean first;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
term = new TermCriteria(TermCriteria.COUNT | TermCriteria.EPS,
20, 0.03);
// Initialize the image and keypoint buffers.
grey = new Mat[CNT];
points = new MatOfPoint2f[CNT];
for (int i=0; i<CNT; i++) {
grey[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
points[i] = new MatOfPoint2f();
}
last = 0;
first = true;
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
if (first) {
// Initialize feature points in first run.
findFeatures(img.getGrey());
first = false;
return
;
}
int idx1, idx2;
idx1 = last;
idx2 = (idx1 + 1) % grey.length;
last = idx2;
grey[idx2] = img.getGrey();
// Keep status and error of running the
// optical flow function.
MatOfByte status = new MatOfByte();
MatOfFloat err = new MatOfFloat();
Video.calcOpticalFlowPyrLK(grey[idx1], grey[idx2],
points[idx1], points[idx2], status, err);
Point [] pts = points[idx2].toArray();
byte [] statArr = status.toArray();
pushStyle();
noStroke();
int count = 0;
for (int i=0; i<pts.length; i++) {
// Skip error cases.
if (statArr[i] == 0)
continue;
int x = (int)constrain((float)pts[i].x, 0, cap.width-1);
int y = (int)constrain((float)pts[i].y, 0, cap.height-1);
color c = cap.pixels[y*cap.width+x];
fill(c);
ellipse(x+cap.width, y, 10, 10);
count++;
}
// Re-initialize feature points when valid points
// drop down to the threshold.
if (count < MIN_PTS)
findFeatures(img.getGrey());
image(img, 0, 0);
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
status.release();
err.release();
}
void
findFeatures(Mat g) {
// Find feature points given the greyscale image g.
int idx1, idx2;
idx1 = last;
idx2 = (idx1 + 1) % grey.length;
last = idx2;
grey[idx2] = g;
MatOfPoint pt = new MatOfPoint();
// Calculate feature points at pixel level.
Imgproc.goodFeaturesToTrack(grey[idx2], pt,
TRACK_PTS, 0.01, 10);
points[idx2] = new MatOfPoint2f(pt.toArray());
// Recalculate feature points at subpixel level.
Imgproc.cornerSubPix(grey[idx2], points[idx2],
new Size(10, 10),
new Size(-1, -1), term);
grey[idx2].copyTo(grey[idx1]);
points[idx2].copyTo(points[idx1]);
pt.release();
}
void keyPressed() {
if (keyCode == 32) {
// Press SPACE to initialize feature points.
findFeatures(img.getGrey());
}
}
关于数据结构,程序在名为grey
的数组变量中保存了两个连续的灰度帧。它还需要在称为points
的MatOfPoint2f
数组中保存两个连续的特征点列表。您使用整数变量last
来跟踪数组中哪个索引是最后一个图像帧数据。boolean
变量first
表示是否第一次运行draw()
循环。在第一次运行的情况下,它将通过调用findFeatures()
找到特征点,并更新前一帧和当前帧信息。功能findFeatures()
与您在之前的练习Chapter07_03
中所做的相同。
在draw()
函数中,将索引idx1
更新到最后一帧,将idx2
更新到当前帧。更新后,使用主函数Video.calcOpticalFlowPyrLK()
计算上一帧和当前帧之间的光流信息。函数的四个输入参数是前一帧、当前帧、前一帧特征点和当前帧特征点。该函数有两个输出。第一个是MatOfByte
变量status
,当找到相应的流时返回 1,否则返回 0。第二个输出是当前练习中未使用的误差度量。然后,for
循环将遍历所有有效的流程,并在当前帧特征点绘制小圆圈。该程序还对有效的流数据进行计数,如果该数目低于阈值MIN_PTS
,它将启动findFeatures()
功能来重新计算当前视频图像的特征点。图 7-5 是程序的样例截图。
图 7-5。
Optical flow visualization
可视化流程信息
代替在屏幕上绘制当前的特征点,您可以生成更有创造性的光流信息的可视化。下一个例子,Chapter07_05
,是一个流信息的交互动画。逻辑很简单。将每对特征点从前一个位置连接到其当前位置。
// Optical flow animation
import processing.video.*;
import org.opencv.core.*;
import org.opencv.video.Video;
import org.opencv.imgproc.Imgproc;
final int CNT = 2;
final int TRACK_PTS = 200;
final int MAX_DIST = 100;
Capture cap;
CVImage img;
TermCriteria term;
// Keep two consecutive frames and feature
// points list.
Mat [] grey;
MatOfPoint2f [] points;
int last;
boolean first;
void
setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
term = new TermCriteria(TermCriteria.COUNT | TermCriteria.EPS,
20, 0.03);
grey = new Mat[CNT];
points = new MatOfPoint2f[CNT];
for (int i=0; i<CNT; i++) {
grey[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
points[i] = new MatOfPoint2f();
}
last = 0;
first = true;
smooth();
}
void draw() {
if (!cap.available())
return;
fillBack();
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
if (first) {
findFeatures(img.getGrey());
first = false;
return;
}
int idx1, idx2;
idx1 = last;
idx2 = (idx1 + 1) % grey.length;
last = idx2;
grey[idx2] = img.getGrey();
MatOfByte status = new MatOfByte();
MatOfFloat err = new MatOfFloat();
Video.calcOpticalFlowPyrLK(grey[idx1], grey[idx2],
points[idx1], points[idx2], status, err);
// pt1 - last feature points list
// pt2 - current feature points list
Point [] pt1 = points[idx1].toArray();
Point [] pt2 = points[idx2].toArray();
byte [] statArr = status.toArray();
PVector p1 = new PVector(0, 0);
PVector p2 = new PVector(0, 0);
pushStyle();
stroke(255, 200);
noFill();
for (int i=0; i<pt2.length; i++) {
if (statArr[i] == 0)
continue;
// Constrain the points inside the video frame.
p1.x = (int)constrain((float)pt1[i].x, 0, cap.width-1);
p1.y = (int)constrain((float)pt1[i].y, 0, cap.height-1);
p2.x = (int)constrain((float)pt2[i].x, 0, cap.width-1);
p2.y = (int)constrain((float)pt2[i].y, 0, cap.height-1);
// Discard the flow with great distance.
if (p1.dist(p2) > MAX_DIST)
continue;
line(p1.x+cap.width, p1.y, p2.x+cap.width, p2.y);
}
// Find
new feature points for each frame.
findFeatures(img.getGrey());
image(img, 0, 0);
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
status.release();
err.release();
}
void findFeatures(Mat g) {
grey[last] = g;
MatOfPoint pt = new MatOfPoint();
Imgproc.goodFeaturesToTrack(grey[last], pt,
TRACK_PTS, 0.01, 10);
points[last] = new MatOfPoint2f(pt.toArray());
Imgproc.cornerSubPix(grey[last], points[last],
new Size(5, 5),
new Size(-1, -1), term);
pt.release();
}
void fillBack() {
// Set background color with transparency.
pushStyle();
noStroke();
fill(0, 0, 0, 80);
rect(cap.width, 0, cap.width, cap.height);
popStyle();
}
要创建运动模糊效果,不要将背景颜色完全清除为黑色。在fillBack()
功能中,你用一个半透明的矩形填充背景来创建线条的运动轨迹。图 7-6 显示了动画的截图。
图 7-6。
Optical flow animation
在创造性编码中,你经常没有正确和明确的答案。在大多数情况下,你只是不停地问“如果呢?”问题。从前面的练习开始,你可以问,如果你不把屏幕清成黑色会怎么样?如果你从视频图像中提取线条的颜色会怎么样?如果使用不同的笔画粗细会怎样?下一个练习Chapter07_06
,通过将流动动画积累成一种手势绘画的形式来说明这些想法。你可以很容易地将这些效果与杰森·布拉克等画家的动作绘画联系起来。
// Optical flow drawing
import processing.video.*;
import org.opencv.core.*;
import org.opencv.video.Video;
import org.opencv.imgproc.Imgproc;
final int CNT = 2;
final int TRACK_PTS = 150;
final int MAX_DIST = 100;
Capture
cap;
CVImage img;
TermCriteria term;
Mat [] grey;
MatOfPoint2f [] points;
int last;
boolean first;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
term = new TermCriteria(TermCriteria.COUNT | TermCriteria.EPS,
20, 0.03);
// Initialize the buffers for the 2 images and 2 keypoint lists.
grey = new Mat[CNT];
points = new MatOfPoint2f[CNT];
for (int i=0; i<CNT; i++) {
grey[i] = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
points[i] = new MatOfPoint2f();
}
last = 0;
first = true;
smooth();
}
void draw() {
// Note that we do not clear the background.
if (!cap.available())
return;
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
if (first) {
findFeatures(img.getGrey());
first = false;
return
;
}
int idx1, idx2;
idx1 = last;
idx2 = (idx1 + 1) % grey.length;
last = idx2;
grey[idx2] = img.getGrey();
MatOfByte status = new MatOfByte();
MatOfFloat err = new MatOfFloat();
Video.calcOpticalFlowPyrLK(grey[idx1], grey[idx2],
points[idx1], points[idx2], status, err);
Point [] pt2 = points[idx2].toArray();
Point [] pt1 = points[idx1].toArray();
byte [] statArr = status.toArray();
PVector p1 = new PVector(0, 0);
PVector p2 = new PVector(0, 0);
pushStyle();
noFill();
for (int i=0; i<pt2.length; i++) {
if (statArr[i] == 0)
continue;
p1.x = (int)constrain((float)pt1[i].x, 0, cap.width-1);
p1.y = (int)constrain((float)pt1[i].y, 0, cap.height-1);
p2.x = (int)constrain((float)pt2[i].x, 0, cap.width-1);
p2.y = (int)constrain((float)pt2[i].y, 0, cap.height-1);
if (p1.dist(p2) > MAX_DIST)
continue;
color c = cap.pixels[(int)p2.y*cap.width+(int)p2.x];
stroke(red(c), green(c), blue(c), (int)random(100, 160));
strokeWeight(random(3, 6));
line(p1.x+cap.width, p1.y, p2.x+cap.width, p2.y);
c = cap.pixels[(int)p1.y*cap.width+(int)p1.x];
stroke(red(c), green(c), blue(c), (int)random(120, 240));
strokeWeight(random(1, 4));
line(p1.x+cap.width, p1.y, p2.x+cap.width, p2.y);
}
findFeatures(img.getGrey());
image(img, 0, 0);
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
status.release();
err.release();
}
void
findFeatures(Mat g) {
// Re-initialize the feature points.
grey[last] = g;
MatOfPoint pt = new MatOfPoint();
Imgproc.goodFeaturesToTrack(grey[last], pt,
TRACK_PTS, 0.01, 10);
points[last] = new MatOfPoint2f(pt.toArray());
Imgproc.cornerSubPix(grey[last], points[last],
new Size(10, 10),
new Size(-1, -1), term);
pt.release();
}
这个程序类似于上一个,除了你没有清除背景。在绘制流数据的for
循环中,首先从实时视频图像中选取颜色,然后绘制两条线而不是一条线。第一条线比较粗,颜色比较透明。第二条线更细,更不透明。它创造了一种更有绘画感的效果。图 7-7 包含两张光流绘制的效果图截图。我的作品时间运动,第一部分( http://www.magicandlove.com/blog/artworks/movement-in-time-v-1/
)是一个使用稀疏光流从经典好莱坞电影序列中生成手势绘画的例子。
图 7-7。
Optical flow drawing
特征检测
在前面的章节中,您尝试通过使用 Harris 角点方法和带有 Shi 和 Tomasi 方法的goodFeaturesToTrack()
函数来定位关键特征点。OpenCV 为您提供了通用的关键点处理来检测、描述它们,并在连续的帧之间进行匹配。在本节中,您将首先学习如何使用features2d
模块中的FeatureDetector
类来识别关键点。下一个练习Chapter07_07
将演示该类的基本操作:
// Features detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.FeatureDetector;
final float MIN_RESP = 0.003;
Capture cap;
CVImage img;
FeatureDetector fd;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
// Create the instance of the class.
fd = FeatureDetector.create(FeatureDetector.ORB);
smooth();
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
MatOfKeyPoint pt = new MatOfKeyPoint();
// Detect keypoints from the image.
fd.detect(grey, pt);
image(cap, 0, 0);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(grey);
tint(255, 100);
image(out, cap.width, 0);
noTint();
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = pt.toArray();
for (KeyPoint kp : kps) {
// Skip the keypoints that are less likely.
if (kp.response < MIN_RESP)
continue;
float x1 = (float)kp.pt.x;
float y1 = (float)kp.pt.y;
float x2 = x1 + kp.size*cos(radians(kp.angle))/2;
float y2 = y1 + kp.size*sin(radians(kp.angle))/2;
// size is the diameter of neighborhood.
ellipse(x1+cap.width, y1, kp.size, kp.size);
// Draw also the orientation direction.
line(x1+cap.width, y1, x2+cap.width, y2);
}
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
pt.release();
}
您使用FeatureDetector
类实例fd
来处理主要任务。在setup()
函数中,您用FeatureDetector.create()
函数创建了实例fd
。该参数指示您使用的检测器类型。在 OpenCV 3.1 的 Java 版本中,有以下类型:
AKAZE
,DYNAMIC_AKAZE
,GRID_AKAZE
,PYRAMID_AKAZE
,BRISK
,DYNAMIC_BRISK
,GRID_BRISK
,PYRAMID_BRISK
,FAST
,DYNAMIC_FAST
,GRID_FAST
,PYRAMID_FAST
,GFTT
,DYNAMIC_GFTT
,GRID_GFTT
,PYRAMID
,GFTT
,HARRIS
,DYNAMIC_HARRIS
,GRID_HARRIS
,PYRAMID_HARRIS
,MSER
,DYNAMIC_MSER
,GRID_MSER
,PYRAMID_MSER
,ORB
,DYNAMIC_ORB
,GRID_ORB
,PYRAMID_ORB
,SIMPLEBLOB
、DYNAMIC_SIMPLEBLOB
、GRID_SIMPLEBLOB
、PYRAMID_SIMPLEBLOB
在当前的练习中,您将使用类型FeatureDetector.ORB
。各种探测器类型的详细描述超出了本书的范围。然而,您可以参考本章后面的图 7-9 来比较各种探测器类型。
在draw()
函数中,您使用方法fd.detect(grey, pt)
来执行关键点检测,并将结果存储在名为pt
的MatOfKeyPoint
实例中。将pt
转换成KeyPoint
数组kps
后,使用for
循环遍历每个KeyPoint
对象。对于每个KeyPoint
,属性pt
是点的位置。属性response
描述了它成为关键点的可能性。您将它与阈值MIN_RESP
进行比较,以跳过值较小的那些。属性size
是关键点邻域的直径。属性angle
显示关键点方向。使用一个圆来表示关键点及其邻域大小,使用一条直线来表示方向。图 7-8 显示了一个示例截图。灰度图像以较暗的色调显示,与关键点圆圈形成较高的对比度。
图 7-8。
Feature detection in features2d
图 7-9 显示了使用不同FeatureDetector
类型检测到的关键点的集合。
图 7-9。
Comparison of different FeatureDetector types
您可以使用关键点信息进行创造性的可视化。然而,在下一节中,您将学习 OpenCV 中的通用特征匹配,以便进行后续跟踪。在使用特征匹配之前,还有一个步骤:关键点描述。您将使用来自features2d
模块的DescriptorExtractor
类来计算关键点的描述符。下一个练习Chapter07_08
,将说明描述符的用法:
// Keypoint descriptor
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.FeatureDetector;
Capture
cap;
CVImage img;
FeatureDetector fd;
DescriptorExtractor de;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
fd = FeatureDetector.create(FeatureDetector.AKAZE);
// Create the instance for the descriptor
de = DescriptorExtractor.create(DescriptorExtractor.AKAZE);
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
image(cap, 0, 0);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(grey);
tint(255, 200);
image(out, cap.width, 0);
noTint();
MatOfKeyPoint pt = new MatOfKeyPoint();
fd.detect(grey, pt);
Mat desc = new Mat();
// Compute the descriptor from grey and pt.
de.compute(grey, pt, desc);
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = pt.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x+cap.width, y, kp.size, kp.size);
}
popStyle();
pt.release();
grey.release();
desc.release();
fill(0);
text(nf(round(frameRate), 2), 10, 20);
}
节目就像上一个。它只添加了一个新类DescriptorExtractor
和它的实例de
。它使用DescriptorExtractor.create()
方法在setup()
函数中创建一个实例。在draw()
函数中,它使用compute()
方法在Mat
中创建名为desc
的描述符。加工窗口中的显示与图 7-8 相似,除了您在屏幕上生成更多的关键点,因为您没有跳过那些响应较低的关键点。对于pt
中的每个关键点,在desc
中将有一个条目用于描述该关键点。一旦在desc
中有了描述符信息,就可以开始下一部分的匹配了。
特征匹配
特征匹配通常涉及两组信息。第一组由已知图像的特征点和描述符组成。你可以把它称为训练集。第二个包括来自新图像的特征点和描述符,通常来自实时捕获图像。您可以将其称为查询集。特征匹配的工作是在训练集和查询集之间进行特征点匹配。进行特征匹配的目的是从训练集中识别已知模式,并跟踪该模式在查询集中的移动位置。在接下来的练习中,您将首先在实时视频流的两个快照之间执行常规特征匹配,在第二个练习中,您将在快照中交互选择一个模式,并尝试跟踪它在实时视频流中的移动位置。
接下来的练习Chapter07_09
,是匹配的准备。它将显示经过训练的快照图像和实时查询图像,以及关键点信息。您可以按下鼠标按钮来切换训练动作。
// Features matching
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.FeatureDetector;
Capture cap;
CVImage img;
FeatureDetector fd;
DescriptorExtractor de;
// Two sets of keypoints: train, query
MatOfKeyPoint trainKp, queryKp;
// Two sets of descriptor: train, query
Mat trainDc, queryDc;
Mat grey
;
// Keep if training started.
boolean trained;
// Keep the trained image
.
PImage trainImg;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
trainImg = createImage(cap.width, cap.height, ARGB);
fd = FeatureDetector.create(FeatureDetector.BRISK);
de = DescriptorExtractor.create(DescriptorExtractor.BRISK);
trainKp = new MatOfKeyPoint();
queryKp = new MatOfKeyPoint();
trainDc = new Mat();
queryDc = new Mat();
grey = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
trained = false;
smooth();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
if (trained) {
image(trainImg, 0, 0);
image(cap, trainImg.width, 0);
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
grey = img.getGrey();
fd.detect(grey, queryKp);
de.compute(grey, queryKp, queryDc);
drawTrain();
drawQuery();
} else {
image(cap, 0, 0);
image(cap, cap.width, 0);
}
pushStyle();
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
}
void
drawTrain() {
// Draw the keypoints for the trained snapshot.
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = trainKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x, y, kp.size, kp.size);
}
popStyle();
}
void
drawQuery() {
// Draw the keypoints for live query image.
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = queryKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x+trainImg.width, y, kp.size, kp.size);
}
popStyle();
}
void
mousePressed() {
// Press mouse button to toggle training.
if (!trained) {
arrayCopy(cap.pixels, trainImg.pixels);
trainImg.updatePixels();
img.copy(trainImg, 0, 0, trainImg.width, trainImg.height,
0, 0, img.width, img.height);
img.copyTo();
grey = img.getGrey();
fd.detect(grey, trainKp);
de.compute(grey, trainKp, trainDc);
trained = true;
} else {
trained = false;
}
}
这个程序相对简单。你保存了两对数据结构。第一对是用于训练图像的MatOfKeyPoint
、trainKp
和查询图像的queryKp
。第二对由描述符trainDc
和queryDC
组成。当用户按下鼠标按钮时,它将拍摄当前视频流的快照,并使用该图像来计算训练的关键点trainKp
和描述符trainDc
。在draw()
函数中,如果有经过训练的图像,程序将从实时视频图像中计算查询关键点queryKp
和描述符queryDc
。图像和关键点信息都将显示在处理窗口中。
图 7-10 显示了运行程序的示例截图。左图是静止图像及其训练好的关键点。右图是现场视频图像及其查询要点。
图 7-10。
Feature points from the trained and query images
下一个练习Chapter07_10
将引入匹配来识别训练图像和查询图像之间的对应关键点。
// Features matching
import processing.video.*;
import java.util.Arrays;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.FeatureDetector;
import org.opencv.features2d.DescriptorExtractor;
import org.opencv.features2d.DescriptorMatcher;
final
int MAX_DIST = 200;
Capture cap;
CVImage img;
FeatureDetector fd;
DescriptorExtractor de;
MatOfKeyPoint trainKp, queryKp;
Mat trainDc, queryDc;
DescriptorMatcher match;
Mat grey;
boolean trained;
PImage trainImg;
void
setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
trainImg = createImage(cap.width, cap.height, ARGB);
fd = FeatureDetector.create(FeatureDetector.ORB);
de = DescriptorExtractor.create(DescriptorExtractor.ORB);
match = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_L1);
trainKp = new MatOfKeyPoint();
queryKp = new MatOfKeyPoint();
trainDc = new Mat();
queryDc = new Mat();
grey = Mat.zeros(cap.height, cap.width, CvType.CV_8UC1);
trained = false;
smooth();
}
void
draw() {
if (!cap.available())
return
;
background(0);
cap.read();
if (trained) {
image(trainImg, 0, 0);
image(cap, trainImg.width, 0);
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
grey = img.getGrey();
fd.detect(grey, queryKp);
de.compute(grey, queryKp, queryDc);
MatOfDMatch pairs = new MatOfDMatch();
// Perform key point matching.
match.match(queryDc, trainDc, pairs);
DMatch [] dm = pairs.toArray();
KeyPoint [] tKp = trainKp.toArray();
KeyPoint [] qKp = queryKp.toArray();
// Connect the matched key points.
for (DMatch d : dm) {
// Skip those with large distance.
if (d.distance>MAX_DIST)
continue;
KeyPoint t = tKp[d.trainIdx];
KeyPoint q = qKp[d.queryIdx];
line((float)t.pt.x, (float)t.pt.y,
(float)q.pt.x+cap.width, (float)q.pt.y);
}
drawTrain();
drawQuery();
pairs.release();
} else
{
image(cap, 0, 0);
image(cap, cap.width, 0);
}
pushStyle();
fill(0);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
}
void
drawTrain() {
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = trainKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x, y, kp.size, kp.size);
}
popStyle();
}
void
drawQuery() {
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = queryKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x+trainImg.width, y, kp.size, kp.size);
}
popStyle();
}
void
mousePressed() {
if (!trained) {
arrayCopy(cap.pixels, trainImg.pixels);
trainImg.updatePixels();
img.copy(trainImg, 0, 0, trainImg.width, trainImg.height,
0, 0, img.width, img.height);
img.copyTo();
grey = img.getGrey();
fd.detect(grey, trainKp);
de.compute(grey, trainKp, trainDc);
trained = true;
} else {
trained = false;
}
}
大部分代码与前面的程序Chapter07_09
相同。然而,在这个代码中你有一些新的条目。在setup()
函数中,您必须用下面的语句初始化DescriptorMatcher
类实例match
:
match = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_L1);
静态create()
方法内部的参数是匹配方法。支持以下暴力破解方法的变体:BRUTEFORCE
、BRUTEFORCE_HAMMING
、BRUTEFORCE_HAMMINGLUT
、BRUTEFORCE_L1
和BRUTEFORCE_SL2
。如果点击加工图像内部,将在draw()
函数内部执行以下语句:
match.match(queryDc, trainDc, pairs);
match()
功能将执行实时图像的关键点描述符queryDc
和左侧存储图像的关键点描述符trainDc
之间的匹配。变量pairs
将所有匹配的关键点对存储为一个MatOfDMatch
实例。DMatch
是一种数据结构,用于维护存储在查询和训练关键点列表queryKp
和trainKp
中的关键点queryIdx
和trainIdx
的匹配索引。之后的for
循环将枚举所有的关键点匹配对,并为匹配距离d.distance
小于MAX_DIST
阈值的匹配对绘制匹配线。图 7-11 显示了执行的结果截图。
图 7-11。
Feature matching
在许多情况下,您不会通过网络摄像头使用整个图像作为训练图像模式。您可以只选择图像的一部分作为您想要跟踪的图案。在下一个练习Chapter07_11
中,您将使用鼠标绘制一个矩形,仅选择实时图像的一部分进行跟踪。这类似于大多数图形软件中的矩形选框工具。您单击并拖动以定义一个矩形区域作为训练图像,并仅使用该区域内的那些关键点来匹配来自实况视频的查询图像中的那些关键点。为了简化主程序,您定义了一个单独的类Dragging
,从这里处理鼠标交互。
import org.opencv.core.Rect;
// Define 3 states of mouse drag action.
public enum State {
IDLE,
DRAGGING,
SELECTED
}
// A class to handle the mouse drag action
public class Dragging {
PVector p1, p2;
Rect roi;
State state;
public Dragging() {
p1 = new PVector(Float.MAX_VALUE, Float.MAX_VALUE);
p2 = new PVector(Float.MIN_VALUE, Float.MIN_VALUE);
roi = new Rect(0, 0, 0, 0);
state = State.IDLE;
}
void init(PVector m) {
empty(m);
state = State.DRAGGING;
}
void update(PVector m) {
p2.set(m.x, m.y);
roi.x = (int)min(p1.x, p2.x);
roi.y = (int)min(p1.y, p2.y);
roi.width = (int)abs(p2.x-p1.x);
roi.height = (int)abs(p2.y-p1.y);
}
void move(PVector m) {
update(m);
}
void stop(PVector m) {
update(m);
state = State.SELECTED;
}
void empty(PVector m) {
p1.set(m.x, m.y);
p2.set(m.x, m.y);
roi.x = (int)m.x;
roi.y = (int)m.y;
roi.width = 0;
roi.height = 0;
}
void
reset(PVector m) {
empty(m);
state = State.IDLE;
}
boolean
isDragging() {
return (state == State.DRAGGING);
}
boolean isSelected() {
return (state == State.SELECTED);
}
boolean isIdle() {
return (state == State.IDLE);
}
Rect getRoi() {
return roi;
}
}
该类定义了鼠标交互的三种状态:IDLE
,当没有选择开始时;DRAGGING
,用户点击并开始拖动鼠标时;和SELECTED
,当用户释放鼠标按钮以确认选择矩形时,roi
。该类维护两个PVector
对象:p1
,选择矩形的左上角,和p2
,选择矩形的右下角。当用户开始点击-拖动动作时,程序将调用init()
方法。在拖动动作过程中,它会调用move()
方法。当用户停止并释放鼠标按钮时,它将调用stop()
方法。当用户点击而没有任何拖动时,它将通过调用reset()
方法清除选择。该类还为用户提供了三个布尔方法(isIdle()
、isDragging()
和isSelected()
)来查询交互的状态。
主程序类似于Chapter07_10
练习,除了您有额外的代码来处理选择交互和消除选择矩形外的关键点的方法。
// Features matching with selection
import processing.video.*;
import java.util.Arrays;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.features2d.FeatureDetector;
import org.opencv.features2d.DescriptorExtractor;
import org.opencv.features2d.DescriptorMatcher;
import org.opencv.calib3d.Calib3d;
Capture cap;
CVImage img;
// Feature detector, extractor and matcher
FeatureDetector fd;
DescriptorExtractor de;
DescriptorMatcher match;
// Key points and descriptors for train and query
MatOfKeyPoint trainKp, queryKp;
Mat trainDc, queryDc;
// Buffer for the trained image
PImage trainImg;
// A class to work with mouse drag & selection
Dragging drag;
Mat hg;
MatOfPoint2f trainRect, queryRect;
void setup() {
size(1280, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
trainImg = createImage(cap.width, cap.height, ARGB);
fd = FeatureDetector.create(FeatureDetector.ORB);
de = DescriptorExtractor.create(DescriptorExtractor.ORB);
match = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_HAMMING);
trainKp = new MatOfKeyPoint();
queryKp = new MatOfKeyPoint();
trainDc = new Mat();
queryDc = new Mat();
hg = Mat.eye(3, 3, CvType.CV_32FC1);
drag = new Dragging();
smooth();
trainRect = new MatOfPoint2f();
queryRect = new MatOfPoint2f();
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
Mat grey = img.getGrey();
image(trainImg, 0, 0);
image(cap, trainImg.width, 0);
if (drag.isDragging()) {
drawRect(cap.width);
} else if (drag.isSelected()) {
drawRect(0);
matchPoints(grey);
drawTrain();
drawQuery();
}
pushStyle();
fill(80);
text(nf(round(frameRate), 2), 10, 20);
popStyle();
grey.release();
}
void matchPoints(Mat im) {
// Match the trained and query key points.
fd.detect(im, queryKp);
de.compute(im, queryKp, queryDc);
// Skip if the trained or query descriptors are empty.
if (!queryDc.empty() &&
!trainDc.empty()) {
MatOfDMatch pairs = new MatOfDMatch();
match.match(queryDc, trainDc, pairs);
DMatch [] dm = pairs.toArray();
// Convert trained and query MatOfKeyPoint to array.
KeyPoint [] tKp = trainKp.toArray();
KeyPoint [] qKp = queryKp.toArray();
float minDist = Float.MAX_VALUE;
float maxDist = Float.MIN_VALUE;
// Obtain the min and max distances of matching.
for (DMatch d : dm) {
if (d.distance < minDist) {
minDist = d.distance;
}
if (d.distance > maxDist) {
maxDist = d.distance;
}
}
float
thresVal = 2*minDist;
ArrayList<Point> trainList = new ArrayList<Point>();
ArrayList<Point> queryList = new ArrayList<Point>();
pushStyle();
noFill();
stroke(255);
for (DMatch d : dm) {
if (d.queryIdx >= qKp.length ||
d.trainIdx >= tKp.length)
continue;
// Skip match data with distance larger than
// 2 times of min distance.
if (d.distance > thresVal)
continue;
KeyPoint t = tKp[d.trainIdx];
KeyPoint q = qKp[d.queryIdx];
trainList.add(t.pt);
queryList.add(q.pt);
// Draw a line for each pair of matching key points.
line((float)t.pt.x, (float)t.pt.y,
(float)q.pt.x+cap.width, (float)q.pt.y);
}
MatOfPoint2f trainM = new MatOfPoint2f();
MatOfPoint2f queryM = new MatOfPoint2f();
trainM.fromList(trainList);
queryM.fromList(queryList);
// Find the homography matrix between the trained
// key points and query key points.
// Proceed only with more than 5 key points.
if (trainList.size() > 5 &&
queryList.size() > 5) {
hg = Calib3d.findHomography(trainM, queryM, Calib3d.RANSAC, 3.0);
if (!hg.empty()) {
// Perform perspective transform to the
// selection rectangle with the homography matrix.
Core.perspectiveTransform(trainRect, queryRect, hg);
}
pairs.release();
trainM.release();
queryM.release();
hg.release();
}
if (!queryRect.empty()) {
// Draw the transformed selection matrix.
Point [] out = queryRect.toArray();
stroke(255, 255, 0);
for (int i=0; i<out.length; i++) {
int j = (i+1) % out.length;
Point p1 = out[i];
Point p2 = out[j];
line((float)p1.x+cap.width, (float)p1.y,
(float)p2.x+cap.width, (float)p2.y);
}
}
}
popStyle();
}
void
drawRect(float ox) {
// Draw the selection rectangle.
pushStyle();
noFill();
stroke(255, 255, 0);
rect(drag.getRoi().x+ox, drag.getRoi().y,
drag.getRoi().width, drag.getRoi().height);
popStyle();
}
void
drawTrain() {
// Draw the trained key points.
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = trainKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x, y, 10, 10);
}
popStyle();
}
void drawQuery() {
// Draw live image key points.
pushStyle();
noFill();
stroke(255, 200, 0);
KeyPoint [] kps = queryKp.toArray();
for (KeyPoint kp : kps) {
float x = (float)kp.pt.x;
float y = (float)kp.pt.y;
ellipse(x+trainImg.width, y, 10, 10);
}
popStyle();
}
void mouseClicked() {
// Reset the drag rectangle.
drag.reset(new PVector(0, 0));
}
void mousePressed() {
// Click only on the right hand side of the window
// to start the drag action.
if (mouseX < cap.width || mouseX >= cap.width*2)
return;
if (mouseY < 0 || mouseY >= cap.height)
return;
drag.init(new PVector(mouseX-cap.width, mouseY));
}
void
mouseDragged() {
// Drag the selection rectangle.
int x = constrain(mouseX, cap.width, cap.width*2-1);
int y = constrain(mouseY, 0, cap.height-1);
drag.move(new PVector(x-cap.width, y));
}
void mouseReleased() {
// Finalize the selection rectangle.
int x = constrain(mouseX, cap.width, cap.width*2-1);
int y = constrain(mouseY, 0, cap.height-1);
drag.stop(new PVector(x-cap.width, y));
// Compute the trained key points and descriptor.
arrayCopy(cap.pixels, trainImg.pixels);
trainImg.updatePixels();
CVImage tBGR = new CVImage(trainImg.width, trainImg.height);
tBGR.copy(trainImg, 0, 0, trainImg.width, trainImg.height,
0, 0, tBGR.width, tBGR.height);
tBGR.copyTo();
Mat temp = tBGR.getGrey();
Mat tTrain = new Mat();
// Detect and compute key points and descriptors.
fd.detect(temp, trainKp);
de.compute(temp, trainKp, tTrain);
// Define the selection rectangle.
Rect r = drag.getRoi();
// Convert MatOfKeyPoint to array.
KeyPoint [] iKpt = trainKp.toArray();
ArrayList<KeyPoint> oKpt = new ArrayList<KeyPoint>();
trainDc.release();
// Select only the key points inside selection rectangle.
for (int i=0; i<iKpt.length; i++) {
if (r.contains(iKpt[i].pt)) {
// Add key point to the output list.
oKpt.add(iKpt[i]);
trainDc.push_back(tTrain.row(i));
}
}
trainKp.fromList(oKpt);
// Compute the selection rectangle as MatOfPoint2f.
ArrayList<Point> quad = new ArrayList<Point>();
quad.add(new Point(r.x, r.y));
quad.add(new Point(r.x+r.width, r.y));
quad.add(new Point(r.x+r.width, r.y+r.height));
quad.add(new Point(r.x, r.y+r.height));
trainRect.fromList(quad);
queryRect.release();
tTrain.release();
temp.release();
}
在处理窗口中,屏幕上将有两个图像。左边的是当用户通过鼠标拖动动作执行选择时的训练图像。右边是现场视频图像。当用户想要做出选择时,用户需要点击并拖动右边的实时图像。当选择矩形被确认时,它将与实时视频图像的快照一起被发送到左侧。处理事件处理器mouseClicked()
、mousePressed()
、mouseDragged()
和mouseReleased()
管理交互选择过程。在mouseReleased()
方法中,你有额外的代码来首先从实时视频图像的灰度版本中检测关键点;其次计算关键点的描述符;第三遍所有的关键点,只选择那些在选择矩形内的,drag.getRoi()
;第四,准备已训练的关键点列表trainKp
和描述符trainDc
;最后将选择矩形转换为名为trainRect
的MatOfPoint2f
变量。
在draw()
函数中,您只需在DRAGGING
状态下绘制临时选择矩形。在SELECTED
状态下,你将调用matchPoints()
函数,这是程序中最复杂的函数。在这个函数中,它首先从实时视频图像中检测关键点,并计算描述符。当训练描述符和查询描述符都不为空时,它执行关键点匹配。注意,经过训练的描述符trainDc
仅包含选择矩形内的关键点描述。匹配后,该函数将遍历所有匹配对,找出名为pairs
的MatOfDMatch
对象内的最小和最大距离。在随后的循环中,只处理距离小于最小距离值两倍的匹配对。在for
循环之后,你将绘制连接所有匹配关键点的线,并从关键点列表中创建另外两个MatOfPoint2f
变量trainM
和queryM
。当trainM
和queryM
都包含五个以上的关键点时,使用Calib3d.findHomography()
方法从两个关键点列表中计算转换矩阵(单应矩阵)hg
。通过单应矩阵hg
,执行透视变换Core.perspectiveTransform()
,将保存在trainRect
中的选择矩形转换为queryRect
。queryRect
形状由转换后的矩形的四个角的坐标组成,位于窗口的右侧。本质上,四个角将定义跟踪图案的矩形。matchPoints()
函数的最后一部分绘制了连接queryRect
中四个角的四条直线。
图 7-12 显示了结果截图。右侧的四边形是通过使用从左侧的静态训练图像中检测到的模式来跟踪的区域。为了获得最佳效果,您选择的图案应该包含高对比度的视觉纹理。你也应该避免在背景中出现类似的纹理。在matchPoints()
功能中,您建立一个阈值来跳过差异大于两倍最小距离的匹配关键点。您可以降低阈值来减少噪音条件。
图 7-12。
Key point matching with a selection rectangle
除了绘制四边形的轮廓之外,下一个练习Chapter07_12
将在实时网络摄像头图像上绘制的四边形上执行纹理映射。我没有在这里列出整个练习的源代码,我只是在Chapter07_11
中强调了原始版本的变化。您定义了一个全局的PImage
变量photo
,来保存您想要映射到被跟踪图案顶部的图像。在setup()
函数中,你使用P3D
渲染为size(1280, 480, P3D)
,同时设置纹理模式为textureMode(NORMAL)
正常。在matchPoints()
函数的末尾,您有以下代码来绘制上一个练习Chapter07_11
中的四边形:
if (!queryRect.empty()) {
// Draw the transformed selection matrix.
Point [] out = queryRect.toArray();
stroke(255, 255, 0);
for (int i=0; i<out.length; i++) {
int j = (i+1) % out.length;
Point p1 = out[i];
Point p2 = out[j];
line((float)p1.x+cap.width, (float)p1.y,
(float)p2.x+cap.width, (float)p2.y);
}
}
在这个新版本中,Chapter07_12
,你通过使用beginShape()
和endShape()
函数来绘制四边形。在形状定义中,使用四个vertex()
函数通过纹理映射选项绘制四边形。
if (!queryRect.empty()) {
// Draw the transformed selection matrix.
Point [] out = queryRect.toArray();
noStroke();
fill(255);
beginShape();
texture(photo);
vertex((float)out[0].x+cap.width, (float)out[0].y, 0, 0, 0);
vertex((float)out[1].x+cap.width, (float)out[1].y, 0, 1, 0);
vertex((float)out[2].x+cap.width, (float)out[2].y, 0, 1, 1);
vertex((float)out[3].x+cap.width, (float)out[3].y, 0, 0, 1);
endShape(CLOSE);
}
生成的图像将类似于图 7-13 所示。
图 7-13。
Key points matching with texture mapped onto the rectangle
您可能会发现,前面的练习是构建无标记增强现实应用的基础。在更高级的使用中,PImage
变量photo
会被一个三维物体代替。然而,这超出了本书讨论细节的范围。如果感兴趣,可以在 OpenCV 相关文档中寻找 3D 姿态估计。
人脸检测
在交互式媒体制作中,艺术家和设计师经常求助于 OpenCV 的人脸检测功能。该功能是 OpenCV objdetect
模块中的特性之一。该实现基于 Paul Viola 和 Michael Jones 在 2001 年发表的论文“使用简单特征的增强级联进行快速对象检测”。人脸检测是一个机器学习过程。这意味着,在执行人脸检测之前,您需要训练程序来学习有效和无效的人脸。然而,在 OpenCV 中,发行版包括保存在data/haarcascades
文件夹中的预训练信息。您可以使用任何一个 XML 文件来检测特征,如正面脸、侧面脸、眼睛,甚至表情,如微笑。
在下一个练习Chapter07_13
中,您将使用参数文件haarcascade_frontalface_default.xml
检测用户的正面人脸。该文件位于opencv-3.1.0/data/haarcascades
的 OpenCV 分发文件夹中。您需要将该文件从 OpenCV 发行版复制到加工草图的data
文件夹中。
// Face detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.objdetect.CascadeClassifier;
// Detection image size
final
int W = 320, H = 240;
Capture cap;
CVImage img;
CascadeClassifier face;
// Ratio between capture size and
// detection size
float ratio;
void setup() {
size(640, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width, height);
cap.start();
img = new CVImage(W, H);
// Load the trained face information.
face = new CascadeClassifier(dataPath("haarcascade_frontalface_default.xml"));
ratio = float(width)/W;
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
image(cap, 0, 0);
Mat grey = img.getGrey();
// Perform face detction. Detection
// result is in the faces.
MatOfRect faces = new MatOfRect();
face.detectMultiScale(grey, faces);
Rect [] facesArr = faces.toArray();
pushStyle();
fill(255, 255, 0, 100);
stroke(255);
// Draw each detected face.
for (Rect r : facesArr) {
rect(r.x*ratio, r.y*ratio, r.width*ratio, r.height*ratio);
}
grey.release();
faces.release();
noStroke();
fill(0);
text(nf(round(frameRate), 2, 0), 10, 20);
popStyle();
}
您在人脸检测中使用的参数属于CascadeClassifier
类。首先,您必须定义这个类的一个实例face
。在setup()
功能中,你用来自文件haarcascade_frontalface_default.xml
的训练过的正面面部细节创建新的实例,该文件被复制到data
文件夹中。您还可以使用处理函数dataPath()
来返回data
文件夹的绝对路径。为了优化性能,您在以下语句中使用一个较小尺寸(320×240)的灰度图像grey
进行检测:
face.detectMultiScale(grey, faces);
第一个参数是要检测人脸的灰度图像。结果会在第二个参数中,也就是MatOfRect
变量faces
。通过将它转换成一个Rect
数组facesArr
,您可以使用一个for
循环来显示所有的边界矩形。图 7-14 显示了程序的一个示例显示。
图 7-14。
Face detection
一旦检测到人脸,您可以进一步检测人脸的边框内的面部特征。在下面的练习Chapter07_14
中,您将在一张脸内执行微笑检测。这个节目就像上一个。检测到面部后,使用边框创建一个较小的图像,并在其中检测微笑面部特征。为了测试程序,您还需要将 OpenCV 发行版中的haarcascade_smile.xml
文件复制到加工草图的data
文件夹中。
// Smile detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.objdetect.CascadeClassifier;
// Face
detection size
final int W = 320, H = 240;
Capture cap;
CVImage img;
// Two classifiers, one for face, one for smile
CascadeClassifier face, smile;
float ratio;
void setup() {
size(640, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width, height);
cap.start();
img = new CVImage(W, H);
face = new CascadeClassifier(dataPath("haarcascade_frontalface_default.xml"));
smile = new CascadeClassifier(dataPath("haarcascade_smile.xml"));
ratio = float(width)/W;
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
noStroke();
image(cap, 0, 0);
Mat grey = img.getGrey();
MatOfRect faces = new MatOfRect();
// Detect the faces first.
face.detectMultiScale(grey, faces, 1.15, 3,
Objdetect.CASCADE_SCALE_IMAGE,
new Size(60, 60), new Size(200, 200));
Rect [] facesArr = faces.toArray();
pushStyle();
for (Rect r : facesArr) {
fill(255, 255, 0, 100);
stroke(255, 0, 0);
float cx = r.x + r.width/2.0;
float cy = r.y + r.height/2.0;
ellipse(cx*ratio, cy*ratio,
r.width*ratio, r.height*ratio);
// For each face, obtain the image within the bounding box.
Mat fa = grey.submat(r);
MatOfRect m = new MatOfRect();
// Detect smiling expression.
smile.detectMultiScale(fa, m, 1.2, 25,
Objdetect.CASCADE_SCALE_IMAGE,
new Size(30, 30), new Size(80, 80));
Rect [] mArr = m.toArray();
stroke(0, 0, 255);
noFill();
// Draw the line of the mouth.
for (Rect sm : mArr) {
float yy = sm.y+r.y+sm.height/2.0;
line((sm.x+r.x)*ratio, yy*ratio,
(sm.x+r.x+sm.width)*ratio, yy*ratio);
}
fa.release();
m.release();
}
noStroke();
fill(0);
text(nf(round(frameRate), 2, 0), 10, 20);
popStyle();
grey.release();
faces.release();
}
在setup()
函数中,您初始化两个分类器,一个用于您在前一个练习中使用的人脸。第二个分类器是一个新的分类器,其训练信息在haarcascade_smile.xml
中。在draw()
功能中,您还可以使用另一个版本的detectMultiScale()
功能。前两个参数是相同的。第三个参数是图像在每个比例下缩小的比例因子。数字越大,检测速度越快,但这是以不太准确为代价的。第四个参数是保留的最小邻居数量。较大的数量将消除更多的错误检测。第五个参数是一个伪参数。最后两个参数是您想要检测的对象(面部或微笑)的最小和最大尺寸。
在第一个for
循环中,显示所有椭圆形状的面。对于每个面,你使用包围矩形r
创建一个子矩阵(感兴趣的区域)fa
。然后你在这个小图像中检测出微笑,并在检测的中心画一条水平线。图 7-15 展示了一次成功的微笑检测。
图 7-15。
Successful smile detection
图 7-16 显示了另一个微笑检测不成功的试验。
图 7-16。
Unsuccessful smile detection
人物检测
除了常规的面部特征检测,OpenCV 中的objdetect
模块还通过HOGDescriptor
(梯度方向直方图)类提供了人物检测功能。你可以使用这个类从数字图像中检测整个人体。下面的练习Chapter07_15
将演示如何使用HOGDescriptor
功能从实时视频图像中检测人体。为了获得最佳效果,您需要在相对清晰的背景下检测整个身体。
// People detection
import processing.video.*;
import org.opencv.core.*;
import org.opencv.objdetect.HOGDescriptor;
// Detection size
final int W = 320, H = 240;
Capture
cap;
CVImage img;
// People detection
descriptor
HOGDescriptor hog;
float
ratio;
void setup() {
size(640, 480);
background(0);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
println(Core.VERSION);
cap = new Capture(this, width, height);
cap.start();
img = new CVImage(W, H);
// Initialize the descriptor.
hog = new HOGDescriptor();
// User the people detector.
hog.setSVMDetector(HOGDescriptor.getDefaultPeopleDetector());
ratio = float(width)/W;
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copy(cap, 0, 0, cap.width, cap.height,
0, 0, img.width, img.height);
img.copyTo();
image(cap, 0, 0);
Mat grey = img.getGrey();
MatOfRect found = new MatOfRect();
MatOfDouble weight = new MatOfDouble();
// Perform the people detection.
hog.detectMultiScale(grey, found, weight);
Rect [] people = found.toArray();
pushStyle();
fill(255, 255, 0, 100);
stroke(255);
// Draw the bounding boxes of people detected.
for (Rect r : people) {
rect(r.x*ratio, r.y*ratio, r.width*ratio, r.height*ratio);
}
grey.release();
found.release();
weight.release();
noStroke();
fill(0);
text(nf(round(frameRate), 2, 0), 10, 20);
popStyle();
}
相比人脸检测,程序更简单。您不需要加载任何经过训练的数据文件。您只需用下面的语句初始化HOGDescriptor class
实例hog
并设置默认的人员描述符信息:
hog.setSVMDetector(HOGDescriptor.getDefaultPeopleDetector());
在draw()
函数中,使用detectMultiScale()
方法从灰度图像grey
中识别人物,并将结果保存在MatOfRect
变量found
中。最后一个参数是一个伪参数。在for
循环中,用矩形绘制每个边界框r
。图 7-17 为程序截图。
图 7-17。
People detection
结论
在这一章中,你看到了从图像中识别关键点的不同方法。使用从两个连续帧中识别的关键点,可以执行稀疏光流分析或通用关键点描述符匹配来跟踪帧之间的视觉模式。这项技术对增强现实应用很有用。除了关键点跟踪,您还探索了 OpenCV 中面部特征和全身检测的简单使用。这些方法有利于艺术家和设计师通过计算机视觉进行具体化交互。在下一章中,您将了解在部署应用时使用处理的专业实践。