基于android平台的视觉检测智能相机的实现-opencv4android的应用

时间:2022-04-19 20:27:28

前段时间有一个关于光电图像处理的小竞赛,要求设计一款具备图像图形识别和测量的相机,将制定场景中指定形状目标识别并测量尺寸。
想到可能对大家会有帮助,对于在安卓平台上实现机器视觉算法还是个不错的例子,故将代码和方法放出来。本实例仅用了一点点安卓的知识和opencv4android的函数,供新手参考= =。(当然希望大神们的宝贵意见)

首先,关于平台的配置,大可参考另一篇文章点击这里
我使用的opencv版本是 opencv library-2.4.11
对于我们用到的java api ,可以在这里看官方文档。OpenCV4Android Reference官方文档
现在有关java api的书和资料很少,有本Joseph Howse 的 《Android Application Programming with OpenCV》 2013可以借鉴下。

好,现在开始言归正传。


识别和测量方法介绍

圆形识别方法
对于识别简单曲线,霍夫变换是一个著名通用又有效的方法。本系统就是采用了霍夫圆变换的算法进行圆形的识别。在opencv中提供的了相应的函数,我们可以适当调用其进行圆形的识别。

正方形识别方法
正方形的检测没有很著名和简单的算法,基于霍夫变换来检测方形复杂程度会增加很多而且确实有其他方法可以进行良好的代替。
我们可以先对图像进行预处理,之后找到其轮廓。然后将轮廓逼近成多边形。对于逼近成的多边形。我们可以进行遍历,依次检测,我们若限定多边形有4个顶点且两边夹角为90且两边相等,即可得到我们想要的正方形。

尺寸测量策略
本作品采用了参考物来标定比例尺的方法。首先,我们在被测平台上放一个已知尺寸的且可检测的几何图形,可以计算此时的已知实际长度和像素之比,即为每像素的实际长度。之后只要保持相机位置不动进行测量,即可得到测量的实际长度。
对于圆形直径的测量,在霍夫圆变换返回的曲线里,我们就可以得到圆形的半径。对于正方形的边长,我们采用先计算面积再开平方的策略,可以较准确的测得边长。
(这其实只是最最简陋的一种标定的方式,还可以采用棋盘格,考虑镜头畸变等,详细请自行查阅资料~)

界面设计
界面的设计要对应于产品的功能。
如下是在eclipse 中界面布局的实时预览截图。
基于android平台的视觉检测智能相机的实现-opencv4android的应用

坐下角为测量结果的显示,由于要实现实时的检测,这个结果每0.3秒更新一次。

右上角同样是显示的测量结果,但是这是取过平均值后的结果。在平时测量中,考虑到由于相机抖动导致测量值不稳定的现象,我们通过计算平均值得到一个确切的测量结果。
平均值计算方式为每3s计算一次平均值,即取左下角输出的实时结果进行计算。

右下角的按钮为获取比例尺的功能。只有通过此按钮成功获得了比例尺,之前的长度才会进行计算输出。在检测状态时按下此按钮,实现计算比例尺,且重复点击可更新。

点击右上角可以看见软件的主菜单。分别为:
开始检测。检测圆形。检测正方形。开启/关闭平均值计算功能

如下是进行检测和测量时,界面的室机运行截图:
基于android平台的视觉检测智能相机的实现-opencv4android的应用

如下是进行校准是,且展开菜单时的实机截图:
基于android平台的视觉检测智能相机的实现-opencv4android的应用


程序设计

设计平台与工具

本作品采用android 平台进行开发,故要使用Android开发的特定工具与插件。
我们选择了eclipse 配合 adt 插件的方式进行android 主程序的开发。
对于计算机视觉库,其本身在各个平台上都有相应的支持,故我们选择了OpenCV4android
配合opencv 的java 版本进行开发。

圆形识别类 DetectCircles

此工具类中封装了进行圆形检测的函数。将主要的两个函数列举出来。
findCircles 调用opencv 霍夫圆变换函数进行圆形检测,是圆形检测的关键。
drawCircles 调用circle 函数进行圆形绘制,标记出检测到的边缘。

public class DetectCircles {

public static double radMax=0;
public static double radMin=0;
public static double rad=0;
public static void findCircles(Mat dstImage,Mat circles){

Imgproc.HoughCircles(dstImage,circles,Imgproc.CV_HOUGH_GRADIENT,1.5,0);
}

public static void drawCircles(Mat image,Mat circles){
radMax = 0;
radMin = 0;
rad = 0;
for (int i = 0; i < circles.cols(); i++)
{
double vCircle[] = circles.get(0,i);
double x = vCircle[0];
double y = vCircle[1];
double radius = vCircle[2];
Core.circle(image, new Point(x,y), (int) radius, new Scalar(0,255,0),3);
radMax = (radius>radMax)? radius:radMax;
if (i == 0)
radMin = radius;
radMin = (radius < radMin) ? radius : radMin;
}
}

public static double getRad()
{
rad = 0.5 * (radMax + radMin);
return rad;
}
}

正方形识别类 DetectSquares
总体思路如下:
1.转化为二值图像后用findCountours 查找轮廓
2.对查找到的所有轮廓进行遍历
对于每一个轮廓:
1.approxPolyDP函数进行多边形逼近,方便之后从正方形几何特性判断
2.若有4 个顶点,且边长几乎相等,且夹角近乎90度,判定为正方形
下面将有详细注释的正方形检测类贴出。

public class DetectSquares {
//检测正方形 工具类

private static double maxArea = 0, area = 0;

public static double lengthMax = 0,line4;

//利用余弦定理 计算夹角余弦
public static double angle(Point pt1, Point pt2, Point pt0) {
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return (dx1 * dx2 + dy1 * dy2)
/ Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2)
+ 1e-10);
}
//计算两边长度之差
public static double isEqual(Point pt1, Point pt2, Point pt0) {
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return Math.sqrt((dx1 * dx1 + dy1 * dy1))
- Math.sqrt((dx2 * dx2 + dy2 * dy2));
}

public static double getLength() {
//由面积计算边长
lengthMax = Math.sqrt(maxArea);
return lengthMax;

}

public static void findSquares(Mat image, List<MatOfPoint> squares) {

maxArea = 0;

Mat gray = new Mat(image.size(), CvType.CV_8U);

List<MatOfPoint> contours = new ArrayList<MatOfPoint>();

// threshold(gray0, gray, (l+1)*255/N, 255, Imgproc.THRESH_BINARY);
// Imgproc.GaussianBlur(gray0, gray0, new org.opencv.core.Size(9,9),
// 2,2);
// Imgproc.medianBlur(gray0, gray0, 9);
// Imgproc.Canny(image, image, 0, 50);
//转化为二值图像
Imgproc.threshold(image, gray, 100, 255, Imgproc.THRESH_BINARY);
// Imgproc.adaptiveThreshold(image, gray, 255,
// Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 5, 1);
// return gray;
//寻找所有 轮廓
Imgproc.findContours(gray, contours, new Mat(), Imgproc.RETR_LIST,
Imgproc.CHAIN_APPROX_SIMPLE);


MatOfPoint approx;
MatOfPoint2f contouri;
MatOfPoint2f approx2f = new MatOfPoint2f();

//对寻找到的轮廓遍历
for (int i = 0; i < contours.size(); i++) {
//由于approxPolyDP参数类型的原因 对MatOfPoint 进行转化
contouri = new MatOfPoint2f(contours.get(i).toArray());
//从轮廓逼近多边形曲线
Imgproc.approxPolyDP(contouri, approx2f,
Imgproc.arcLength(contouri, true) * 0.02, true);
//将类型转换回来
approx = new MatOfPoint(approx2f.toArray());

//对于生成的逼近多边形 应该有四个顶点 对最小面积限制 防止检测无用四边形
if (approx.toList().size() == 4
& Math.abs(Imgproc.contourArea(approx)) > 30) {
double maxCosine = 0;
double maxGap = 0;

for (int j = 2; j < 5; j++) {
//角度计算
double cosine = Math.abs(angle(approx.toArray()[j % 4],
approx.toArray()[j - 2], approx.toArray()[j - 1]));
//边长差计算
double gap = Math.abs(isEqual(approx.toArray()[j % 4],
approx.toArray()[j - 2], approx.toArray()[j - 1]));
maxCosine = Math.max(maxCosine, cosine);
maxGap = Math.max(maxGap, gap);
}
//对最大的cos 和最大的边长差进行限制
if (maxCosine < 0.3 & maxGap < 20) {
area = Math.abs(Imgproc.contourArea(approx));
maxArea = Math.max(maxArea, area);
//将检测好的方形加入squares
squares.add(approx);
}
}
}
}

public static void drawSquares(Mat image, List<MatOfPoint> squares) {

for (int i = 0; i < squares.size(); i++) {
//对方形每条边分别画线
for (int j = 0; j < 4; j++) {
Point pt1 = squares.get(i).toArray()[j];
Point pt2 = squares.get(i).toArray()[(j + 1) % 4];
Core.line(image, pt1, pt2, new Scalar(255, 0, 0), 3);
// double dx = pt1.x - pt2.x;
// double dy = pt1.y - pt2.y;
//line4 += Math.sqrt((dx * dx + dy * dy));

}

// lengthMax = Math.max(lengthMax,line4/4);
// line4 =0;
}


}


}

平均值计算功能的开发
在实际测量的过程中,由于各种原因导致实时测量的结果会发生变化和抖动。故我们采取计算平均值的方法,得到一个稳定的数值,作为最后的测量结果。

final Handler myHandler = new Handler() {
@Override
public void handleMessage(Message msg) {

if (msg.what == 0x1234) {
d = DetectCircles.getRad() * 2;
l = DetectSquares.getLength();
if (isCalculate) {//如果开启平均值计算功能
calcuFang.setVisibility(View.VISIBLE);
calcuYuan.setVisibility(View.VISIBLE);
jieguo.setVisibility(View.VISIBLE);
if (l != 0 & d != 0) {//l d 为0时不计算平均值
if (counter % 11 == 0) {//第11 次 输出结果
calcuYuan.setText("直径平均值"
+ (double) Math.round(dSum / 10 * 100)
/ 100);
calcuFang.setText("边长平均值"
+ (double) Math.round(lSum / 10 * 100)
/ 100);
jieguo.setText("直径平均值"
+ (double) Math.round(dSum / 10 * 100)
/ 100 + "mm\n" + "边长平均值"
+ (double) Math.round(lSum / 10 * 100)
/ 100+"mm");
counter = 0;
dSum = 0;
lSum = 0;
} else {//显示正在加第几位数据
calcuYuan
.setText("计算直径平均值(" + (counter % 11) + ")");
calcuFang
.setText("计算边长平均值(" + (counter % 11) + ")");

dSum += d * scale;
lSum += l * scale;
}
counter++;
}
} else {//关闭计算平均值功能
calcuFang.setVisibility(View.INVISIBLE);
calcuYuan.setVisibility(View.INVISIBLE);
jieguo.setVisibility(View.INVISIBLE);
}

//实时显示测量结果
showMeasureC.setText("圆直径为:"
+ (double) Math.round(d * scale * 100) / 100+"mm");
//DetectCircles.changeRad2zero();
showMeasureS.setText("正方形边长为:"
+ (double) Math.round(l * scale * 100) / 100+"mm");
//DetectSquares.changeLength2zero();
}
}
};

遇到的问题及解决对策
1.检测的帧数较慢
在检测前的预处理进行优化的取舍

            Imgproc.cvtColor(image, dstImage, Imgproc.COLOR_BGR2GRAY);
// Imgproc.bilateralFilter(dstImage, dstImage, 25, 25*2, 25/2);
// Imgproc.medianBlur(dstImage, dstImage,5);
Imgproc.blur(dstImage, dstImage, new org.opencv.core.Size(5, 5));
// Imgproc.GaussianBlur(dstImage, dstImage, new
// org.opencv.core.Size(9, 9), 2, 2);
// Imgproc.Canny(dstImage, dstImage, 50, 200);

比如,选择均值滤波,可以提高效率。

其次,在检测中,可以不用对每个颜色通道进行处理,可以直接用threshold 函数进行二值化 等等可以对检测的效率进行优化,虽然对效果有了一定的取舍。

2方形检测不稳定,有时能成功绘制边缘,有时则不行
if (maxCosine < 0.2 & maxGap < 10) 通过对参数的适当设置,可以解决问题,之前参数过于苛刻,有些帧时导致识别正方形失败。

测试结果与误差分析
我们使用了两款手机进行了测试:
取d=34.5mm L=39.2mm 的圆形和正方形进行测试。
在荣耀3x上得到如下几组数据:
1. d=35.03mm 相对误差1.5% L=39.17mm 相对误差 0.08%
2. D=35.75mm 相对误差 3.6% L=39.87mm 相对误差 1.7%
3. D=35.07mm 相对误差 1.6% l=38.7mm 相对误差 1.3%
在荣耀6 上得到如下几组数据:
D=34.45mm 相对误差 0.14% L=39.05% 相对误差 0.38%

可以看出每次测量结果并不相同,有着一定的随机因素,包括我们测量时的抖动,高度的复原没有做精确等等。同时,因为荣耀6有着更高的分辨率,故测量结果有着更好的结果。


主要内容就是这些。我把程序的源代码也上传了上来,里面有详细的注释,希望能帮到大家源代码免积分下载