Processing OpenCV 计算机视觉高级教程(二)
原文:Pro Processing for Images and Computer Vision with OpenCV
四、几何与变换
在本章中,您将继续处理数码图像的转换。在前一章中,您主要修改了图像的像素颜色信息,在处理中使用了内置函数和自定义函数。在这一章中,你将关注于在不改变图像内容的情况下使图像的像素网格变形。本质上,这改变了图像中每个像素的位置,从而修改了原始图像的几何形状。由于处理语言缺乏这样的功能,您将使用 OpenCV 来处理这些练习。同时,你将探索在三维特征处理中实现数字图像的几何变换。以下是本章涵盖的主题:
- 图像变换
- 图象取向
- 图像大小调整
- 仿射变换
- 透视变换
- 线性坐标与极坐标
- 三维空间
- 普通像素映射
图像变换
第一种类型的图像转换是翻译。在这种类型中,如图 4-1 所示,由矩形网格定义的整个数字图像在水平或垂直方向上移动。图像的大小和方向在变换前后保持不变。
图 4-1。
Image translation
本章介绍的第二种和第三种变换会改变图像的方向。它们是旋转和翻转。旋转时,图像在 2D 平面上沿假想的 z 轴旋转,没有任何尺寸变化或变形,如图 4-2 所示。在处理过程中,旋转的锚点是左上角(0,0)。
图 4-2。
Image rotation
翻转是沿着 x 轴和/或 y 轴的反射。在您将要进行的练习中,您可以在单个轴或两个轴上翻转图像。图 4-3 显示了正方形图像的垂直翻转。
图 4-3。
Image flipping
这三种类型的变换保留了原始图像的大小和形状。然而,图 4-4 所示的下一种变换将改变图像的大小。这是一个调整大小的变换。
图 4-4。
Image resize
前面介绍的四种变换保持了图像的形状。下一种类型,仿射变换,将扭曲原始形状,但它仍然保留平行线。矩形像素网格会变换成平行四边形,如图 4-5 所示。
图 4-5。
Affine transform
我要介绍的最后一种几何变换是透视变换。它将矩形图像网格转换为任意四点凸多边形。该变换还对应于透视投影,其中用附近的照相机将 3D 对象投影到 2D 平面上。图 4-6 显示了一个透视变换的例子。
图 4-6。
Perspective transform
图象取向
通过图像定向,我指的是诸如在二维平面中翻转和旋转图像的任务。通过使用flip()函数,在 OpenCV 中很容易实现翻转或反射图像。在二维图形中,您可以让沿水平轴、垂直轴或两个轴翻转。flip()函数的语法和参数如下:
public static void flip(Mat src, Mat dst, int flipCode);
该命令将根据flipCode值中指定的内容,将src矩阵翻转为dst矩阵。flipCode的零值将沿 x 轴翻转,正值将沿 y 轴翻转,负值将沿两个轴翻转。下面的练习Chapter04_01演示了翻转在 OpenCV 处理中的使用。记住,如第一章所述,在加工草图和CVImage类定义中包含code文件夹。code文件夹包含所有必需的 OpenCV Java 和本地文件。处理窗口大小适合并排显示两个图像。原始图像(600×600 像素)将位于左侧,翻转后的图像将位于右侧。
import org.opencv.core.*;
PImage img;
CVImage cv;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
cv = new CVImage(img.width, img.height);
noLoop();
}
void draw() {
background(0);
cv.copyTo(img);
Mat mat = cv.getBGR();
Core.flip(mat, mat, -1);
cv.copyTo(mat);
image(img, 0, 0);
image(cv, img.width, 0);
mat.release();
}
程序生成的图像包含两部分,如图 4-7 所示。左边是原始图像,右边是沿两个轴翻转的图像。
图 4-7。
Transform with flip in both axes
下一个练习Chapter04_02,将帮助您学习图像旋转的命令。旋转图像需要遵循两个步骤。第一步是计算旋转变换矩阵。第二步是对源图像应用旋转变换矩阵。获取旋转矩阵的第一个命令的语法如下:
public static Mat Imgproc.getRotationMatrix2D(Point center, double angle, double scale)
第一个参数center,是源图像中旋转中心点的坐标。第二个参数angle是以度为单位测量的旋转角度。请注意,处理旋转以弧度为单位,而 OpenCV 旋转以度为单位。第三个参数scale,是转换中应用的比例因子。该函数将输出一个 2×3 的矩阵,如下所示:
a b (1-a)*center.x-b*center.y
-b a b*center.x+(1-a)*center.y
这里,a = scale*cos(angle)和b = scale*sin(angle)。
一旦你有了旋转变换矩阵,你可以用warpAffine()函数将矩阵应用到源图像。语法如下:
public static void Imgproc.warpAffine(Mat src, Mat dst, Mat m, Size dsize)
第一个参数src是源图像。第二个参数dst是目标图像,其类型与src相同,大小与第四个参数dsize中指定的相同。第三个参数,m,是从上一步获得的旋转变换矩阵。第四个参数dsize是目标图像的大小。同样,确保带有 OpenCV 库和CVImage类的code文件夹在加工草图文件夹中。原始图像为 600×600 像素。旋转后的图像将显示在原始图像的右侧。本练习的完整源代码Chapter04_02如下所示:
import org.opencv.core.*;
import org.opencv.imgproc.*;
CVImage cvout;
Mat in;
PImage img;
Point ctr;
float angle;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
CVImage cvin = new CVImage(img.width, img.height);
cvout = new CVImage(cvin.width, cvin.height);
cvin.copyTo(img);
in = cvin.getBGR();
ctr = new Point(img.width/2, img.height/2);
angle = 0;
frameRate(30);
}
void draw() {
background(0);
Mat rot = Imgproc.getRotationMatrix2D(ctr, angle, 1.0);
Mat out = new Mat(in.size(), in.type());
Imgproc.warpAffine(in, out, rot, out.size());
cvout.copyTo(out);
image(img, 0, 0);
image(cvout, img.width, 0);
angle += 0.5;
angle %= 360;
out.release();
rot.release();
}
在代码中,你使用CVImage cvout来保持旋转后的图像。Mat in以 OpenCV 矩阵格式保存输入图像。OpenCV 类Point ctr以图像的中心作为旋转的支点。float angle是当前旋转角度。在draw()功能中,每转一圈,它将增加半度。图 4-8 显示了运行草图的加工窗口示例。
图 4-8。
Rotation transform with digital image
通过使用处理tint()功能,您可以在旋转显示中获得更多乐趣。在使用image()函数之前,你可以通过指定一个小于 255 的 alpha 值来改变填充颜色的透明度,比如tint(255, 20)。在draw()功能中,如果去掉background(0),增加两个tint()功能,就可以实现旋转图像中的运动模糊效果。新的draw()功能如下:
void draw() {
// background(0);
Mat rot = Imgproc.getRotationMatrix2D(ctr, angle, 1.0);
Mat out = new Mat(in.size(), in.type());
Imgproc.warpAffine(in, out, rot, out.size());
cvout.copyTo(out);
tint(255, 255);
image(img, 0, 0);
tint(255, 20);
image(cvout, img.width, 0);
angle += 0.5;
angle %= 360;
out.release();
rot.release();
}
在image(cvout, img.width, 0)之前的tint(255, 20)函数将设置透明的填充颜色。在这种情况下,只有旋转图像会有运动模糊效果,而不是左侧的原始图像。图 4-9 显示了结果。
图 4-9。
Rotation transform with motion blur
图像大小调整
在前面的部分中,变换翻转和旋转不会改变图像的大小/面积。如果您想要更改图像大小,同时保持其形状,可以使用调整大小变换。该函数是来自 OpenCV Imgproc模块的resize(),如下图所示:
public static void Imgproc.resize(Mat src, Mat dst, Size dsize)
第一个参数src是源图像。第二个参数dst是目标图像。第三个参数,dsize,是目标图像的大小。它属于 OpenCV Size类。下图Chapter04_03展示了resize()功能在图形合成中的使用。程序中原始图像的大小为 800×600 像素。
import org.opencv.core.*;
import org.opencv.imgproc.*;
PImage img;
CVImage cv;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
cv = new CVImage(img.width, img.height);
cv.copyTo(img);
noLoop();
}
void draw() {
background(0);
Mat in = cv.getBGR();
Mat out = new Mat(new Size(img.width*0.5, img.height*0.5), in.type());
Imgproc.resize(in, out, out.size());
CVImage small = new CVImage(out.cols(), out.rows());
small.copyTo(out);
image(img, 0, 0);
tint(255, 100, 100);
image(small, img.width, 0);
tint(100, 100, 255);
image(small, img.width, small.height);
}
该程序创建了原始图像img的副本,宽度和高度都是原来的一半。较小的图像small在加工窗口的右侧以不同的色调显示两次,如图 4-10 所示。
图 4-10。
Resize transform with color tint
不使用 OpenCV,您也可以使用PImage类的copy()方法获得相同的结果。下一个练习,Chapter04_04,将展示你如何用copy()方法创作同样的作品。测试图像的尺寸为 800×600 像素。
PImage img;
void setup() {
size(1200, 600);
img = loadImage("hongkong.png");
noLoop();
}
void draw() {
background(0);
PImage small = createImage(round(img.width*0.5),
round(img.height*0.5), ARGB);
small.copy(img, 0, 0, img.width, img.height,
0, 0, small.width, small.height);
small.updatePixels();
image(img, 0, 0);
tint(255, 100, 100);
image(small, img.width, 0);
tint(100, 100, 255);
image(small, img.width, small.height);
}
copy()方法将像素从原始图像img复制到目标图像small。除了源图像之外,这些参数还包括源图像和目标图像的偏移量(x, y)和大小(width, height)。
仿射变换
下一个几何变换是仿射变换,它可以在变换中保留平行线。要定义转换矩阵,您需要在源图像中有三个点,以及它们在目标图像中的相应位置。在下一个练习Chapter04_05中,您将使用图像的左上角、右上角和右下角来定义变换。假设你有原始图像,img。来自源图像的三个点如下:
0, 0img.width-1, 0img.width-1, img.height-1
仿射变换后,假设这三个点将分别移动到以下位置:
50, 50img.width-100, 100img.width-50, img.height-100
在这个程序中,你需要根据六个角点的映射来计算变换矩阵。使用矩阵,您可以将其应用于整个图像以创建输出图像。用于测试的图像尺寸为 600×600 像素。代码如下:
import org.opencv.core.*;
import org.opencv.imgproc.*;
PImage img;
CVImage cv;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
cv = new CVImage(img.width, img.height);
cv.copyTo(img);
noLoop();
}
void draw() {
background(0);
MatOfPoint2f srcMat = new MatOfPoint2f(new Point(0, 0),
new Point(img.width-1, 0),
new Point(img.width-1, img.height-1));
MatOfPoint2f dstMat = new MatOfPoint2f(new Point(50, 50),
new Point(img.width-100, 100),
new Point(img.width-50, img.height-100));
Mat affine = Imgproc.getAffineTransform(srcMat, dstMat);
Mat in = cv.getBGR();
Mat out = new Mat(in.size(), in.type());
Imgproc.warpAffine(in, out, affine, out.size());
cv.copyTo(out);
image(img, 0, 0);
image(cv, img.width, 0);
in.release();
out.release();
affine.release();
}
draw()功能有两个步骤。第一个是基于六个角点计算变换矩阵。这是通过Imgproc.getAffineTransform()功能完成的。
public static Mat Imgproc.getAffineTransform(MatOfPoint2f src, MatOfPoint2f dst)
第一个参数由源图像中的三个点组成。第二个参数由目标图像中的三个对应点组成。两个参数都属于 OpenCV 类MatOfPoint2f。它类似于 C++中的vector和 Java 中的ArrayList。你可以把它看作是基类Point的有序集合。第二步是对源图像in应用仿射矩阵,并使用您在上一节中学习的warpAffine()函数生成目标矩阵out。图 4-11 显示了加工窗口中显示的结果图像。
图 4-11。
Affine transform
下一个练习Chapter04_06,是仿射变换在图像处理中更实际的应用。该计划将允许用户改变锚点,以操纵变形的程度。在源代码中,您将引入另外一个类Corner,来表示您可以拖动来改变转换的每个锚点。Corner类的定义如下:
public class Corner {
float radius;
PVector pos;
boolean picked;
public Corner(float x, float y) {
pos = new PVector(x, y);
radius = 10.0;
picked = false;
}
PVector getPos() {
return pos;
}
void drag(float x, float y) {
if (picked) {
PVector p = new PVector(x, y);
pos.set(p.x, p.y);
}
}
void pick(float x, float y) {
PVector p = new PVector(x, y);
float d = p.dist(pos);
if (d < radius) {
picked = true;
pos.set(p.x, p.y);
}
}
void unpick() {
picked = false;
}
void draw() {
pushStyle();
fill(255, 255, 0, 160);
noStroke();
ellipse(pos.x, pos.y, radius*2, radius*2);
popStyle();
}
}
该类将显示一个圆圈来指示数字图像的角。在仿射变换中,只使用三个角。在本练习中,您将使用左上角、右上角和右下角。用户可以单击并拖动来移动角点。在下一节中使用透视转换时,您将重用该类。这里显示了Chapter04_06的主程序:
import org.opencv.core.*;
import org.opencv.imgproc.*;
PImage img;
CVImage cvout;
PVector offset;
MatOfPoint2f srcMat, dstMat;
Mat in;
Corner [] corners;
void setup() {
size(720, 720);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
CVImage cvin = new CVImage(img.width, img.height);
cvin.copyTo(img);
in = cvin.getBGR();
cvout = new CVImage(img.width, img.height);
offset = new PVector((width-img.width)/2, (height-img.height)/2);
srcMat = new MatOfPoint2f(new Point(0, 0),
new Point(img.width-1, 0),
new Point(img.width-1, img.height-1));
dstMat = new MatOfPoint2f();
corners = new Corner[srcMat.rows()];
corners[0] = new Corner(0+offset.x, 0+offset.y);
corners[1] = new Corner(img.width-1+offset.x, 0+offset.y);
corners[2] = new Corner(img.width-1+offset.x, img.height-1+offset.y);
}
void draw() {
background(0);
drawFrame();
Point [] points = new Point[corners.length];
for (int i=0; i<corners.length; i++) {
PVector p = corners[i].getPos();
points[i] = new Point(p.x-offset.x, p.y-offset.y);
}
dstMat.fromArray(points);
Mat affine = Imgproc.getAffineTransform(srcMat, dstMat);
Mat out = new Mat(in.size(), in.type());
Imgproc.warpAffine(in, out, affine, out.size());
cvout.copyTo(out);
image(cvout, offset.x, offset.y);
for (Corner c : corners) {
c.draw();
}
out.release();
affine.release();
}
void
drawFrame() {
pushStyle();
noFill();
stroke(100);
line(offset.x-1, offset.y-1,
img.width+offset.x, offset.y-1);
line(img.width+offset.x, offset.y-1,
img.width+offset.x, img.height+offset.y);
line(offset.x-1, img.height+offset.y,
img.width+offset.x, img.height+offset.y);
line(offset.x-1, offset.y-1,
offset.x-1, img.height+offset.y);
popStyle();
}
void
mousePressed() {
for (Corner c : corners) {
c.pick(mouseX, mouseY);
}
}
void mouseDragged() {
for (Corner c : corners) {
if (mouseX<offset.x ||
mouseX>offset.x+img.width ||
mouseY<offset.y ||
mouseY>offset.y+img.height)
continue;
c.drag(mouseX, mouseY);
}
}
void mouseReleased() {
for (Corner c : corners) {
c.unpick();
}
}
该程序添加了鼠标事件处理程序来管理由Corner类定义的带有锚点的鼠标点击动作。在draw()函数中,使用另一种方法来初始化dstMat矩阵。您已经将points定义为一个Point数组。在每一帧中,将锚点信息从corners复制到points,并使用fromArray()方法初始化dstMat以进行后续处理。程序的其余部分与上一个类似。图 4-12 显示了程序的可视化显示。
图 4-12。
Interactive affine transform
透视变换
透视变换的用法类似于上一节中的仿射变换,只是您需要使用四个点而不是三个点来定义变换。在变换之后,它不能像在仿射变换中那样保持平行线。生成透视变换矩阵的函数如下:
public static Mat Imgproc.getPerspectiveTransform(MatOfPoint2f src, MatOfPoint2f dst)
第一个参数src,是来自源图像的四个锚点的集合(MatOfPont2f))。在练习中,Chapter04_07,你使用输入图像的四个角,img。它们如下:
0, 0:左上角img.width-1, 0:右上角img.width-1, img.height-1:右下角0, img.height-1:左下角
第二个参数dst,是变换后输出图像的四个角点的集合(MatOfPoint2f))。您采用上一个练习中的类Corner。用户可以单击/拖动角点,以交互方式更改变换矩阵。源代码和前面的差不多。您只需用透视变换替换仿射变换,并使用四个点而不是三个点。这里使用的原始图像的大小是 700×700 像素。
PImage img;
CVImage cvout;
PVector offset;
MatOfPoint2f srcMat, dstMat;
Mat in;
Corner [] corners;
void
setup() {
size(720, 720);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("hongkong.png");
CVImage cvin = new CVImage(img.width, img.height);
cvin.copyTo(img);
in = cvin.getBGR();
cvout = new CVImage(img.width, img.height);
offset = new PVector((width-img.width)/2, (height-img.height)/2);
srcMat = new MatOfPoint2f(new Point(0, 0),
new Point(img.width-1, 0),
new Point(img.width-1, img.height-1),
new Point(0, img.height-1));
dstMat = new MatOfPoint2f();
corners = new Corner[srcMat.rows()];
corners[0] = new Corner(0+offset.x, 0+offset.y);
corners[1] = new Corner(img.width-1+offset.x, 0+offset.y);
corners[2] = new Corner(img.width-1+offset.x, img.height-1+offset.y);
corners[3] = new Corner(0+offset.x, img.height-1+offset.y);
}
void
draw() {
background(0);
drawFrame();
Point [] points = new Point[corners.length];
for (int i=0; i<corners.length; i++) {
PVector p = corners[i].getPos();
points[i] = new Point(p.x-offset.x, p.y-offset.y);
}
dstMat.fromArray(points);
Mat transform = Imgproc.getPerspectiveTransform(srcMat, dstMat);
Mat out = new Mat(in.size(), in.type());
Imgproc.warpPerspective(in, out, transform, out.size());
cvout.copyTo(out);
image(cvout, offset.x, offset.y);
for (Corner c : corners) {
c.draw();
}
out.release();
transform.release();
}
void
drawFrame() {
pushStyle();
noFill();
stroke(100);
line(offset.x-1, offset.y-1,
img.width+offset.x, offset.y-1);
line(img.width+offset.x, offset.y-1,
img.width+offset.x, img.height+offset.y);
line(offset.x-1, img.height+offset.y,
img.width+offset.x, img.height+offset.y);
line(offset.x-1, offset.y-1,
offset.x-1, img.height+offset.y);
popStyle();
}
void
mousePressed() {
for (Corner c : corners) {
c.pick(mouseX, mouseY);
}
}
void mouseDragged() {
for (Corner c : corners) {
if (mouseX<offset.x ||
mouseX>offset.x+img.width ||
mouseY<offset.y ||
mouseY>offset.y+img.height)
continue;
c.drag(mouseX, mouseY);
}
}
void mouseReleased() {
for (Corner c : corners) {
c.unpick();
}
}
为了执行透视变换,你使用新的warpPerspective()函数和矩阵transform,它是从上一个getPerspectiveTransform()矩阵生成的,如图 4-13 所示。
图 4-13。
Perspective transform with interactivity
请注意,当用户单击/拖动角点时,您不会检查新形状是否是凸形的。当新形状不是凸形时,可能会导致图像失真。
线性坐标与极坐标
您使用的 x,y 坐标系统是线性的,或笛卡尔坐标。这两个轴是互相垂直的直线。除了线性坐标系,你还可以用半径和角度的测量来表示二维平面上的一个点(x,y),如图 4-14 所示。
图 4-14。
Linear and polar coordinates
OpenCV 通过图像处理模块Imgproc提供了将图像从线性坐标空间转换到极坐标空间的转换函数。该功能如下所示:
public static void Imgproc.linearPolar(Mat src, Mat dst, Point center, double maxRadius, int flags)
第一个参数src是源图像。第二个参数dst是目标图像,其大小和类型与源图像相同。第三个参数,center,是转换中心。你通常把它设置在图像的中心。第四个参数maxRadius是要变换的边界圆的半径。第五个参数flags,是插值方法的组合。使用双线性插值INTER_LINEAR,填充所有目标像素WARP_FILL_OUTLIERS。在演示练习Chapter04_08中,您将使用实时网络摄像头作为输入图像,并并排显示源图像和转换后的图像。
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.*;
Capture cap;
CVImage img, out;
int capW, capH;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
capW = width/2;
capH = height;
cap = new Capture(this, capW, capH);
cap.start();
img = new CVImage(cap.width, cap.height);
out = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat linear = img.getBGR();
Mat polar = new Mat();
Point ctr = new Point(cap.width/2, cap.height/2);
double radius = min(cap.width, cap.height)/2.0;
Imgproc.linearPolar(linear, polar, ctr, radius,
Imgproc.INTER_LINEAR+Imgproc.WARP_FILL_OUTLIERS);
out.copyTo(polar);
image(cap, 0, 0);
image(out, cap.width, 0);
linear.release();
polar.release();
}
在本练习中,您将半径设置为视频图像高度的一半。图 4-15 显示了加工窗口图像。
图 4-15。
Linear to polar transform
OpenCV 还提供了另一种极坐标变换,logPolar()。它类似于linearPolar()函数,只是它使用距离的自然对数。以下练习Chapter04_09展示了如何使用logPolar()功能处理网络摄像头图像:
import processing.video.*;
import org.opencv.core.*;
import org.opencv.imgproc.*;
Capture cap;
CVImage img, out;
int capW, capH;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
capW = width/2;
capH = height;
cap = new Capture(this, capW, capH);
cap.start();
img = new CVImage(cap.width, cap.height);
out = new CVImage(cap.width, cap.height);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat linear = img.getBGR();
Mat polar = new Mat();
Point ctr = new Point(cap.width/2, cap.height/2);
double radius = (double)min(cap.width, cap.height)/2.0;
double m = (double)cap.width/log((float)radius);
Imgproc.logPolar(linear, polar, ctr, m,
Imgproc.INTER_LINEAR+Imgproc.WARP_FILL_OUTLIERS);
out.copyTo(polar);
image(cap, 0, 0);
image(out, cap.width, 0);
linear.release();
polar.release();
}
该功能声称模仿人类的“视网膜中央凹”视觉,看起来更“自然”图 4-16 显示了结果图像。
图 4-16。
Linear to log polar transform
三维空间
除了使用 OpenCV 中的图像处理模块,您还可以使用处理中的 3D 图形功能来转换图像。在本书的大部分练习中,您使用image()函数直接在屏幕上显示PImage对象实例。在处理过程中,还有其他方式来显示图像。下面的练习Chapter04_10演示了如何使用PShape类( https://processing.org/reference/PShape.html )和PImage类作为贴图纹理。这个练习由两幅图像组成。第一个是来自一个PImage实例的背景图像。第二个是来自Capture实例的前景图像。3D 空间中的旋转模拟透视变换。
import processing.video.*;
Capture cap;
PImage img;
PShape canvas;
int capW, capH;
float angle;
void setup() {
size(800, 600, P3D);
hint(DISABLE_DEPTH_TEST);
capW = 640;
capH = 480;
cap = new Capture(this, capW, capH);
cap.start();
img = loadImage("hongkong.png");
canvas = createShape(RECT, 0, 0, cap.width, cap.height);
canvas.setStroke(false);
canvas.setTexture(cap);
shapeMode(CENTER);
angle = 0;
}
void draw() {
if (!cap.available())
return;
cap.read();
background(0);
image(img, 0, 0);
translate(width/2, height/2, -100);
rotateX(radians(angle));
shape(canvas, 0, 0);
angle += 0.5;
angle %= 360;
}
程序的第一个变化是size()功能。它有一个额外的参数P3D,表示您现在处于 3D 显示模式。高级用户可以使用 OpenGL 提示功能来控制渲染参数( https://processing.org/tutorials/rendering/ )。您在这里使用的hint()函数有一个参数来禁用渲染中的深度测试,这样前面的表面就不会遮挡后面的表面。在setup()函数中,您用下面的函数定义了PShape实例canvas:
createShape(RECT, 0, 0, cap.width, cap.height);
这将创建一个矩形形状RECT,左上角为(0,0),宽度和高度等于网络摄像头捕捉的宽度和高度。下一条语句禁用描边颜色。您还可以使用canvas.setTexture(cap)将来自网络摄像头的PImage cap关联为形状的纹理canvas。在draw()功能中,首先将背景清除为黑色,然后在加工窗口上直接显示背景图像img。translate()功能将图形移动到屏幕中心,在 z 方向也是负值。rotateX()功能将图形沿 X 轴旋转angle中指定的量。请注意,旋转以弧度为度量单位。如果使用的是度,则需要使用radians()功能进行转换。最后一步是使用shape()函数在中心(0,0)显示PShape实例canvas。还要注意,在setup()函数中,您将shapeMode设置为CENTER,而不是左上角。示例显示将类似于图 4-17 所示。
图 4-17。
Perspective transform in Processing by rotation
在上一个练习中,您使用内置矩形形状来定义形状。事实上,你可以定义你自己的顶点。下面的练习Chapter04_11,将使用一系列vertex()命令定义相同的图形。之后,您可以检索单个顶点并改变其位置,以实现更动态的动画。
import processing.video.*;
Capture cap;
PImage img;
PShape canvas;
int capW, capH;
float angle;
int vCnt;
void setup() {
size(800, 600, P3D);
hint(DISABLE_DEPTH_TEST);
capW = 640;
capH = 480;
cap = new Capture(this, capW, capH);
cap.start();
img = loadImage("hongkong.png");
canvas = createShape();
canvas.beginShape();
canvas.textureMode(NORMAL);
canvas.texture(cap);
canvas.noStroke();
canvas.vertex(0, 0, 0, 0, 0);
canvas.vertex(cap.width, 0, 0, 1, 0);
canvas.vertex(cap.width, cap.height, 0, 1, 1);
canvas.vertex(0, cap.height, 0, 0, 1);
canvas.endShape(CLOSE);
shapeMode(CENTER);
angle = 0;
vCnt = canvas.getVertexCount();
}
void draw() {
if (!cap.available())
return;
cap.read();
background(0);
image(img, 0, 0);
for (int i=0; i<vCnt; i++) {
PVector pos = canvas.getVertex(i);
if (i < 2) {
pos.z = 100*cos(radians(angle*3));
} else {
pos.z = 100*sin(radians(angle*5));
}
canvas.setVertex(i, pos);
}
translate(width/2, height/2, -100);
rotateY(radians(angle));
shape(canvas, 0, 0);
angle += 0.5;
angle %= 360;
}
图 4-18 显示了结果。
图 4-18。
Perspective transform with custom shape
前面的练习仅使用矩形的四个角来指定矩形。图像没有失真太多。如果使用线框栅格作为纹理贴图的骨架,可以修改每个点以进一步扭曲图像。图 4-19 显示了网格和映射在其上的图像。
图 4-19。
Image text-mapped on a grid
要在处理中定义网格,您可以使用QUAD_STRIP形状。首先,为整个网格定义一个GROUP形状。第二,将网格的每一行创建为一个QUAD_STRIP形状。要在QUAD_STRIP中创建一个单元格,您必须按以下顺序定义点:左上、左下、右上和右下。第三,将每一行作为子对象添加到GROUP形状中。图 4-20 说明了该形状的详细配置。
图 4-20。
A GROUP shape with QUAD_STRIP as children
下一个练习Chapter04_12,展示了如何使用一个叫做canvas的GROUP PShape来定义详细纹理映射的网格:
import processing.video.*;
Capture cap;
PShape canvas;
int capW, capH;
float step;
void setup() {
size(800, 600, P3D);
hint(DISABLE_DEPTH_TEST);
capW = 640;
capH = 480;
step = 40;
cap = new Capture(this, capW, capH);
cap.start();
initShape();
shapeMode(CENTER);
}
void initShape() {
// initialize the GROUP PShape grid
canvas = createShape(GROUP);
int nRows = floor(cap.height/step) + 1;
int nCols = floor(cap.width/step) + 1;
for (int y=0; y<nRows-1; y++) {
// initialize each row of the grid
PShape tmp = createShape();
tmp.beginShape(QUAD_STRIP);
tmp.texture(cap);
for (int x=0; x<nCols; x++) {
// initialize the top-left, bottom-left points
int x1 = (int)constrain(x*step, 0, cap.width-1);
int y1 = (int)constrain(y*step, 0, cap.height-1);
int y2 = (int)constrain((y+1)*step, 0, cap.height-1);
tmp.vertex(x1, y1, 0, x1, y1);
tmp.vertex(x1, y2, 0, x1, y2);
}
tmp.endShape();
canvas.addChild(tmp);
}
}
void draw() {
if (!cap.available())
return;
cap.read();
background(100);
translate(width/2, height/2, -80);
rotateX(radians(20));
shape(canvas, 0, 0);
}
难的部分在initShape()函数中完成。首先将整个网格的PShape定义为
canvas = createShape(GROUP)
然后遍历网格中的每个单元格。请注意,您需要通过将总行数nRows和总列数nCols加 1 来处理右边距和下边距。对于每一行,您将临时变量tmp定义为一个QUAD_STRIP形状。创建完QUAD_STRIP中的所有顶点后,使用canvas.addChild(tmp)将其添加到canvas形状中。现在,您可以将视频捕获图像作为纹理映射到网格上。然而,你不会就此止步。您的目的是改变顶点在屏幕上的 z 位置,这样您就可以获得视频捕获的失真图像。
下一个练习Chapter04_13,将在网格中维护一个二维的顶点数组。此外,您将为每个顶点设置一个随机的初始 z 位置。
import processing.video.*;
Capture cap;
PShape canvas;
int capW, capH;
float step;
PVector [][] points;
float angle;
void setup() {
size(800, 600, P3D);
hint(DISABLE_DEPTH_TEST);
capW = 640;
capH = 480;
step = 20;
cap = new Capture(this, capW, capH);
cap.start();
initGrid();
initShape();
shapeMode(CENTER);
angle = 0;
}
void initGrid() {
// initialize the matrix of points for texture mapping
points = new PVector[floor(cap.height/step)+1][floor(cap.width/step)+1];
for (int y=0; y<points.length; y++) {
for (int x=0; x<points[y].length; x++) {
float xVal = constrain(x*step, 0, cap.width-1);
float yVal = constrain(y*step, 0, cap.height-1);
// random z value
points[y][x] = new PVector(xVal, yVal, noise(x*0.2, y*0.2)*60-30);
}
}
}
void initShape() {
// initialize the GROUP PShape grid
canvas = createShape(GROUP);
for (int y=0; y<points.length-1; y++) {
// initialize each row of the grid
PShape tmp = createShape();
tmp.beginShape(QUAD_STRIP);
tmp.noStroke();
tmp.texture(cap);
for (int x=0; x<points[y].length; x++) {
PVector p1 = points[y][x];
PVector p2 = points[y+1][x];
tmp.vertex(p1.x, p1.y, p1.z, p1.x, p1.y);
tmp.vertex(p2.x, p2.y, p2.z, p2.x, p2.y);
}
tmp.endShape();
canvas.addChild(tmp);
}
}
void draw() {
if (!cap.available())
return;
cap.read();
lights();
background(100);
translate(width/2, height/2, -100);
rotateX(radians(angle*1.3));
rotateY(radians(angle));
shape(canvas, 0, 0);
angle += 0.5;
angle %= 360;
}
被称为points的PVector的 2D 数组维护网格的所有顶点。函数initGrid()初始化位置信息。对于 z 位置,您使用 Perlin 噪声函数来初始化它。initShape()函数将复制points数组中的信息,用适当的纹理映射创建GROUP PShape、canvas。请注意,您还可以使用lights()功能来启用draw()功能中的默认照明条件。生成的图像(如图 4-21 所示)将类似于 3D 地形,网络摄像头图像映射在其上。
图 4-21。
Texture map with irregular surface
在上一个练习中,请注意栅格中的顶点没有移动。它们在setup()函数中创建一次,没有任何进一步的改变。在下一个练习Chapter04_14中,您将尝试根据网络摄像头图像制作顶点动画,以便获得交互式观看体验。不是为每个顶点的 z 位置输入一个随机数,而是通过使用来自网络摄像头的颜色信息来改变它的值。在加工中,默认的颜色模式是RGB。但是,如果您想要明确使用亮度信息,可以将其切换到 HSB(色调、饱和度、亮度)。在本练习中,您的目标是用该像素的亮度信息交换顶点的 z 位置。可以使用的功能是brightness()。
import processing.video.*;
Capture cap;
int capW, capH;
float step;
PVector [][] points;
float angle;
PShape canvas;
void setup() {
size(800, 600, P3D);
hint(DISABLE_DEPTH_TEST);
capW = 640;
capH = 480;
step = 10;
cap = new Capture(this, capW, capH);
cap.start();
initGrid();
initShape();
shapeMode(CENTER);
angle = 0;
}
void initGrid() {
// initialize the matrix of points for texture mapping
points = new PVector[floor(cap.height/step)+1][floor(cap.width/step)+1];
for (int y=0; y<points.length; y++) {
for (int x=0; x<points[y].length; x++) {
float xVal = constrain(x*step, 0, cap.width-1);
float yVal = constrain(y*step, 0, cap.height-1);
points[y][x] = new PVector(xVal, yVal, 0);
}
}
}
void initShape() {
canvas = createShape(GROUP);
for (int y=0; y<points.length-1; y++) {
// initialize each row of the grid
PShape tmp = createShape();
tmp.beginShape(QUAD_STRIP);
tmp.noFill();
for (int x=0; x<points[y].length; x++) {
PVector p1 = points[y][x];
PVector p2 = points[y+1][x];
tmp.vertex(p1.x, p1.y, p1.z);
tmp.vertex(p2.x, p2.y, p2.z);
}
tmp.endShape();
canvas.addChild(tmp);
}
}
color getColor(int x, int y) {
// obtain color information from cap
int x1 = constrain(floor(x*step), 0, cap.width-1);
int y1 = constrain(floor(y*step), 0, cap.height-1);
return cap.get(x1, y1);
}
void updatePoints() {
// update the depth of vertices using color
// brightness from cap
float factor = 0.3;
for (int y=0; y<points.length; y++) {
for (int x=0; x<points[y].length; x++) {
color c = getColor(x, y);
points[y][x].z = brightness(c)*factor;
}
}
}
void updateShape() {
// update the color and depth of vertices
for (int i=0; i<canvas.getChildCount(); i++) {
for (int j=0; j<canvas.getChild(i).getVertexCount(); j++) {
PVector p = canvas.getChild(i).getVertex(j);
int x = constrain(floor(p.x/step), 0, points[0].length-1);
int y = constrain(floor(p.y/step), 0, points.length-1);
p.z = points[y][x].z;
color c = getColor(x, y);
canvas.getChild(i).setStroke(j, c);
canvas.getChild(i).setVertex(j, p);
}
}
}
void draw() {
if (!cap.available())
return;
cap.read();
updatePoints();
updateShape();
background(0);
translate(width/2, height/2, -100);
rotateX(radians(angle));
shape(canvas, 0, 0);
angle += 0.5;
angle %= 360;
}
initGrid()函数初始化点数组。initShape()函数使用来自点数组的信息来初始化PShape canvas。在该函数中,您不需要直接在每个QUAD_STRIP子节点中设置纹理。您可以启用描边颜色,但禁用子形状的填充颜色。在draw()函数中,你编写了updatePoints()函数来根据颜色亮度更新顶点的 z 位置。updateShape()函数遍历PShape canvas的所有子节点,并更新顶点的 z 位置和笔画颜色。图 4-22 显示了显示窗口的示例。
图 4-22。
3D effect using brightness as depth
请注意,在图像中,颜色较深的区域看起来较深,而较亮的区域在网格平面中较高。如果将initShape()函数中的tmp.noFill()语句改为tmp.noStroke(),将updateShape()函数中的canvas.getChild(i).setStroke(j, c)语句改为canvas.getChild(i).setFill(j, c),就可以将线框显示切换为实心版本,如图 4-23 所示。
图 4-23。
3D effect with brightness as depth
普通像素映射
除了 Processing 和 OpenCV 中用于图像转换的内置函数之外,您还可以通过从源图像到目标图像的逐像素映射来编写通用图像转换算法。在本章的最后一个练习Chapter04_15中,您将尝试将第一幅图像img1中的单个像素复制到第二幅图像img2中。该变换将基于正弦和余弦函数产生的谐波运动。这里使用的图像大小为 600×600 像素。
PImage img1, img2;
float angle;
void setup() {
size(1200, 600);
img1 = loadImage("hongkong.png");
img2 = createImage(img1.width, img1.height, ARGB);
angle = 0;
}
void draw() {
// Variables rx, ry are for the radii of the sine/cosine functions
// Variables ax, ay are for the angles of the sine/cosine functions
background(0);
for (int y=0; y<img2.height; y++) {
float ay = y*angle/img2.height;
float ry = y*angle/360.0;
for (int x=0; x<img2.width; x++) {
float ax = x*angle/img2.width;
float rx = x*angle/360.0;
int x1 = x + (int)(rx*cos(radians(ay)));
int y1 = y + (int)(ry*sin(radians(ax)));
x1 = constrain(x1, 0, img1.width-1);
y1 = constrain(y1, 0, img1.height-1);
img2.pixels[y*img2.width+x] = img1.pixels[y1*img1.width+x1];
}
}
angle += 1;
angle %= 360;
img2.updatePixels();
image(img1, 0, 0);
image(img2, img1.width, 0);
}
第一个图像img1是源图像。第二个图像img2,与img1大小相同。在draw()函数的嵌套for循环中,你从相反的方向穿过目标图像中的每个像素img2。对于每个像素,您可以从源图像中找到应该将哪个像素复制到目标图像中。对于源像素,采用正弦和余弦函数,变量影响半径和角度。总体结果是一个扭曲效果作用于源图像的动画,如图 4-24 所示。图 4-24 和图 4-25 显示了不同时间点的两个样本显示。
图 4-25。
General mapping of pixels sample 2
图 4-24。
General mapping of pixels sample 1
这是你从动画中捕捉到的第二个瞬间(图 4-25 )。
从目标图像返回到源图像的原因是不要让任何目标像素为空。这是开发像素映射转换时的常见做法。
结论
本章描述了通过改变像素位置从而改变几何图形来修改图像的步骤。Processing 和 OpenCV 都具有几何变换功能。为了简化编码任务,您可以根据应用需求选择使用哪一个。或者,您可以通过指定目标图像中的所有像素以及它们在源图像中的来源来编写自己的图像转换函数。到目前为止,您只为创造性结果修改了图像。你还没有尝试去理解这些图像。在下一章,你将开始理解图像中的内容。
五、结构的识别
在前两章中学习了图像处理之后,您将开始使用处理和 OpenCV 探索计算机视觉。在前几章中,网络摄像头图像是创意输出的来源材料。你没有尝试去理解图像的内容。在本章中,您可以使用计算机视觉的概念来识别图像中的结构。通过这些结构,你会对图像的内容有更多的理解。本章将涉及的主题如下:
- 图像准备
- 边缘检测
- 车道检测
- 圆形检测
- 轮廓处理
- 形状检测
图像准备
在发送源图像进行检测之前,通常需要对图像进行优化。所谓优化,我指的是减少原始图像中不必要信息的过程。例如,当您想要识别图像中的直线时,通常不需要彩色图像。灰度的就可以了。有时,黑白图像可能足以满足形状检测的目的。以下是准备图像进行检测时要遵循的步骤:
- 转换为灰度
- 转换成黑白图像
- 形态学操作(侵蚀、扩张)
- 模糊操作(平滑)
转换为灰度
在第二章中,你学习了如何通过改变每个像素将彩色 RGB 图像转换成灰度图像。在下面的练习中,您将探索在处理和 OpenCV 中的不同方法来达到相同的效果。第一个练习Chapter05_01,将在处理中使用filter()函数。本练习中使用的示例图像的大小为 600×600 像素。
PImage source, grey;
void setup() {
size(1200, 600);
source = loadImage("sample04.jpg");
grey = createImage(source.width, source.height, ARGB);
noLoop();
}
void draw() {
background(0);
arrayCopy(source.pixels, grey.pixels);
grey.updatePixels();
grey.filter(GRAY);
image(source, 0, 0);
image(grey, source.width, 0);
}
该程序还演示了如何使用arrayCopy()函数有效地从一个数组复制到另一个相同大小的数组。转换图像的实际函数是grey.filter(GRAY)。程序会将原始图像和灰度并排显示进行对比,如图 5-1 所示。
图 5-1。
Grayscale conversion in Processing
下一个版本Chapter05_02将使用 OpenCV 函数来执行灰度转换。请注意,在章节 2 示例Chapter02_21中定义的CVImage类中,您已经编写了getGrey()方法来返回灰度图像矩阵。在使用 OpenCV 进行加工之前,请记住将code文件夹和CVImage定义复制到草图文件夹。样本图像的大小为 600×600 像素。
PImage source;
CVImage srccv, greycv;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
source = loadImage("sample04.jpg");
srccv = new CVImage(source.width, source.height);
srccv.copyTo(source);
greycv = new CVImage(source.width, source.height);
noLoop();
}
void draw() {
background(0);
Mat mat = srccv.getGrey();
greycv.copyTo(mat);
image(source, 0, 0);
image(greycv, source.width, 0);
mat.release();
}
在程序中,您使用CVImage实例greycv来保存通过getGrey()方法转换后的灰度图像。
转换成黑白图像
您在上一节中获得的灰度图像通常包含 256 级灰色调。在某些应用中,您可能希望只有两个级别,简单的黑色和白色。在这种情况下,您可以使用以下方法将灰度图像进一步转换为黑白图像。练习Chapter05_03将向您展示如何使用处理filter()函数来实现这一点。本练习中示例图像的大小为 600×600 像素。
PImage source, grey, bw;
void setup() {
size(1800, 600);
source = loadImage("sample01.jpg");
grey = createImage(source.width, source.height, ARGB);
bw = createImage(source.width, source.height, ARGB);
noLoop();
}
void draw() {
background(0);
arrayCopy(source.pixels, grey.pixels);
grey.updatePixels();
grey.filter(GRAY);
arrayCopy(grey.pixels, bw.pixels);
bw.updatePixels();
bw.filter(THRESHOLD, 0.5);
image(source, 0, 0);
image(grey, source.width, 0);
image(bw, source.width+grey.width, 0);
}
我经常把黑白转换称为阈值转换。当灰度值低于阈值时,灰度值高于阈值的像素将被认为是黑白的。这里用的函数是bw.filter(THRESHOLD, 0.5),其中数字 0.5 是阈值。图 5-2 显示窗口。
图 5-2。
Black-and-white image conversion with thresholding
左边的图像是原始照片。中间的是第一个filter()函数后的灰度版。右边的是第二个filter()功能后的黑白图像,这次带有选项THRESHOLD。下一个练习Chapter05_04将展示一个在 OpenCV 中完成的版本:
PImage source;
CVImage srccv, bwcv;
void setup() {
size(1800, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
source = loadImage("sample04.jpg");
srccv = new CVImage(source.width, source.height);
bwcv = new CVImage(source.width, source.height);
srccv.copyTo(source);
noLoop();
}
void draw() {
background(0);
Mat grey = srccv.getGrey();
Mat bw = new Mat();
Imgproc.threshold(grey, bw, 127, 255, Imgproc.THRESH_BINARY);
bwcv.copyTo(bw);
srccv.copyTo(grey);
image(source, 0, 0);
image(srccv, source.width, 0);
image(bwcv, source.width+srccv.width, 0);
grey.release();
bw.release();
}
以下是执行阈值操作的 OpenCV 函数:
Imgproc.threshold(grey, bw, 127, 255, Imgproc.THRESH_BINARY);
在函数中,第一个数字 127 是 0 到 255 范围内的中点。这是阈值。第二个数字 255 是灰度级别的最大数字。
形态学运算
图像处理中的形态学操作是修改图像中图案形状的变换。在这一节中,我只介绍侵蚀和扩张操作。下面的练习Chapter05_05展示了如何在处理过程中做到这一点:
PImage source, grey, bw, dilate, erode;
void setup() {
size(1800, 600);
source = loadImage("sample02.jpg");
grey = createImage(source.width, source.height, ARGB);
bw = createImage(source.width, source.height, ARGB);
dilate = createImage(source.width, source.height, ARGB);
erode = createImage(source.width, source.height, ARGB);
noLoop();
}
void draw() {
background(0);
arrayCopy(source.pixels, grey.pixels);
grey.updatePixels();
grey.filter(GRAY);
arrayCopy(grey.pixels, bw.pixels);
bw.updatePixels();
bw.filter(THRESHOLD, 0.5);
arrayCopy(bw.pixels, erode.pixels);
arrayCopy(bw.pixels, dilate.pixels);
erode.updatePixels();
dilate.updatePixels();
dilate.filter(DILATE);
erode.filter(ERODE);
image(bw, 0, 0);
image(erode, bw.width, 0);
image(dilate, bw.width+erode.width, 0);
}
结果显示包含三幅图像,如图 5-3 所示。左边的是来自THRESHOLD滤镜的黑白图像。中间的是ERODE版本。右边的是DILATE版本。
图 5-3。
Erode and dilate filters in Processing
ERODE滤镜减少白色区域的数量,而DILATE滤镜增加白色区域的数量。对于想要消除黑暗、微小噪声模式的应用来说,DILATE滤镜将是一个不错的选择。对于 OpenCV 版本,请参考以下练习,Chapter05_06:
PImage source;
CVImage srccv, bwcv, erodecv, dilatecv;
void setup() {
size(1800, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
source = loadImage("sample02.jpg");
srccv = new CVImage(source.width, source.height);
bwcv = new CVImage(source.width, source.height);
erodecv = new CVImage(source.width, source.height);
dilatecv = new CVImage(source.width, source.height);
srccv.copyTo(source);
noLoop();
}
void draw() {
background(0);
Mat grey = srccv.getGrey();
Mat bw = new Mat();
Imgproc.threshold(grey, bw, 127, 255, Imgproc.THRESH_BINARY);
Mat erode = new Mat();
Mat dilate = new Mat();
Mat elem = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
Imgproc.erode(bw, erode, elem);
Imgproc.dilate(bw, dilate, elem);
bwcv.copyTo(bw);
erodecv.copyTo(erode);
dilatecv.copyTo(dilate);
image(bwcv, 0, 0);
image(erodecv, bwcv.width, 0);
image(dilatecv, bwcv.width+erodecv.width, 0);
grey.release();
bw.release();
erode.release();
dilate.release();
}
程序使用前一个Imgproc.threshold()函数先将灰度图像转换成黑白图像。随后的Imgproc.erode()和Imgproc.dilate()功能将分别执行侵蚀和扩张形态操作。在进行侵蚀和扩张操作之前,您需要另一个矩阵,称为elem,它是描述形态学操作的结构化元素或内核。它通常有三种形状。
Imgproc.MORPH_RECTImgproc.MORPH_CROSSImgproc.MORPH_ELLIPSE
不同形状参数的elem的内容如下所示:
你会发现,尺寸为 3×3 时,MORPH_CROSS和MORPH_ELLIPSE效果是一样的。对于更大的尺寸,它们会有所不同。MORPH_CROSS只会在中间的行和列中有一个 1,而MORPH_ELLIPSE会有一个近似圆形的 1。过滤操作将使用矩阵elem扫描源图像。只有那些在elem中具有值 1 的像素将被收集用于计算。DILATE过滤器将用elem中定义的邻域像素中的最大值替换原始图像像素。ERODE滤镜将用邻域中的最小值替换原始图像像素。你可以在 OpenCV 文档中的 http://docs.opencv.org/3.1.0/d4/d86/group__imgproc__filter.html#gac2db39b56866583a95a5680313c314ad 找到这三个图形的细节。对于Size()参数,尺寸越大,变换效果越明显。一般来说,它是一个有一对奇数的正方形。
模糊操作
要进一步减少图像中的噪点或不必要的细节,您可以考虑使用模糊效果。Processing 和 OpenCV 都有一个模糊过滤器或函数。下一个练习Chapter05_07,在处理中使用模糊滤镜来执行操作:
PImage source, blur;
void setup() {
size(1200, 600);
source = loadImage("sample03.jpg");
blur = createImage(source.width, source.height, ARGB);
noLoop();
}
void draw() {
background(0);
arrayCopy(source.pixels, blur.pixels);
blur.updatePixels();
blur.filter(BLUR, 3);
image(source, 0, 0);
image(blur, source.width, 0);
}
这个程序很简单。它使用带有BLUR选项的filter()函数。选项后的数字是模糊量。数字越大,图像越模糊。图 5-4 显示了程序产生的显示窗口。
图 5-4。
Blur filter in Processing
对于 OpenCV,有几个模糊函数。在下一个练习Chapter05_08中,您将探究其中的一些并比较结果。它使用 OpenCV 的imgproc模块中的blur()、medianBlur()和GaussianBlur()函数。第一个blur()功能是局部平均操作,其中新图像像素是其邻域像素的平均值。在计算平均值时,GaussianBlur()函数对较近的像素赋予较高的权重,这对于去除可见噪声更有效。medianBlur()函数采用中值而不是平均值来计算新的像素值,这在去除噪声的同时更有效地保留了边缘/边界。
PImage source;
CVImage srccv, blurcv, mediancv, gaussiancv;
void setup() {
size(1800, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
source = loadImage("sample03.jpg");
srccv = new CVImage(source.width, source.height);
blurcv = new CVImage(source.width, source.height);
mediancv = new CVImage(source.width, source.height);
gaussiancv = new CVImage(source.width, source.height);
srccv.copyTo(source);
noLoop();
}
void draw() {
background(0);
Mat mat = srccv.getBGR();
Mat blur = new Mat();
Mat median = new Mat();
Mat gaussian = new Mat();
Imgproc.medianBlur(mat, median, 9);
Imgproc.blur(mat, blur, new Size(9, 9));
Imgproc.GaussianBlur(mat, gaussian, new Size(9, 9), 0);
blurcv.copyTo(blur);
mediancv.copyTo(median);
gaussiancv.copyTo(gaussian);
image(blurcv, 0, 0);
image(mediancv, blurcv.width, 0);
image(gaussiancv, blurcv.width+mediancv.width, 0);
mat.release();
blur.release();
median.release();
gaussian.release();
}
三个功能的模糊图像并排显示,如图 5-5 所示。
图 5-5。
Three blurring functions in OpenCV
作为本节的总结,您将结合这些操作来构建一个实际的应用,将实时网络摄像头图像转换为二进制黑白图像,以供以后处理。第一个版本是纯处理为练习而写的,Chapter05_09,如下所示:
import processing.video.*;
Capture cap;
void setup() {
size(1280, 480);
cap = new Capture(this, width/2, height);
cap.start();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
PImage tmp = createImage(cap.width, cap.height, ARGB);
arrayCopy(cap.pixels, tmp.pixels);
tmp.filter(GRAY);
tmp.filter(BLUR, 2);
tmp.filter(THRESHOLD, 0.25);
tmp.filter(DILATE);
image(cap, 0, 0);
image(tmp, cap.width, 0);
text(nf(round(frameRate), 2), 10, 20);
}
在程序中,你结合了模糊,灰度,阈值和腐蚀操作。对于纯处理实现,性能并不好。您添加text()功能,在屏幕上显示当前的帧速率,以便进行比较。图 5-6 显示加工显示窗口。
图 5-6。
Image preparation in Processing
对于 OpenCV 实现,在练习Chapter05_10中,您还可以将图像操作合并到一个单独的程序中,并将实时网络摄像头图像作为输入。性能比纯处理版好很多。
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.GaussianBlur(tmp1, tmp2, new Size(5, 5), 0);
Imgproc.threshold(tmp2, tmp1, 80, 255, Imgproc.THRESH_BINARY);
Mat elem = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
Imgproc.dilate(tmp1, tmp2, elem);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp2);
image(cap, 0, 0);
image(out, cap.width, 0);
tmp1.release();
tmp2.release();
elem.release();
text(nf(round(frameRate), 2), 10, 20);
}
图 5-7 显示加工显示窗口图像。帧速率明显高于处理版本。
图 5-7。
Image preparation in Processing with OpenCV
边缘检测
了解了准备图像的步骤后,您将发现的第一个结构是图像中任何对象的边缘或轮廓。计算机实际上不理解任何图像内容。它只能系统地扫描每个像素及其邻居。对于那些与相邻像素有明显色差的像素,您可以断定这些像素属于可能将两个对象或一个对象与其背景分开的轮廓。
处理没有边缘检测滤波器,尽管实现起来并不困难。对于 OpenCV,可以使用约翰·f·坎尼在 1986 年开发的著名的坎尼边缘检测器。为了运行边缘检测,执行模糊操作以去除噪声并将彩色图像转换为灰度通常是有益的。下一个练习Chapter05_11将说明这些步骤:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.GaussianBlur(tmp1, tmp2, new Size(7, 7), 1.5, 1.5);
Imgproc.Canny(tmp2, tmp1, 10, 30);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp1);
image(cap, 0, 0);
image(out, cap.width, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
以下是边缘检测的主要功能:
Imgproc.Canny(tmp2, tmp1, 10, 30);
图像tmp2是模糊的灰度图像。图像tmp1是包含边缘图像的图像。该函数有两个阈值。第一个数字是下限。如果一个像素的梯度值低于较低的阈值,它将被拒绝。第二个数字是上限。如果像素的梯度值大于上限阈值,它将被接受为边缘像素。如果像素的梯度值介于两个阈值之间,则只有当它连接到高于上限阈值的另一个像素时,它才会被接受为边缘。Canny 也建议第二个是第一个的 2 到 3 倍之间的值。值越大,图像中检测到的边缘越少。图 5-8 显示了检测结果。
图 5-8。
Canny edge detection
作为比较,您也可以使用threshold()功能将灰度图像转换为黑白图像。之后,您可以使用黑白图像执行边缘检测。下一个练习Chapter05_12演示了这种方法:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.GaussianBlur(tmp1, tmp2, new Size(7, 7), 1.5, 1.5);
Imgproc.threshold(tmp2, tmp1, 110, 255, Imgproc.THRESH_BINARY);
Imgproc.Canny(tmp1, tmp2, 10, 30);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp2);
img.copyTo(tmp1);
image(img, 0, 0);
image(out, img.width, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
生成的图像更加抽象,如图 5-9 所示。最终图像中的细节和噪点会更少。
图 5-9。
Canny edge detection with black-and-white image
车道检测
除了检测图像中形状的边缘或边界,您还可以使用 OpenCV 中的 Hough 直线变换来检测直线段。官方 OpenCV 文档中有霍夫线变换背后的数学细节;你可以在 http://docs.opencv.org/3.1.0/d9/db0/tutorial_hough_lines.html 找到文档。下面的练习Chapter05_13是处理中的一个简单实现:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
noStroke();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.Canny(tmp1, tmp2, 50, 150);
MatOfPoint2f lines = new MatOfPoint2f();
Imgproc.HoughLines(tmp2, lines, 1, PI/180, 100);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp2);
image(cap, 0, 0);
image(out, cap.width, 0);
Point [] points = lines.toArray();
pushStyle();
noFill();
stroke(255);
for (Point p : points) {
double rho = p.x;
double theta = p.y;
double a = cos((float)theta);
double b = sin((float)theta);
PVector pt1, pt2;
double x0 = rho*a;
double y0 = rho*b;
pt1 = new PVector((float)(x0 + cap.width*(-b)), (float)(y0 + cap.width*(a)));
pt2 = new PVector((float)(x0 - cap.width*(-b)), (float)(y0 - cap.width*(a)));
line(pt1.x, pt1.y, pt2.x, pt2.y);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
lines.release();
}
线检测的主要命令是Imgproc.HoughLines()功能。第一个参数是 Canny 边缘检测后的黑白图像。第二个参数是存储所有检测到的行信息的输出矩阵。由于它是一个 1×N 双通道矩阵,为了方便起见,您使用了子类MatOfPoint2f。其余的参数将决定检测的准确性。从高中代数中,你大概明白一条线可以用下面的式子来表示:
y = m * x + c
在HoughLines()函数中,同一行由另一个公式表示。
rho = x * cos (theta) + y * sin(theta)
这里,rho是图像原点到直线的垂直距离,theta是垂直线与水平 x 轴所成的角度。HoughLines()函数保存一个 2D 数组;第一维是rho的值,以像素为单位,第二维是theta的值,以度为单位。
第三个参数是测量rho的像素分辨率。本例中的值 1 表示rho的分辨率为 1 个像素。较大的值通常会生成更多精度较低的线。第四个参数是测量theta的角度分辨率。本例中的值PI/180表示theta的分辨率为 1 度。第五个参数决定了线条的检测效果。在本例中,将只报告那些通过的点超过 100 的线。在线检测之后,您将lines矩阵转换为Point的数组。数组中的每个成员都是一行。您使用for循环中的计算来计算每条线的两个端点,最后line()函数用白色绘制这条线。
图 5-10 显示了加工窗口。检测到的线条绘制在实时网络摄像头图像上。
图 5-10。
Hough line transform detection
OpenCV 还有另一个叫做HoughLinesP()的直线检测函数,它更高效,使用起来也更友好。它将返回每条线段的两个端点。下面的练习Chapter05_14说明了该函数的用法:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.Canny(tmp1, tmp2, 50, 150);
Mat lines = new Mat();
Imgproc.HoughLinesP(tmp2, lines, 1, PI/180, 80, 30, 10);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp2);
image(out, cap.width, 0);
pushStyle();
fill(100);
rect(0, 0, cap.width, cap.height);
noFill();
stroke(0);
for (int i=0; i<lines.rows(); i++) {
double [] pts = lines.get(i, 0);
float x1 = (float)pts[0];
float y1 = (float)pts[1];
float x2 = (float)pts[2];
float y2 = (float)pts[3];
line(x1, y1, x2, y2);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
lines.release();
}
对于HoughLinesP()函数的参数,第一个是图像矩阵。第二个参数是存储所有线段信息的输出矩阵lines。第三个参数1是像素分辨率,而第四个参数PI/180是角度分辨率,单位为度。第五个参数80是阈值。第六个参数30是最小线路长度。第七个参数10是最大线间隙。输出lines是一个一维矩阵,只有一列多行。在draw()函数内的for循环中,您遍历来自lines的所有行。每个元素实际上是另一个大小为 4 的数组。前两个是第一个端点的x和y位置。数组的第三和第四个元素是第二个端点的 x 和 y 位置。对于这两个端点,使用line()功能在它们之间画一条直线。图 5-11 显示了结果图像。
图 5-11。
Hough line transform detection
在下一个练习Chapter05_15中,您将使用创意图像处理中常用的一种技术来修改之前的练习。对于每条线段,计算它的中点并对像素颜色信息进行采样。使用此颜色,您可以更改该线段的描边颜色。其结果将类似于绘画中的彩色素描技术。
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.Canny(tmp1, tmp2, 20, 60);
Mat lines = new Mat();
Imgproc.HoughLinesP(tmp2, lines, 1, PI/180, 70, 30, 10);
image(cap, 0, 0);
pushStyle();
noFill();
for (int i=0; i<lines.rows(); i++) {
double [] pts = lines.get(i, 0);
float x1 = (float)pts[0];
float y1 = (float)pts[1];
float x2 = (float)pts[2];
float y2 = (float)pts[3];
int mx = (int)constrain((x1+x2)/2, 0, cap.width-1);
int my = (int)constrain((y1+y2)/2, 0, cap.height-1);
color c = cap.pixels[my*cap.width+mx];
stroke(c);
strokeWeight(random(1, 5));
line(x1+cap.width, y1, x2+cap.width, y2);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
lines.release();
}
请注意,您还引入了一个strokeWeight(random(1, 5))命令来为线段使用不同的笔画粗细。图 5-12 显示输出显示。
图 5-12。
Line detection as drawing
OpenCV 有一个LineSegmentDetector类实现 Rafael Grompone von Gioi 的线段检测器。这种方法将首先在一个非常小的区域中检测图像梯度方向,例如 2×2 像素。相似的方向串接在一起,判断是否可以是线段。下一个练习Chapter05_16使用新方法重新创建上一个练习:
import processing.video.*;
Capture cap;
CVImage img;
LineSegmentDetector line;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
line = Imgproc.createLineSegmentDetector();
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat lines = new Mat();
line.detect(tmp1, lines);
pushStyle();
for (int i=0; i<lines.rows(); i++) {
double [] pts = lines.get(i, 0);
float x1 = (float)pts[0];
float y1 = (float)pts[1];
float x2 = (float)pts[2];
float y2 = (float)pts[3];
int mx = (int)constrain((x1+x2)/2, 0, cap.width-1);
int my = (int)constrain((y1+y2)/2, 0, cap.height-1);
color col = cap.pixels[my*cap.width+mx];
stroke(col);
strokeWeight(random(1, 3));
line(x1+cap.width, y1, x2+cap.width, y2);
}
popStyle();
image(cap, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
lines.release();
}
首先将全局变量line定义为LineSegmentDetector的一个实例。在setup()函数中,您使用带有默认设置的静态函数Imgproc.createLineSegmentDetector()初始化实例。在draw()功能中,检测很简单。使用line.detect()方法完成,输入矩阵tmp1和输出结果lines作为参数。lines矩阵的结构与之前的练习相似。每个条目包含两个端点的x和y位置。结果显示看起来与之前的练习不同,如图 5-13 所示。
图 5-13。
Line detection with the OpenCV LineSegmentDetector
圆形检测
与直线检测类似,OpenCV 图像处理模块imgproc也包括使用霍夫圆变换的圆检测方法HoughCircles()。在下一个练习Chapter05_17中,您将探索此功能,从实时网络摄像头拍摄的准备好的图像中检测圆形:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.GaussianBlur(tmp1, tmp2, new Size(9, 9), 1);
Imgproc.Canny(tmp2, tmp1, 100, 200);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp1);
MatOfPoint3f circles = new MatOfPoint3f();
Imgproc.HoughCircles(tmp1, circles, Imgproc.HOUGH_GRADIENT, 1, tmp1.rows()/8, 200, 45, 0, 0);
Point3 [] points = circles.toArray();
image(cap, 0, 0);
image(out, cap.width, 0);
pushStyle();
noStroke();
fill(0, 0, 255, 100);
for (Point3 p : points) {
ellipse((float)p.x, (float)p.y, (float)(p.z*2), (float)(p.z*2));
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
circles.release();
}
该程序首先将图像转换为灰度,然后应用高斯模糊滤镜,最后检测边缘。然后将 Canny 边缘图像发送到HoughCircles()功能进行圆检测。第一个参数tmp1是输入图像。第二个参数circles是输出结果。第三个参数Imgproc.HOUGH_GRADIENT,是圆检测的唯一选项。第四个参数是分辨率的反比。通常是 1。第五个参数tmp1.rows()/8,是被检测圆之间的最小距离。第六个参数200是内部 Canny 边缘检测器的阈值上限。第七个参数45,是中心检测的阈值。该值越小,它将检测到的圆越多。其余参数是半径的最小值和最大值。它们默认为 0。结果circles是一个一维矩阵。您使用一个MatOfPoint3f来存储它的值。每个条目将包含三个值的数组,对应于圆心(x、y位置)和半径。for循环遍历所有圆圈,并以半透明的蓝色显示。图 5-14 显示了结果图像。
图 5-14。
Hough circle transform for circle detection
您可以通过过度检测来玩圆形检测程序。在下面的练习Chapter05_18中,您特意在HoughCircles()函数的第七个参数中放了一个小值,这样会产生很多错误检测。下面是程序的源代码:
import processing.video.*;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
MatOfPoint3f circles = new MatOfPoint3f();
Imgproc.HoughCircles(img.getGrey(), circles, Imgproc.HOUGH_GRADIENT, 1, img.height/10, 200, 20, 0, 0);
Point3 [] points = circles.toArray();
pushStyle();
noStroke();
for (Point3 p : points) {
int x1 = constrain((int)p.x, 0, cap.width-1);
int y1 = constrain((int)p.y, 0, cap.height-1);
color col = cap.pixels[y1*cap.width+x1];
fill(color(red(col), green(col), blue(col), 160));
ellipse(x1+cap.width, y1, (float)(p.z*2), (float)(p.z*2));
}
popStyle();
image(cap, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
circles.release();
}
你也删除了准备步骤,希望产生更多的圆圈。在for循环中,你使用前一种方法给圆圈上色。在这个版本中,你也为每个圆圈使用半透明的颜色。图 5-15 显示了结果显示。
图 5-15。
Drawing with Hough circle transform
该图像是原始网络摄像头图像的抽象渲染。你可以从颜色的使用和圆圈的位置上看出相似之处。就形状而言,你很难把它们与原作联系起来。
轮廓处理
在前面的小节中,您使用了 OpenCV 图像处理模块imgproc,从数字图像中识别特定的形状。在轮廓处理中,您使用相同的模块来识别图形形状的更一般的轮廓。它包括寻找轮廓和解释轮廓信息的方法。因为这些函数只对二进制图像有效,所以您必须准备好图像,使它们只包含黑白信息。我将介绍轮廓处理的以下步骤:
- 寻找轮廓
- 包围盒
- 最小面积矩形
- 凸包
- 多边形近似
- 测试轮廓中的点
- 检查交叉路口
寻找轮廓
在下一个练习Chapter05_19中,程序首先模糊灰度图像,然后用Canny()函数提取边缘,然后发送给findContours()函数:
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 50, 100);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(cap, 0, 0);
pushStyle();
noFill();
stroke(255, 255, 0);
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
Point [] pts = it.next().toArray();
for (int i=0; i<pts.length-1; i++) {
Point p1 = pts[i];
Point p2 = pts[i+1];
line((float)p1.x+cap.width, (float)p1.y, (float)p2.x+cap.width, (float)p2.y);
}
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
在findContours()函数中,第一个参数是黑白图像。第二个参数是输出等值线数据结构。第三个参数是跟踪外部边缘和内部孔的关系的层级信息。第四个参数Imgproc.RETR_LIST检索轮廓信息,无需跟踪层次关系。第五个参数Imgproc.CHAIN_APPROX_SIMPLE,只将等高线线段压缩成两个端点。您将在后面的练习中使用其他选项。主要输出contours,是MatOfPoint的一个 Java ArrayList。每个MatOfPoint被转换成一个Point的数组。for循环从一个Point到下一个for绘制一条线段。图 5-16 显示了结果图像。
图 5-16。
Contours processing with black-and-white Canny image
下一个练习Chapter05_20没有使用 Canny 边缘检测图像,而是使用由threshold()函数准备的黑白图像:
import processing.video.*;
import java.util.ArrayList;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(5, 5));
Imgproc.threshold(tmp2, tmp1, 80, 255, Imgproc.THRESH_BINARY);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
tmp1 = tmp2.clone();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
CVImage out = new CVImage(cap.width, cap.height);
out.copyTo(tmp1);
image(out, 0, 0);
pushStyle();
noFill();
stroke(255, 255, 0);
for (MatOfPoint ps : contours) {
Point [] pts = ps.toArray();
for (int i=0; i<pts.length-1; i++) {
Point p1 = pts[i];
Point p2 = pts[i+1];
line((float)p1.x+cap.width, (float)p1.y, (float)p2.x+cap.width, (float)p2.y);
}
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
该练习使用了threshold()函数将灰色图像转换为纯黑白图像。findContours()功能可以立即在黑白图像上执行轮廓跟踪。在这两个练习中,我还演示了使用for循环和iterator遍历MatOfPoint的 Java List的不同方式。图 5-17 显示了结果图像。
图 5-17。
Contours processing with threshold image
在接下来的练习Chapter05_21中,您将使用findContours()函数中的另一个选项来仅检索外部轮廓,而不返回那些内部孔。你用Imgproc.RETR_EXTERNAL替换原来的选项Imgproc.RETR_LIST。其余保持不变。新声明如下:
Imgproc.findContours(tmp2, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
如图 5-18 所示,汉字的内轮廓在新选项下不可见。
图 5-18。
Contours processing with the RETR_EXTERNAL option
现在,您将进一步探索轮廓检索模式中的其他选项。下一个练习Chapter05_22,将使用一个更复杂的RETR_CCOMP。它将所有轮廓组织成两个层次。所有外部边界都将位于顶层。这些洞在第二层。对于洞内的任何轮廓也将在顶层。在练习中,您可以利用这些信息用两种不同的颜色填充外部轮廓和孔。程序中使用的源图像大小为 600×600 像素。
import java.util.ArrayList;
CVImage cvimg;
PImage img;
void setup() {
size(1200, 600);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("chinese.png");
cvimg = new CVImage(img.width, img.height);
noLoop();
}
void draw() {
background(0);
cvimg.copyTo(img);
Mat tmp1 = new Mat();
Imgproc.blur(cvimg.getGrey(), tmp1, new Size(3, 3));
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE);
image(img, 0, 0);
pushStyle();
stroke(255);
for (int i=0; i<contours.size(); i++) {
Point [] pts = contours.get(i).toArray();
int parent = (int)hierarchy.get(0, i)[3];
// parent -1 implies it is the outer contour.
if (parent == -1) {
fill(200);
} else {
fill(100);
}
beginShape();
for (Point p : pts) {
vertex((float)p.x+img.width, (float)p.y);
}
endShape(CLOSE);
}
popStyle();
tmp1.release();
hierarchy.release();
}
除了将检索模式更改为RETR_CCOMP,您还可以使用hierarchy矩阵。它是一个一维矩阵。每一列对应于contours矩阵中的一个条目,具有相同的索引排列。hierarchy中的每个条目都是一个有四个值的数组。每个值都是轮廓矩阵中条目的索引。索引的映射如下:
hierarchy.get(0, i)[0]:下一个兄弟轮廓hierarchy.get(0, i)[1]:上一个兄弟轮廓hierarchy.get(0, i)[2]:第一个子轮廓hierarchy.get(0, i)[3]:父轮廓
索引中的值-1 表示相应的条目不可用。如果您看一下draw()函数中的for循环,该语句检查当前轮廓在位置i的父索引。
int parent = (int)hierarchy.get(0, i)[3];
如果它没有任何父级(-1),就用浅灰色着色(如果有,就用深灰色着色)。图 5-19 显示了结果图像。左边的汉字来自原图。右边的图像是具有两种灰色调的轮廓的渲染。
图 5-19。
Contours processing with option RETR_CCOMP
还有另一个检索模式RETR_TREE,它将在层次矩阵中存储每个轮廓的完整父子树关系。由于它的复杂性,我不会在本书中涉及它。
在您检测到图形形状的轮廓后,绘制轮廓将不是您唯一关心的事情。您可能希望确定移动的图形形状之间的相互作用,或者检查重叠区域。在接下来的部分中,您将研究如何理解从图像中检测到的轮廓信息。
包围盒
您可以从轮廓信息中获得的第一个信息是它的边界框。您可以使用 OpenCV 图像处理模块中的boundingRect()函数。输入参数是一个轮廓,由一个MatOfPoint类实例维护。输出是 OpenCV 矩形类,Rect。
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 80, 160);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(cap, 0, 0);
pushStyle();
noStroke();
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
Rect r = Imgproc.boundingRect(it.next());
int cx = (int)(r.x + r.width/2);
int cy = (int)(r.y + r.height/2);
cx = constrain(cx, 0, cap.width-1);
cy = constrain(cy, 0, cap.height-1);
color col = cap.pixels[cy*cap.width+cx];
fill(color(red(col), green(col), blue(col), 200));
rect((float)r.x+cap.width, (float)r.y, (float)r.width, (float)r.height);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
在这个程序中,Chapter05_23,一旦你获得每个包围盒的数据作为一个Rect,你就使用处理函数rect()来绘制矩形。Rect类包含四个属性:x、y、width和height。您还可以从矩形的中心获得颜色信息,并使用它给矩形着色,使其具有透明度。结果是原始图像的抽象渲染,如图 5-20 所示。
图 5-20。
Bounding rectangle for contours
最小面积矩形
OpenCV 图像处理模块有另一个函数minAreaRect(),用于计算轮廓的最小面积边界矩形。在下一个练习Chapter05_24中,您将获得轮廓的最小面积旋转矩形。结果是一个旋转的矩形类RotatedRect。
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 100, 200);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(cap, 0, 0);
pushStyle();
rectMode(CENTER);
noFill();
strokeWeight(2);
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
RotatedRect r = Imgproc.minAreaRect(new MatOfPoint2f(it.next().toArray()));
int cx = constrain((int)r.center.x, 0, cap.width-1);
int cy = constrain((int)r.center.y, 0, cap.height-1);
color col = cap.pixels[cy*cap.width+cx];
stroke(col);
Point [] pts = new Point[4];
r.points(pts);
beginShape();
for (int i=0; i<pts.length; i++) {
vertex((float)pts[i].x+cap.width, (float)pts[i].y);
}
endShape(CLOSE);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
minAreaRect()函数接受一个MatOfPoint2f格式的参数。轮廓输出的每个成员都是MatOfPoint的一个实例。在这种情况下,您必须将其转换为适当的类MatOfPoint2f,然后才能在minAreaRect()函数中使用。以下语句可以执行转换:
new MatOfPoint2f(it.next().toArray())
RotatedRect实例r具有属性center,该属性保持旋转矩形的中心位置。使用中心点找出绘制矩形的颜色信息。要绘制矩形,使用points()方法计算旋转矩形的四个角点。结果是一个Point数组,pts。有了这四个角点,您可以使用beginShape()和endShape(CLOSE)方法,通过指定顶点来绘制矩形。图 5-21 显示了输出图像。
图 5-21。
Minimum-area rectangle of contour
凸包
除了包围盒,还可以使用 OpenCV 来寻找轮廓信息的凸包。您使用的功能是convexHull()。它获取MatOfPoint轮廓信息并输出一个MatOfInt矩阵hull。输出实际上是轮廓索引的一个Point数组。原则上,hull中的条目数小于Point数组pts,因为它只包含构成凸形的点。
import java.util.ArrayList;
import java.util.Iterator;
CVImage cv;
PImage img;
void setup() {
size(1200, 600);
background(50);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
img = loadImage("chinese.png");
cv = new CVImage(img.width, img.height);
noLoop();
}
void draw() {
cv.copyTo(img);
Mat tmp1 = cv.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 100, 200);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(img, 0, 0);
pushStyle();
noFill();
stroke(250);
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
MatOfInt hull = new MatOfInt();
MatOfPoint mPt = it.next();
Point [] pts = mPt.toArray();
Imgproc.convexHull(mPt, hull);
int [] indices = hull.toArray();
beginShape();
for (int i=0; i<indices.length; i++) {
vertex((float)pts[indices[i]].x+img.width, (float)pts[indices[i]].y);
}
endShape(CLOSE);
hull.release();
mPt.release();
}
popStyle();
tmp1.release();
tmp2.release();
}
在这个程序中,Chapter05_25,你使用汉字进行测试。结果会更明显。在while循环中,你遍历每个轮廓并使用hull数组中的顶点创建一个闭合的形状。图 5-22 显示了结果图像以供参考。左边的字符是原件,而右边的图形是从轮廓上看的凸包。
图 5-22。
Convex hull processing in OpenCV
多边形近似
除了使用凸包来简化轮廓,OpenCV 还提供了其他方法来简化轮廓。下一个练习Chapter05_26介绍了一种给定轮廓的多边形近似方法。功能是approxPolyDP()。
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 100, 200);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(cap, 0, 0);
pushStyle();
noFill();
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
strokeWeight(random(5));
stroke(255, random(160, 256));
MatOfPoint2f poly = new MatOfPoint2f();
Imgproc.approxPolyDP(new MatOfPoint2f(it.next().toArray()), poly, 3, true);
Point [] pts = poly.toArray();
beginShape();
for (int i=0; i<pts.length; i++) {
vertex((float)pts[i].x+cap.width, (float)pts[i].y);
}
endShape(CLOSE);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
}
在while循环中,将每个轮廓传递给approxPolyDP()函数。第一个参数是转换成MatOfPoint2f的轮廓信息。第二个参数poly是存储为另一个MatOfPoint2f的输出多边形信息。第三个参数是近似精度。较小的值将具有更接近的近似值。第四个参数中的true值表示近似曲线是闭合的。请注意,您还可以改变描边粗细和描边颜色来模拟手绘动画效果。图 5-23 显示了结果图像。
图 5-23。
Polygon approximation
测试轮廓中的点
下一个练习Chapter05_27,是一个交互式的练习,因为你可以用鼠标改变轮廓的fill()颜色。在draw()功能中,在绘制每个轮廓之前,使用功能pointPolygonTest()执行一个测试,查看当前鼠标位置mouseX和mouseY是否在其中。由于您使用的是窗口的右侧,您必须将mouseX值减去窗口大小的一半,即cap.width。要使用pointPolygonTest()功能,首先将当前轮廓信息mp从MatOfPoint转换为MatOfPoint2f,并将其作为第一个参数传递。第二个参数是存储在Point对象实例中的鼠标位置。第三个布尔参数指示是否要返回距离数据。在本练习中,您使用false返回一个指示器,显示该点是在轮廓内部还是外部。正值表示该点位于轮廓内部,负值表示该点位于轮廓外部,而零表示该点位于边缘上。
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
}
void
draw() {
if (!cap.available())
return;
background(250);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 80, 160);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
image(cap, 0, 0);
pushStyle();
stroke(50);
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
MatOfPoint mp = it.next();
Point [] pts = mp.toArray();
boolean inside = true;
if (mouseX < cap.width) {
noFill();
} else {
int mx = constrain(mouseX-cap.width, 0, cap.width-1);
int my = constrain(mouseY, 0, cap.height-1);
double result = Imgproc.pointPolygonTest(new MatOfPoint2f(pts),
new Point(mx, my), false);
if (result > 0) {
fill(255, 0, 0);
} else {
noFill();
}
}
beginShape();
for (int i=0; i<pts.length; i++) {
vertex((float)pts[i].x+cap.width, (float)pts[i].y);
}
endShape(CLOSE);
}
popStyle();
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
hierarchy.release();
}
在该程序中,当鼠标位置在轮廓内时,将fill()颜色设置为红色。否则就是noFill()。图 5-24 显示鼠标位置在手指形成的孔内的瞬间。
图 5-24。
Testing whether a point is inside a contour with pointPolygonTest
检查交叉路口
在进入一般的形状匹配部分之前,我将用另外一个练习Chapter05_28来总结轮廓处理的使用。在本练习中,您将参考上一个练习Chapter05_24中RotatedRect的使用,并在固定矩形区域和从屏幕上的实时网络摄像头图像生成的旋转矩形之间执行检测。
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
CVImage img;
float minArea, maxArea;
RotatedRect rRect;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = new CVImage(cap.width, cap.height);
minArea = 50;
maxArea = 6000;
// This is the fixed rectangular region of size 200x200.
rRect = new RotatedRect(new Point(cap.width/2, cap.height/2),
new Size(200, 200), 0);
rectMode(CENTER);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
img.copyTo(cap);
Mat tmp1 = img.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 100, 200);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
// Draw the fixed rectangular region.
pushStyle();
fill(255, 20);
stroke(0, 0, 255);
rect((float)rRect.center.x+cap.width,
(float)rRect.center.y, (float)rRect.size.width,
(float)rRect.size.height);
popStyle();
pushStyle();
Iterator<MatOfPoint> it = contours.iterator();
while (it.hasNext()) {
MatOfPoint ctr = it.next();
float area = (float)Imgproc.contourArea(ctr);
// Exclude the large and small rectangles
if (area < minArea || area > maxArea)
continue
;
// Obtain the rotated rectangles from each contour.
RotatedRect r = Imgproc.minAreaRect(new MatOfPoint2f(ctr.toArray()));
Point [] pts = new Point[4];
r.points(pts);
stroke(255, 255, 0);
noFill();
// Draw the rotated rectangles.
beginShape();
for (int i=0; i<pts.length; i++) {
vertex((float)pts[i].x+cap.width, (float)pts[i].y);
}
endShape(CLOSE);
// Compute the intersection between the fixed region and
// each rotated rectangle.
MatOfPoint2f inter = new MatOfPoint2f();
int rc = Imgproc.rotatedRectangleIntersection(r, rRect, inter);
// Skip
the cases with no intersection.
if (rc == Imgproc.INTERSECT_NONE)
continue;
// Obtain the convex hull of the intersection polygon.
MatOfInt idx = new MatOfInt();
MatOfPoint mp = new MatOfPoint(inter.toArray());
Imgproc.convexHull(mp, idx);
int [] idArray = idx.toArray();
Point [] ptArray = mp.toArray();
// Fill the intersection area.
noStroke();
fill(255, 100);
beginShape();
for (int i=0; i<idArray.length; i++) {
Point p = ptArray[idArray[i]];
vertex((float)p.x+cap.width, (float)p.y);
}
endShape(CLOSE);
inter.release();
idx.release();
mp.release();
}
popStyle();
image(cap, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
hierarchy.release();
}
程序首先使用一个RotatedRect实例rRect定义一个固定区域。它的位置在视频采集屏幕的中心,尺寸为 200×200 像素。在draw()功能中,你首先从网络摄像头图像中检索所有轮廓。对于每个轮廓,你筛掉那些尺寸太小或太大的轮廓。对于其余的,您计算存储在变量r中的最小面积旋转矩形。对于每个旋转的矩形r,用下面的语句对照固定区域rRect进行检查:
int rc = Imgproc.rotatedRectangleIntersection(r, rRect, inter);
如果它们之间有交集,顶点信息将在MatOfPoint2f变量inter中。返回代码rc,实际上会告诉你发生的交互的类型。rc的可能值如下:
Imgproc.INTERSECT_NONE(无重叠区域)Imgproc.INTERSECT_PARTIAL(有重叠区域)Imgproc.INTERSECT_FULL(一个矩形在另一个内)
您可以在 http://docs.opencv.org/3.1.0/d3/dc0/group__imgproc__shape.html 找到检查的详细说明。对于有交集的情况,您可以尝试使用半透明填充颜色来绘制重叠区域。然而,您会发现从变量inter返回的顶点顺序并不能保证一个凸形。在程序中,在你把它们画在屏幕上之前,你添加几行来从inter中的顶点找到凸包。图 5-25 显示了程序的样本输出显示。
图 5-25。
Finding intersection between rotated rectangles
形状检测
在本章的最后一节,我将介绍 OpenCV 图像处理模块中的形状匹配函数matchShapes()。Chapter05_29练习的工作机制是构建一个形状模板,您希望将该模板与实时网络摄像头图像进行匹配。在这种情况下,您将使用如图 5-26 所示的汉字。您也可以创建自己的模式。黑色背景上的任何白色形状通常都很好。该图案图像的尺寸为 640×480 像素。
图 5-26。
Sample Chinese character to match with
该程序将从data文件夹中加载图像,并使用您在前面章节中了解到的findContours()函数构建轮廓。因为你事先知道这个字符只包含一个轮廓,你只需将第一个轮廓存储在一个MatOfPoint变量中。以下源代码中的prepareChar()函数执行此功能:
import processing.video.*;
import java.util.ArrayList;
import java.util.Iterator;
Capture cap;
PImage img;
CVImage cv;
MatOfPoint ch;
float maxVal;
void setup() {
size(1280, 480);
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
cap = new Capture(this, width/2, height);
cap.start();
img = loadImage("chinese.png");
ch = prepareChar(img);
cv = new CVImage(cap.width, cap.height);
maxVal = 5;
}
MatOfPoint
prepareChar(PImage i) {
CVImage chr = new CVImage(i.width, i.height);
chr.copyTo(i);
Mat tmp1 = chr.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.threshold(tmp2, tmp1, 127, 255, Imgproc.THRESH_BINARY);
Mat hierarchy = new Mat();
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
tmp1.release();
tmp2.release();
hierarchy.release();
return contours.get(0);
}
void
draw() {
if (!cap.available())
return;
background(0);
cap.read();
cv.copyTo(cap);
Mat tmp1 = cv.getGrey();
Mat tmp2 = new Mat();
Imgproc.blur(tmp1, tmp2, new Size(3, 3));
Imgproc.Canny(tmp2, tmp1, 100, 200);
ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
Mat hierarchy = new Mat();
Imgproc.findContours(tmp1, contours, hierarchy,
Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
Iterator<MatOfPoint> it = contours.iterator();
pushStyle();
while (it.hasNext()) {
MatOfPoint cont = it.next();
double val = Imgproc.matchShapes(ch, cont, Imgproc.CV_CONTOURS_MATCH_I1, 0);
if (val > maxVal)
continue;
RotatedRect r = Imgproc.minAreaRect(new MatOfPoint2f(cont.toArray()));
Point ctr = r.center
;
noStroke();
fill(255, 200, 0);
text((float)val, (float)ctr.x+cap.width, (float)ctr.y);
Point [] pts = cont.toArray();
noFill();
stroke(100);
beginShape();
for (int i=0; i<pts.length; i++) {
vertex((float)pts[i].x+cap.width, (float)pts[i].y);
}
endShape(CLOSE);
}
popStyle();
image(cap, 0, 0);
text(nf(round(frameRate), 2), 10, 20);
tmp1.release();
tmp2.release();
hierarchy.release();
}
在draw()功能中,您可以浏览来自实时网络摄像头图像的每个轮廓。您使用matchShapes()函数来执行匹配。前两个参数是汉字轮廓和每个实况网络摄像机图像轮廓。剩下的就是匹配方法和一个伪参数。返回值val,表示匹配有多接近;数值越小越好。您还可以排除那些返回值大于阈值maxVal的轮廓。使用minAreaRect()功能找出轮廓的中心,以便在屏幕上显示匹配值。程序的其余部分类似于前几节中绘制每个等高线的部分。
在图 5-27 所示的测试中,样本字符与存储的字符不相同。匹配值的范围从 1.5 到 3.5。
图 5-27。
Shape-matching test with other characters
在接下来的测试中,如图 5-28 所示,三个字符中有一个是正确的。正确字符的匹配值约为 0.6。
图 5-28。
Shape-matching test with one correct character
在下一个测试中,如图 5-29 所示,您使用相同的三个字符,但方向颠倒。正确字符的匹配值约为 0.4。
图 5-29。
Shape-matching test with upside-down characters
在下一个测试中,如图 5-30 所示,您使用一个手绘字符。样本字符的匹配值大约为 1.0。您可以在matchShapes()功能中探索匹配方法参数。不同的方法可能会产生不同范围的返回值。有必要进行测试和实验,以找到适合应用的方法。
图 5-30。
Shape-matching test with hand-drawn character
结论
在本章中,您开始了一些计算机视觉任务,以识别和分析数字图像中的结构元素。你从准备图像和提取边缘开始。从边缘信息中,您可以检测到直线和圆等几何元素。通过一般轮廓处理任务,您开发了一个简单的应用来检测实时网络摄像头视频流中更复杂的形状。在下一章,我将介绍从预先录制的或现场直播的视频中检测和分析运动的想法。