欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

大疆无人机 Android 开发总结——视频解码

程序员文章站 2022-10-25 18:39:37
DJI_Mobile_SDK是大疆为开发者提供的开发无人机应用的开发接口,可以实现对无人机飞行的控制,也可以利用无人机相机完成一些视觉任务。目前网上的开发教程主要集中于DJI 开发者社区,网上的资源非常少。废话不多说~~,现在将在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 + "\n\n";
        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 无人机截取取图像帧的介绍,获取图像帧之后就可进行各式各样的图像任务了。

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