前言
在上一章我们介绍了《双目摄像头测量距离》,在这个基础上,我们来了解如何在Android上使用双目测距算法。通过本教程,你不仅掌握如何在Android中使用SBM等双目测距算法,顺便也了解到如何在Android Studio配置OpenCV,通过使用OpenCV可以在Android中实现很多图像处理的功能。
配置OpenCV
下载OpenCV的Android版本源码,官网下载地址:https://opencv.org/releases/,版本是3.4.1的。
1、创建一个Android项目,解压源码压缩包,在Android Studio中点击File
—>Import Model
,然后浏览解压后的sdk/java
添加,如下图所示,如何正常的话会显示OpenCV的版本。
2、复制OpenCV的动态库到app/libs
目录下。
3、修改OpenCVLibrary的build.gradle
的内容,这些内容全都都是app/build.gradle
的内容,主要把applicationId
去掉。
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 22
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
4、修改OpenCVLibrary的AndroidManifest.xml,内容大概如下,其中版本号对应自己导入的OpenCV的版本。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.opencv"
android:versionCode="3410"
android:versionName="3.4.1">
</manifest>
5、最后修改app/build.gradle
的内容。
// 在android下添加以下代码
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
// 在dependencies添加一下代码,根据情况修改版本号
implementation project(path: ':openCVLibrary341')
6、测试OpenCV,在应用中执行以下代码,如果初始化OpenCv成功,那配置OpenCV就已经成功了。
if (OpenCVLoader.initDebug()) {
Log.d(TAG, "OpenCVLoader初始化成功");
}
双目测距
创建一个StereoBMUtil.java
的java工具类,通过这类可以方便其他程序调用。在构造方法中配置StereoBM算法的一下参数,有些参数是相机标定的参数,具体用法参考《双目摄像头测量距离》这篇文章。 更加这篇教程,完成修改StereoBM算的相机标定的参数。
public StereoBMUtil() {
Mat cameraMatrixL = new Mat(3, 3, CvType.CV_64F);
Mat distCoeffL = new Mat(5, 1, CvType.CV_64F);
Mat cameraMatrixR = new Mat(3, 3, CvType.CV_64F);
Mat distCoeffR = new Mat(5, 1, CvType.CV_64F);
Mat T = new Mat(3, 1, CvType.CV_64F);
Mat rec = new Mat(3, 1, CvType.CV_64F);
// 【需要根据摄像头修改参数】左目相机标定参数 fc_left_x 0 cc_left_x 0 fc_left_y cc_left_y 0 0 1
cameraMatrixL.put(0, 0, 849.38718, 0, 720.28472, 0, 850.60613, 373.88887, 0, 0, 1);
//【需要根据摄像头修改参数】左目相机标定参数 kc_left_01, kc_left_02, kc_left_03, kc_left_04, kc_left_05
distCoeffL.put(0, 0, 0.01053, 0.02881, 0.00144, 0.00192, 0.00000);
//【需要根据摄像头修改参数】右目相机标定参数 fc_right_x 0 cc_right_x 0 fc_right_y cc_right_y 0 0 1
cameraMatrixR.put(0, 0, 847.54814, 0, 664.36648, 0, 847.75828, 368.46946, 0, 0, 1);
//【需要根据摄像头修改参数】右目相机标定参数 kc_right_01, kc_right_02, kc_right_03, kc_right_04, kc_right_05
distCoeffR.put(0, 0, 0.00905, 0.02094, 0.00082, 0.00183, 0.00000);
//【需要根据摄像头修改参数】T平移向量
T.put(0, 0, -59.32102, 0.27563, -0.79807);
// 【需要根据摄像头修改参数】rec旋转向量
rec.put(0, 0, -0.00927, -0.00228, -0.00070);
Size imageSize = new Size(imageWidth, imageHeight);
Mat R = new Mat();
Mat Rl = new Mat();
Mat Rr = new Mat();
Mat Pl = new Mat();
Mat Pr = new Mat();
Rect validROIL = new Rect();
Rect validROIR = new Rect();
Calib3d.Rodrigues(rec, R); //Rodrigues变换
//图像校正之后,会对图像进行裁剪,这里的validROI就是指裁剪之后的区域
Calib3d.stereoRectify(cameraMatrixL, distCoeffL, cameraMatrixR, distCoeffR, imageSize, R, T, Rl, Rr, Pl, Pr, Q, Calib3d.CALIB_ZERO_DISPARITY,
0, imageSize, validROIL, validROIR);
Imgproc.initUndistortRectifyMap(cameraMatrixL, distCoeffL, Rl, Pl, imageSize, CvType.CV_32FC1, mapLx, mapLy);
Imgproc.initUndistortRectifyMap(cameraMatrixR, distCoeffR, Rr, Pr, imageSize, CvType.CV_32FC1, mapRx, mapRy);
int blockSize = 18;
int numDisparities = 11;
int uniquenessRatio = 5;
bm.setBlockSize(2 * blockSize + 5); //SAD窗口大小
bm.setROI1(validROIL); //左右视图的有效像素区域
bm.setROI2(validROIR);
bm.setPreFilterCap(61); //预处理滤波器
bm.setMinDisparity(32); //最小视差,默认值为0, 可以是负值,int型
bm.setNumDisparities(numDisparities * 16); //视差窗口,即最大视差值与最小视差值之差,16的整数倍
bm.setTextureThreshold(10);
bm.setUniquenessRatio(uniquenessRatio); //视差唯一性百分比,uniquenessRatio主要可以防止误匹配
bm.setSpeckleWindowSize(100); //检查视差连通区域变化度的窗口大小
bm.setSpeckleRange(32); //32视差变化阈值,当窗口内视差变化大于阈值时,该窗口内的视差清零
bm.setDisp12MaxDiff(-1);
}
创建一个compute()
方法,该方法的参数是Bitmap类型的左右目摄像头的图像。compute()
方法的返回值是图像计算图像结果转换的图像,这给图像可以很直观显示图像的距离。计算结果都存放在xyz
矩阵中。
public Bitmap compute(Bitmap left, Bitmap right) {
Mat rgbImageL = new Mat();
Mat rgbImageR = new Mat();
Mat grayImageL = new Mat();
Mat rectifyImageL = new Mat();
Mat rectifyImageR = new Mat();
Mat grayImageR = new Mat();
//用于存放每个像素点距离相机镜头的三维坐标
xyz = new Mat();
Mat disp = new Mat();
bitmapToMat(left, rgbImageL);
bitmapToMat(right, rgbImageR);
Imgproc.cvtColor(rgbImageL, grayImageL, Imgproc.COLOR_BGR2GRAY);
Imgproc.cvtColor(rgbImageR, grayImageR, Imgproc.COLOR_BGR2GRAY);
Imgproc.remap(grayImageL, rectifyImageL, mapLx, mapLy, Imgproc.INTER_LINEAR);
Imgproc.remap(grayImageR, rectifyImageR, mapRx, mapRy, Imgproc.INTER_LINEAR);
bm.compute(rectifyImageL, rectifyImageR, disp); //输入图像必须为灰度图
Calib3d.reprojectImageTo3D(disp, xyz, Q, true); //在实际求距离时,ReprojectTo3D出来的X / W, Y / W, Z / W都要乘以16
Core.multiply(xyz, new Mat(xyz.size(), CvType.CV_32FC3, new Scalar(16, 16, 16)), xyz);
// 用于显示处理
Mat disp8U = new Mat(disp.rows(), disp.cols(), CvType.CV_8UC1);
disp.convertTo(disp, CvType.CV_32F, 1.0 / 16); //除以16得到真实视差值
Core.normalize(disp, disp8U, 0, 255, Core.NORM_MINMAX, CvType.CV_8U);
Imgproc.medianBlur(disp8U, disp8U, 9);
Bitmap resultBitmap = Bitmap.createBitmap(disp8U.cols(), disp8U.rows(), Bitmap.Config.ARGB_8888);
matToBitmap(disp8U, resultBitmap);
return resultBitmap;
}
执行上一步计算图像的距离之后,通过getCoordinate()
方法可以获取图像中实际的三维坐标,结构是x, y, z
。
public double[] getCoordinate(int dstX, int dstY) {
double x = xyz.get(dstY, dstX)[0];
double y = xyz.get(dstY, dstX)[1];
double z = xyz.get(dstY, dstX)[2];
return new double[]{x, y, z};
}
又是上面的双目测距工具类,接下来就可以很方便实现双目测距。在MainActivity.java
中,简单几步就完成了双目测距,在使用OpenCV之前一定要执行OpenCVLoader.initDebug()
,然后读取assets文件夹中的图像,分别是是左右目拍摄保存的图像,把他们转化成Bitmap用于下一步执行距离计算。
//初始化
if (OpenCVLoader.initDebug()) {
Log.d(TAG, "OpenCVLoader初始化成功");
}
// 加载图片
try {
leftBitmap = BitmapFactory.decodeStream(getAssets().open("Left3.bmp"));
rightBitmap = BitmapFactory.decodeStream(getAssets().open("Right3.bmp"));
imageViewLeft.setImageBitmap(leftBitmap);
imageViewRight.setImageBitmap(rightBitmap);
} catch (IOException e) {
e.printStackTrace();
}
因为我们已经编写了一个StereoBMUtil
工具类,在这里就可以直接计算这两张图像的物体距离了。计算完成之后,为了方便查看图像中的距离,把结果图在ImageView上显示,然后为ImageView添加点击获取坐标事件。用户在点击之后会获取到图像中的坐标,然后使用这个坐标从xyz
中获取拍摄物体的实际三维坐标。
// 执行StereoBM算法
button.setOnClickListener(v -> {
try {
Bitmap result = stereoBMUtil.compute(leftBitmap, rightBitmap);
imageViewResult.setImageBitmap(result);
} catch (Exception e) {
e.printStackTrace();
}
});
// 点击计算后的图片,获取三维坐标数据
imageViewResult.setOnTouchListener((v, event) -> {
// 获取触摸点的坐标 x, y
float x = event.getX();
float y = event.getY();
// 目标点的坐标
float[] dst = new float[2];
Matrix imageMatrix = imageViewResult.getImageMatrix();
Matrix inverseMatrix = new Matrix();
imageMatrix.invert(inverseMatrix);
inverseMatrix.mapPoints(dst, new float[]{x, y});
int dstX = (int) dst[0];
int dstY = (int) dst[1];
// 获取该点的三维坐标
double[] c = stereoBMUtil.getCoordinate(dstX, dstY);
String s = String.format("点(%d, %d) 三维坐标:[%.2f, %.2f, %.2f]", dstX, dstY, c[0], c[1], c[2]);
Log.d(TAG, s);
textView.setText(s);
return true;
});
效果图如下:
使用摄像头测距
上面的是实现读取两张计算物体距离,并没有使用摄像头拍摄,那么接下来我们就通过使用Android设备接的双目摄像头,实时拍摄图像计算物体距离。创建一个新的Activity,命名为CameraActivity
,按照通常的调用摄像头的方式,这样获取到的图像是左右目摄像头拍摄的图片拼接在一起的并且旋转的,我们需要的是把他们旋转回来并把他们裁剪分割,这样就可以获取到了两种分别是左右目摄像头拍摄的图像。
// 拍照获取左右摄像头的图像
button2.setOnClickListener(v -> {
bgView.setVisibility(View.VISIBLE);
ll.setVisibility(View.VISIBLE);
Bitmap imgBitmap = mTextureView.getBitmap();
Bitmap b = Utils.rotateBitmap(imgBitmap, 360 - sensorOrientation);
List<Bitmap> bitmapList = Utils.bisectionBitmap(b);
// 左右目摄像头的图像
leftBitmap = bitmapList.get(0);
rightBitmap = bitmapList.get(1);
imageViewLeft.setImageBitmap(leftBitmap);
imageViewRight.setImageBitmap(rightBitmap);
});
// 把图像翻转回来
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
// 裁剪分割左右目图像
public static List<Bitmap> bisectionBitmap(Bitmap bitmap) {
List<Bitmap> bitmapList = new ArrayList<>();
Bitmap left = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth() / 2, bitmap.getHeight(), null, true);
bitmapList.add(left);
Bitmap right = Bitmap.createBitmap(bitmap, bitmap.getWidth() / 2, 0, bitmap.getWidth() / 2, bitmap.getHeight(), null, true);
bitmapList.add(right);
return bitmapList;
}
接下来的处理方式就跟之前的一样了,使用StereoBMUtil
工具类读取分割后的左右目摄像头的图像执行计算,把结果图在ImageView上显示,然后为ImageView添加点击获取坐标事件。用户在点击之后会获取到图像中的坐标,然后使用这个坐标从xyz
中获取拍摄物体的实际三维坐标。
// 执行StereoBM算法
button4.setOnClickListener(v -> {
Bitmap result = stereoBMUtil.compute(leftBitmap, rightBitmap);
imageViewResult.setImageBitmap(result);
});
// 点击计算后的图片,获取三维坐标数据
imageViewResult.setOnTouchListener((v, event) -> {
// 获取触摸点的坐标 x, y
float x = event.getX();
float y = event.getY();
float[] dst = new float[2];
Matrix imageMatrix = imageViewResult.getImageMatrix();
Matrix inverseMatrix = new Matrix();
imageMatrix.invert(inverseMatrix);
inverseMatrix.mapPoints(dst, new float[]{x, y});
int dstX = (int) dst[0];
int dstY = (int) dst[1];
// 获取该点的三维坐标
double[] c = stereoBMUtil.getCoordinate(dstX, dstY);
String s = String.format("点(%d, %d) 三维坐标:[%.2f, %.2f, %.2f]", dstX, dstY, c[0], c[1], c[2]);
Log.d(TAG, s);
textView.setText(s);
return true;
});
效果图如下: