【问题标题】:How to Read DJI H264 FPV Feed as OpenCV Mat Object?如何将 DJI H264 FPV Feed 读取为 OpenCV Mat 对象?
【发布时间】:2019-10-10 14:00:21
【问题描述】:

TDLR:所有 DJI 开发人员都会受益于将原始 H264 视频流字节数组解码为与 OpenCV 兼容的格式。

我花了很多时间寻找将 DJI 的 FPV 提要读取为 OpenCV Mat 对象的解决方案。我可能忽略了一些基本的东西,因为我对图像编码/解码不太熟悉。

遇到它的未来开发人员可能会遇到一堆与我相同的问题。如果 DJI 开发者可以直接使用 opencv 而不需要第三方库,那就太好了。

如有必要,我愿意使用 ffmpeg 或 JavaCV,但这对于大多数 Android 开发人员来说是一个很大的障碍,因为我们将不得不使用 cpp、ndk、终端进行测试等。这似乎有点矫枉过正。这两种选择似乎都非常耗时。 This JavaCV H264 conversion 似乎不必要地复杂。我是从this relevant question 找到的。

我认为问题在于我们需要同时解码长度为 6 的字节数组(信息数组)和具有当前帧信息的字节数组。

基本上,DJI 的 FPV 提要有多种格式。

  1. VideoFeeder.VideoDataListener 中的原始 H264 (MPEG4)
    // The callback for receiving the raw H264 video data for camera live view
    mReceivedVideoDataListener = new VideoFeeder.VideoDataListener() {
        @Override
        public void onReceive(byte[] videoBuffer, int size) {
            //Log.d("BytesReceived", Integer.toString(videoStreamFrameNumber));
            if (videoStreamFrameNumber++%30 == 0){
                //convert video buffer to opencv array
                OpenCvAndModelAsync openCvAndModelAsync = new OpenCvAndModelAsync();
                openCvAndModelAsync.execute(videoBuffer);
            }
            if (mCodecManager != null) {
                mCodecManager.sendDataToDecoder(videoBuffer, size);
            }
        }
    };

  1. DJI 也有自己的 Android 解码器示例,带有 FFMPEG 可转换为 YUV 格式。
    @Override
    public void onYuvDataReceived(final ByteBuffer yuvFrame, int dataSize, final int width, final int height) {
        //In this demo, we test the YUV data by saving it into JPG files.
        //DJILog.d(TAG, "onYuvDataReceived " + dataSize);
        if (count++ % 30 == 0 && yuvFrame != null) {
            final byte[] bytes = new byte[dataSize];
            yuvFrame.get(bytes);
            AsyncTask.execute(new Runnable() {
                @Override
                public void run() {
                    if (bytes.length >= width * height) {
                        Log.d("MatWidth", "Made it");
                        YuvImage yuvImage = saveYuvDataToJPEG(bytes, width, height);
                        Bitmap rgbYuvConvert = convertYuvImageToRgb(yuvImage, width, height);

                        Mat yuvMat = new Mat(height, width, CvType.CV_8UC1);
                        yuvMat.put(0, 0, bytes);
                        //OpenCv Stuff
                    }
                }
            });
        }
    }

编辑:想看大疆的 YUV 转 JPEG 功能的朋友,这里是示例应用程序:

private YuvImage saveYuvDataToJPEG(byte[] yuvFrame, int width, int height){
        byte[] y = new byte[width * height];
        byte[] u = new byte[width * height / 4];
        byte[] v = new byte[width * height / 4];
        byte[] nu = new byte[width * height / 4]; //
        byte[] nv = new byte[width * height / 4];

        System.arraycopy(yuvFrame, 0, y, 0, y.length);
        Log.d("MatY", y.toString());
        for (int i = 0; i < u.length; i++) {
            v[i] = yuvFrame[y.length + 2 * i];
            u[i] = yuvFrame[y.length + 2 * i + 1];
        }
        int uvWidth = width / 2;
        int uvHeight = height / 2;
        for (int j = 0; j < uvWidth / 2; j++) {
            for (int i = 0; i < uvHeight / 2; i++) {
                byte uSample1 = u[i * uvWidth + j];
                byte uSample2 = u[i * uvWidth + j + uvWidth / 2];
                byte vSample1 = v[(i + uvHeight / 2) * uvWidth + j];
                byte vSample2 = v[(i + uvHeight / 2) * uvWidth + j + uvWidth / 2];
                nu[2 * (i * uvWidth + j)] = uSample1;
                nu[2 * (i * uvWidth + j) + 1] = uSample1;
                nu[2 * (i * uvWidth + j) + uvWidth] = uSample2;
                nu[2 * (i * uvWidth + j) + 1 + uvWidth] = uSample2;
                nv[2 * (i * uvWidth + j)] = vSample1;
                nv[2 * (i * uvWidth + j) + 1] = vSample1;
                nv[2 * (i * uvWidth + j) + uvWidth] = vSample2;
                nv[2 * (i * uvWidth + j) + 1 + uvWidth] = vSample2;
            }
        }
        //nv21test
        byte[] bytes = new byte[yuvFrame.length];
        System.arraycopy(y, 0, bytes, 0, y.length);
        for (int i = 0; i < u.length; i++) {
            bytes[y.length + (i * 2)] = nv[i];
            bytes[y.length + (i * 2) + 1] = nu[i];
        }
        Log.d(TAG,
              "onYuvDataReceived: frame index: "
                  + DJIVideoStreamDecoder.getInstance().frameIndex
                  + ",array length: "
                  + bytes.length);
        YuvImage yuver = screenShot(bytes,Environment.getExternalStorageDirectory() + "/DJI_ScreenShot", width, height);
        return yuver;
    }

    /**
     * Save the buffered data into a JPG image file
     */
    private YuvImage screenShot(byte[] buf, String shotDir, int width, int height) {
        File dir = new File(shotDir);
        if (!dir.exists() || !dir.isDirectory()) {
            dir.mkdirs();
        }
        YuvImage yuvImage = new YuvImage(buf,
                ImageFormat.NV21,
                width,
                height,
                null);

        OutputStream outputFile = null;

        final String path = dir + "/ScreenShot_" + System.currentTimeMillis() + ".jpg";

        try {
            outputFile = new FileOutputStream(new File(path));
        } catch (FileNotFoundException e) {
            Log.e(TAG, "test screenShot: new bitmap output file error: " + e);
            //return;
        }
        if (outputFile != null) {
            yuvImage.compressToJpeg(new Rect(0,
                    0,
                    width,
                    height), 100, outputFile);
        }
        try {
            outputFile.close();
        } catch (IOException e) {
            Log.e(TAG, "test screenShot: compress yuv image error: " + e);
            e.printStackTrace();
        }

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                displayPath(path);
            }
        });
        return yuvImage;
    }

  1. DJI 似乎也有一个“getRgbaData”功能,但实际上没有一个在线示例或 DJI 提供的示例。继续谷歌“DJI getRgbaData”......只有对解释自我解释参数和返回值的api文档的引用,但没有别的。我不知道在哪里调用它,并且似乎没有像 YUV 那样的回调函数。不能直接从 h264b 字节数组中调用,但或许可以从 yuv 数据中获取。

选项 1 比选项 2 更可取,因为 YUV 格式存在质量问题。选项 3 也可能涉及解码器。

这是 DJI 自己的 YUV 转换生成的屏幕截图。

我看过很多关于如何改进 YUV、去除绿色和黄色等等的东西,但是在这一点上,如果 DJI 做不好,我不想在那里投入资源。

关于选项 1,我知道如果我必须走视频解码路线,FFMPEG 和 JavaCV 似乎是不错的选择。

此外,据我了解,OpenCV 无法在没有 FFMPEG 的情况下读取和写入视频文件,但我不是在尝试读取视频文件,而是在尝试读取 H264/MPEG4 byte[] 数组。下面的代码似乎得到了积极的结果。

    /* Async OpenCV Code */
    private class OpenCvAndModelAsync extends AsyncTask<byte[], Void, double[]> {
        @Override
        protected double[] doInBackground(byte[]... params) {//Background Code Executing. Don't touch any UI components
            //get fpv feed and convert bytes to mat array
            Mat videoBufMat = new Mat(4, params[0].length, CvType.CV_8UC4);
            videoBufMat.put(0,0, params[0]);
            //if I add this in it says the bytes are empty.
            //Mat videoBufMat = Imgcodecs.imdecode(encodeVideoBuf, Imgcodecs.IMREAD_ANYCOLOR);
            //encodeVideoBuf.release();
            Log.d("MatRgba", videoBufMat.toString());
            for (int i = 0; i< videoBufMat.rows(); i++){
                for (int j=0; j< videoBufMat.cols(); j++){
                    double[] rgb = videoBufMat.get(i, j);
                    Log.i("Matrix", "red: "+rgb[0]+" green: "+rgb[1]+" blue: "+rgb[2]+" alpha: "
                            + rgb[3] + " Length: " + rgb.length + " Rows: "
                            + videoBufMat.rows() + " Columns: " + videoBufMat.cols());
                }
            }
            double[] center = openCVThingy(videoBufMat);
            return center;
        }
        protected void onPostExecute(double[] center) {
            //handle ui or another async task if necessary
        }
    }

行 = 4,列 > 30k。我得到了很多看起来有效的 RGB 值,例如 red = 113, green=75, blue=90, alpha=220 作为一个虚构的例子;但是,我得到了大量的 0,0,0,0 值。这应该没问题,因为黑色是 0,0,0(尽管我认为 alpha 会更高)并且我的图像中有一个黑色物体。我似乎也没有得到任何白色值 255、255、255,即使也有很多白色区域。我没有记录整个字节,所以它可能在那里,但我还没有看到它。

但是,当我尝试从该图像计算轮廓时,我几乎总是得到矩(中心 x,y)正好位于图像的中心。此错误与我的颜色过滤器或轮廓算法无关,因为我在 python 中编写了一个脚本并测试我通过读取静止图像并在两个 Python 中获得完全相同数量的轮廓、位置等在 Android 中正确实现了它和安卓。

我注意到它与 videoBuffer 字节大小有关(如果你能解释为什么每隔一个长度是 6 就可以加分)

2019-05-23 21:14:29.601 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2425
2019-05-23 21:14:29.802 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2659
2019-05-23 21:14:30.004 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:30.263 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6015
2019-05-23 21:14:30.507 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:30.766 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4682
2019-05-23 21:14:31.005 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:31.234 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2840
2019-05-23 21:14:31.433 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4482
2019-05-23 21:14:31.664 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:31.927 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4768
2019-05-23 21:14:32.174 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:32.433 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4700
2019-05-23 21:14:32.668 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:32.864 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4740
2019-05-23 21:14:33.102 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:33.365 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4640

我的问题:

我。这是将 h264 字节读取为 mat 的正确格式吗? 假设格式是 RGBA,这意味着 row = 4 和 columns = byte[].length 和 CvType.CV_8UC4。我的高度和宽度正确吗?有些东西告诉我 YUV 高度和宽度已关闭。我得到了一些有意义的结果,但轮廓正好在中心,就像 H264 一样。

二。 OpenCV 是否像这样在 android 中处理 MP4?如果没有,我们是否需要使用 FFMPEG 或 JavaCV?

三。 int大小是否与它有关?为什么 int 大小有时是 6,有时是 2400 到 6000?我听说过此帧信息和有关下一帧的信息之间的区别,但我根本没有足够的知识知道如何在这里应用它。

我开始认为这就是问题所在。由于我需要获取 6 字节数组以获取有关下一帧的信息,因此我的模 30 可能不正确。那么我应该将第 29 帧或第 31 帧作为每个帧的格式字节传递吗?这是如何在 opencv 中完成的,还是我们注定要使用复杂的 ffmpeg?我将如何加入相邻的帧/字节数组?

四。我可以使用 Imcodecs 解决这个问题吗?我希望opencv能够本机地处理一个帧是来自这个帧的颜色还是关于下一帧的信息。我添加了以下代码,但我得到一个空数组:

Mat videoBufMat = Imgcodecs.imdecode(new MatOfByte(params[0]), Imgcodecs.IMREAD_UNCHANGED);

这也是空的:

Mat encodeVideoBuf = new Mat(4, params[0].length, CvType.CV_8UC4);
encodeVideoBuf.put(0,0, params[0]);
Mat videoBufMat = Imgcodecs.imdecode(encodeVideoBuf, Imgcodecs.IMREAD_UNCHANGED);

V。我应该尝试将字节转换为 Android jpeg 然后导入吗?为什么 djis yuv 解码器看起来那么复杂?这让我不想尝试 ffmpeg 或 Javacv 并坚持使用 Android 解码器或 opencv 解码器。

六。我应该在什么阶段调整帧大小以加快计算速度?

编辑: DJI 支持人员回复了我,并确认他们没有任何样品可用于我所描述的操作。现在是我们社区向所有人提供此功能的时候了!

经过进一步研究,我认为 opencv 无法处理此问题,因为 opencv 的 android sdk 没有视频文件/url 的功能(除了自制的 MJPEG 编解码器)。

那么,Android 中有没有一种方法可以转换为 mjpeg 或类似格式以便阅读?在我的应用程序中,我每秒只需要 1 或 2 帧,所以也许我可以将图像保存为 jpeg。

但对于实时应用程序,我们可能需要编写自己的解码器。请提供帮助,以便我们可以将其提供给所有人!这个question 看起来很有希望:

【问题讨论】:

  • 您是否尝试使用 imshow 可视化 yuv 框架?在这种情况下,Y 通道将映射到蓝色,U 将映射到绿色,V 到红色通道以进行可视化。
  • 正如我所提到的,这是 dji 原生 yuv 到 jpeg 的转换。这就是保存到我的平板电脑上的 jpeg。我没有使用 imshow
  • 这绝对是一个很好的建议,说明它为什么不起作用。但我只是使用 djis 示例代码来产生这些结果
  • 我在 DJI 的示例代码中添加了 YUV 保存到 JPEG 功能,供有兴趣的朋友参考

标签: android opencv ffmpeg javacv dji-sdk


【解决方案1】:

首先H264和h264是不同的。与h264 H264 x264 X264混合也是ez。上次我使用时,我记得我为 DJI 设备使用了 h264 选项。确保选择正确的编解码器

ffmpeg 和 ffplay 将直接工作。我记得 Opencv 可以建立在这 2 个之上。所以使用 FFMEPG/FFSHOW 插件转换为 cv::Mat 应该不难。按照文档操作

OpenCV 可以使用 FFmpeg 库 (http://ffmpeg.org/) 作为后端 录制、转换和流式传输音频和视频。 FFMpeg 是一个完整的, 交叉引用解决方案。如果在配置时启用 FFmpeg OpenCV 比 CMake 会下载并安装二进制文件 OPENCV_SOURCE_CODE/3rdparty/ffmpeg/。要在运行时使用 FFMpeg,您 必须将 FFMepg 二进制文件与您的应用程序一起部署。

https://docs.opencv.org/3.4/d0/da7/videoio_overview.html

上次,我要玩 DJI PSDK。他们只允许在 UDP 端口 udp://192.168.5.293:23003 使用 H.264 进行流式传输 所以我写了一个简单的ffmpeg接口来流式传输到PSDK。但我必须事先调试它。所以我使用 ffplay 来显示这个网络流以证明它正在工作。这是显示流的脚本。所以你必须在此基础上工作才能作为 opencv 插件工作

ffplay -f h264 -i udp://192.168.1.45:23003 

【讨论】:

    猜你喜欢
    • 2019-09-12
    • 2017-07-15
    • 2015-04-11
    • 2015-01-21
    • 2015-01-04
    • 1970-01-01
    • 2017-05-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多