在Android实现双目测距

  |   4 评论   |   0 浏览   |   夜雨飘零

前言

在上一章我们介绍了《双目摄像头测量距离》,在这个基础上,我们来了解如何在 Android 上使用双目测距算法。通过本教程,你不仅掌握如何在 Android 中使用 SBM 等双目测距算法,顺便也了解到如何在 Android Studio 配置 OpenCV,通过使用 OpenCV 可以在 Android 中实现很多图像处理的功能。

配置 OpenCV

下载 OpenCV 的 Android 版本源码,官网下载地址:https://opencv.org/releases/,如果读者无法下载,笔者也提供的源码下载,版本是 3.4.1 的,下载地址:https://resource.doiduoyi.com/#736y3wk

1、创建一个 Android 项目,解压源码压缩包,在 Android Studio 中点击 File--->Import Model,然后浏览解压后的 sample/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;
});

效果图如下:

本项目源码: https://resource.doiduoyi.com/#cosaa9o


标题:在Android实现双目测距
作者:夜雨飘零
地址:https://blog.doiduoyi.com/articles/1589600553629.html

评论

  • 夜雨飘零 @wyl 回复»

    不清楚你说的哪个坐标下是什么意思,这个得到的是图片对应位置的三维坐标

  • 夜雨飘零 @wyl 回复»

    是的,单位厘米

  • wyl @wyl 回复»

    是相机距离物体的真实距离么

  • wyl 回复»

    您好,请问这里得到的三维坐标是在哪个坐标系下的?

发表评论