zoukankan      html  css  js  c++  java
  • 大疆无人机 Android 开发总结——视频解码

            DJI_Mobile_SDK是大疆为开发者提供的开发无人机应用的开发接口,可以实现对无人机飞行的控制,也可以利用无人机相机完成一些视觉任务。目前网上的开发教程主要集中于DJI 开发者社区网上的资源非常少。废话不多说~~,现在将在Android项目中学习到的东西总结一下。

          使用大疆无人机做计算机视觉项目,第一步就是要将从云台相机中获取的视频流解析成图像帧,DJI在github上提供了视频解码成图像帧的Demo程序。官网说明文档并没有对如何将这个解码Demo集成进自己的项目进行说明,只是简单说明了DJIVideoStreamDecoder和NativeHelper类的主要用途。附上解码的源程序

    Android源代码地址https://github.com/DJI-Mobile-SDK-Tutorials/Android-VideoStreamDecodingSample.git

     

    下面就将对如何使用这个模块进行说明

    一、模块结构

          首先要说明的是,整个解码过程是通过FFmpeg和MediaCodec实现,按照官网的教程,DJIVideoStreamDecoder.java和NativeHelper.java是实现解码的关键类。按照官网的教程分为以下步骤:

    1. 初始化一个NativeHelper的实例对象,来监听来自无人机高空的视频数据。

    2.将原始的H.264视频数据送入FFmpeg中解析。

    3.将解析完成的视频数据从FFmpeg中取出,并将解析后的数据缓存到图像帧序列中

    4.将MediaCodec作为一个解码器,然后对视频中的I帧进行捕获。

    5.解码完成后,可为MediaCodec的输出数据配置一个TextureView或SurfaceView用来对视频画面进行预览,或者调用监听器对解码数据进行监听完成其他操作。

    6.释放FFmpeg和MediaCodec资源。

    二、解码调用

           看完上述步骤,我们对解码过程有了初步的认识,以下是DJIVideoStreamDecoder类中的变量。其中instance是解码类的实例,解码出的视频帧会存放在frameQueue中。handle类涉及线程控制,如果需要了解HandleThread的用法,请点击此链接。在Demo中解码线程已经全部实现,不需要我们再做任何处理。

          1.DJIVideoStreamDecoder.java

        private static DJIVideoStreamDecoder instance;
        private Queue<DJIFrame> frameQueue;
        private HandlerThread dataHandlerThread;
        private Handler dataHandler;
        private HandlerThread callbackHandlerThread;
        private Handler callbackHandler;
        private Context context;
        private MediaCodec codec;
        private Surface surface;
    
        public int frameIndex = -1;
        private long currentTime;
        public int width;
        public int height;
        private boolean hasIFrameInQueue = false;
        private boolean hasIFrameInCodec;
        private ByteBuffer[] inputBuffers;
        private ByteBuffer[] outputBuffers;
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        LinkedList<Long> bufferChangedQueue=new LinkedList<Long>();
    
        private long createTime;

    2.Mainactivity.java

           实现流数据转换为图像的关键步骤在MainActivity.java中实现,值得注意的是在Android系统中,图像是以YUVImage的格式传递,因此,在存储数据的时候要使用YUV图像格式,对于每秒解析的图像帧数量,通过DJIVIdeoStreamDecoder.getInstance().frameIndex控制,比如Demo中对30取余,表示仅对序号为30的倍数的图像帧存储,如果每秒帧率为30,则每秒只取一帧图像。进而可通过调节分母的大小实现取帧频率的控制。

      将raw数据解析成YUV格式图像的源代码

    @Override
        public void onYuvDataReceived(byte[] yuvFrame, int width, int height) {
            //In this demo, we test the YUV data by saving it into JPG files.
            if (DJIVideoStreamDecoder.getInstance().frameIndex % 30 == 0) {
                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);
                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];

       将Buffer中的raw数据整理成jpeg图像

        /* Save the buffered data into a JPG image file*/
        private void screenShot(byte[] buf, String shotDir) {
            File dir = new File(shotDir);
            if (!dir.exists() || !dir.isDirectory()) {
                dir.mkdirs();
            }
            YuvImage yuvImage = new YuvImage(buf,
                    ImageFormat.NV21,
                    DJIVideoStreamDecoder.getInstance().width,
                    DJIVideoStreamDecoder.getInstance().height,
                    null);
            OutputStream outputFile;
            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,
                        DJIVideoStreamDecoder.getInstance().width,
                        DJIVideoStreamDecoder.getInstance().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);
                }
            });
        }
    
        public void onClick(View v) {
            if (screenShot.isSelected()) {
                screenShot.setText("Screen Shot");
                screenShot.setSelected(false);
                if (useSurface) {
                    DJIVideoStreamDecoder.getInstance().changeSurface(videostreamPreviewSh.getSurface());
                }
                savePath.setText("");
                savePath.setVisibility(View.INVISIBLE);
            } else {
                screenShot.setText("Live Stream");
                screenShot.setSelected(true);
                if (useSurface) {
                    DJIVideoStreamDecoder.getInstance().changeSurface(null);
                }
                savePath.setText("");
                savePath.setVisibility(View.VISIBLE);
                pathList.clear();
            }
        }
    
        private void displayPath(String path){
            path = path + "
    
    ";
            if(pathList.size() < 6){
                pathList.add(path);
            }else{
                pathList.remove(0);
                pathList.add(path);
            }
            StringBuilder stringBuilder = new StringBuilder();
            for(int i = 0 ;i < pathList.size();i++){
                stringBuilder.append(pathList.get(i));
            }
            savePath.setText(stringBuilder.toString());
        }

      在大疆的Demo程序中,选择采用存储磁盘的方式来获取是各帧。处理函数为Mainactivity类中screenShot(byte[] buf, String shotDir)方法在此方法中使用Android内置类YUVImage的compressToJpeg()方法以流的方式进行存储,存储路径通过shotDir传入。

      以上就是关于DJI 无人机截取取图像帧的介绍,获取图像帧之后就可进行各式各样的图像任务了。

      小菜鸟一个,大家一起学习交流咯。 

  • 相关阅读:
    推荐系统-01-简单逻辑回归
    顶部BANNER
    大数据-12-Spark+Kafka构建实时分析Dashboard
    大数据-10-Spark入门之支持向量机SVM分类器
    大数据-11-案例演习-淘宝双11数据分析与预测
    大数据-09-Intellij idea 开发java程序操作HDFS
    大数据-08-Sqoop入门
    大数据-07-Spark之流数据
    准确度,精确度, 召回率
    [转]springcloud(九):配置中心和消息总线(配置中心终结版)
  • 原文地址:https://www.cnblogs.com/fancy-li/p/11439985.html
Copyright © 2011-2022 走看看