Processing OpenCV 计算机视觉高级教程(三)

原文:Pro Processing for Images and Computer Vision with OpenCV

协议:CC BY-NC-SA 4.0

六、理解运动

在上一章中,你学习了如何理解一帧图像中的内容。在这一章中,您将开始了解多帧数字视频或实时网络摄像头流中的运动。作为一个简单的解释,只要两个连续帧之间有差异,就可以识别运动。在计算机视觉中,你试图使用各种方法来理解这些差异,以便理解运动方向和前景背景分离等现象。在这一章的开始,我将介绍数字艺术家在处理动态图像时使用的现有方法。我将涉及的主题如下:

  • 运动图像的效果
  • 帧差分
  • 背景去除
  • 光流
  • 运动历史

运动图像的效果

在 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()函数中,程序的每一帧都会将网络摄像头视频图像的快照复制到一个更小的叫做imgPImage中。它将从左到右、从上到下遍历整个屏幕,将最新的帧粘贴到网格的每个单元格中。在粘贴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 帧记录到名为imgPImage数组中。用户还可以在那一秒钟内获得屏幕上 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 帧,你使用两个数组,imgshp。数组img将每个视频帧存储为一个PImage,它将作为纹理映射到数组shp的每个成员之上,如PShapedraw()功能管理整个图框块的旋转,如图 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);
}

在这个程序中,你可以按下鼠标键从网络摄像头直播流中录制一个静态图像,并将其作为背景帧存储在名为backPImage变量中。在每一帧中,在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,通过交换两个指针索引prevcurr来维护视频流中的前一帧和当前帧。其余的代码与前一个程序类似。它使用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()函数创建的。要使用该对象,您需要将视频帧和前景蒙版MatfgMask传递给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();
}

在这个程序中,您将显示四幅图像。左上方的是实时视频流。右上角的是静态背景图像,存储在名为backPImage实例中。左下角的是前景蒙版,如前一个练习所示。右下角的是显示在背景图像上面的前景图像。您还可以尝试另一种背景减除方法,即 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_contribopencv_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();
}

该程序使用一个名为bufMat数组来维护来自网络摄像头的两个连续帧。基本上,它利用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()功能找出运动方向。唯一的区别是您使用子矩阵作为每个图像mhiorientmask的感兴趣区域。其余部分与您在练习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

稀疏光流

您在第六章中学习了如何使用密集光流功能。在本节中,我将解释如何使用稀疏光流进行运动检测。在密集光流中,您检查并跟踪缩减像素采样图像中的所有像素,而在稀疏光流中,您只检查选定数量的像素。这些是您感兴趣跟踪的点,称为特征点。一般来说,它们是角点。以下是您需要遵循的步骤:

  1. 识别特征点。
  2. 提高分的准确性。
  3. 计算这些点的光流。
  4. 可视化流程信息。

识别特征点

下一个练习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()函数。它将返回名为cornersMatOfPoint变量中的特征点信息。剩余的三个参数是检测的点的最大数量、检测的质量水平和每个特征点之间的最小距离。将corners变量转换成名为pointsPoint数组后,循环遍历它,用从原始视频捕获图像中提取的颜色将每个角绘制成一个圆。图 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()函数之后,你得到一个名为cornersMatOfPoint变量中的特征点列表。新函数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的数组变量中保存了两个连续的灰度帧。它还需要在称为pointsMatOfPoint2f数组中保存两个连续的特征点列表。您使用整数变量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 版本中,有以下类型:

  • AKAZEDYNAMIC_AKAZEGRID_AKAZEPYRAMID_AKAZE
  • BRISKDYNAMIC_BRISKGRID_BRISKPYRAMID_BRISK
  • FASTDYNAMIC_FASTGRID_FASTPYRAMID_FAST
  • GFTTDYNAMIC_GFTTGRID_GFTTPYRAMIDGFTT
  • HARRISDYNAMIC_HARRISGRID_HARRISPYRAMID_HARRIS
  • MSERDYNAMIC_MSERGRID_MSERPYRAMID_MSER
  • ORBDYNAMIC_ORBGRID_ORBPYRAMID_ORB
  • SIMPLEBLOBDYNAMIC_SIMPLEBLOBGRID_SIMPLEBLOBPYRAMID_SIMPLEBLOB

在当前的练习中,您将使用类型FeatureDetector.ORB。各种探测器类型的详细描述超出了本书的范围。然而,您可以参考本章后面的图 7-9 来比较各种探测器类型。

draw()函数中,您使用方法fd.detect(grey, pt)来执行关键点检测,并将结果存储在名为ptMatOfKeyPoint实例中。将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;
  }
}

这个程序相对简单。你保存了两对数据结构。第一对是用于训练图像的MatOfKeyPointtrainKp和查询图像的queryKp。第二对由描述符trainDcqueryDC组成。当用户按下鼠标按钮时,它将拍摄当前视频流的快照,并使用该图像来计算训练的关键点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()方法内部的参数是匹配方法。支持以下暴力破解方法的变体:BRUTEFORCEBRUTEFORCE_HAMMINGBRUTEFORCE_HAMMINGLUTBRUTEFORCE_L1BRUTEFORCE_SL2。如果点击加工图像内部,将在draw()函数内部执行以下语句:

match.match(queryDc, trainDc, pairs);

match()功能将执行实时图像的关键点描述符queryDc和左侧存储图像的关键点描述符trainDc之间的匹配。变量pairs将所有匹配的关键点对存储为一个MatOfDMatch实例。DMatch是一种数据结构,用于维护存储在查询和训练关键点列表queryKptrainKp中的关键点queryIdxtrainIdx的匹配索引。之后的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;最后将选择矩形转换为名为trainRectMatOfPoint2f变量。

draw()函数中,您只需在DRAGGING状态下绘制临时选择矩形。在SELECTED状态下,你将调用matchPoints()函数,这是程序中最复杂的函数。在这个函数中,它首先从实时视频图像中检测关键点,并计算描述符。当训练描述符和查询描述符都不为空时,它执行关键点匹配。注意,经过训练的描述符trainDc仅包含选择矩形内的关键点描述。匹配后,该函数将遍历所有匹配对,找出名为pairsMatOfDMatch对象内的最小和最大距离。在随后的循环中,只处理距离小于最小距离值两倍的匹配对。在for循环之后,你将绘制连接所有匹配关键点的线,并从关键点列表中创建另外两个MatOfPoint2f变量trainMqueryM。当trainMqueryM都包含五个以上的关键点时,使用Calib3d.findHomography()方法从两个关键点列表中计算转换矩阵(单应矩阵)hg。通过单应矩阵hg,执行透视变换Core.perspectiveTransform(),将保存在trainRect中的选择矩形转换为queryRectqueryRect形状由转换后的矩形的四个角的坐标组成,位于窗口的右侧。本质上,四个角将定义跟踪图案的矩形。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 中面部特征和全身检测的简单使用。这些方法有利于艺术家和设计师通过计算机视觉进行具体化交互。在下一章中,您将了解在部署应用时使用处理的专业实践。

内容概要:本文《2025年全球AI Coding市场洞察研究报告》由亿欧智库发布,深入分析了AI编程工具的市场现状和发展趋势。报告指出,AI编程工具在2024年进入爆发式增长阶段,成为软件开发领域的重要趋势。AI编程工具不仅简化了代码生成、调试到项目构建等环节,还推动编程方式从人工编码向“人机协同”模式转变。报告详细评估了主流AI编程工具的表现,探讨了其商业模式、市场潜力及未来发展方向。特别提到AI Agent技术的发展,使得AI编程工具从辅助型向自主型跃迁,提升了任务执行的智能化和全面性。报告还分析了AI编程工具在不同行业和用户群体中的应用,强调了其在提高开发效率、减少重复工作和错误修复方面的显著效果。最后,报告预测2025年AI编程工具将在精准化和垂直化上进一步深化,推动软件开发行业进入“人机共融”的新阶段。 适合人群:具备一定编程基础,尤其是对AI编程工具有兴趣的研发人员、企业开发团队及非技术人员。 使用场景及目标:①了解AI编程工具的市场现状和发展趋势;②评估主流AI编程工具的性能和应用场景;③探索AI编程工具在不同行业中的具体应用,如互联网、金融、游戏等;④掌握AI编程工具的商业模式和盈利空间,为企业决策提供参考。 其他说明:报告基于亿欧智库的专业研究和市场调研,提供了详尽的数据支持和前瞻性洞察。报告不仅适用于技术从业者,也适合企业管理者和政策制定者,帮助他们在技术和商业决策中更好地理解AI编程工具的价值和潜力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值