大疆無人機 Android 開發總結——視頻解碼

来源:https://www.cnblogs.com/fancy-li/archive/2019/08/31/11439985.html
-Advertisement-
Play Games

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 無人機截取取圖像幀的介紹,獲取圖像幀之後就可進行各式各樣的圖像任務了。

  小菜鳥一個,大家一起學習交流咯。 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Navicat Premium 功能很強大,支持不同資料庫客戶端的連接,並且使用工具可以生成兩個庫差異的sql腳本,方便dev與test環境表結構同步,具體操作方法如下 單擊運行,實現兩個庫中模式表結構同步,也可導出sql腳本,發給測試人員自己執行sql腳本。 Navicat Premium 12. ...
  • 1. MySQL中的函數 <1>加密函數 <2>聚合函數 <3>數學相關的函數 <4>字元相關的函數 <5>日期函數 <6>流程式控制制函數 2. 用戶的創建和授權(DCL) <1>授權的語法格式 <2>取消許可權的語法格式 <3>顯示授予的許可權 <4>刪除用戶 3.MySQL中的索引 <1>概念: <2 ...
  • 一、Flume簡介 Apache Flume 是一個分散式,高可用的數據收集系統。它可以從不同的數據源收集數據,經過聚合後發送到存儲系統中,通常用於日誌數據的收集。Flume 分為 NG 和 OG (1.0 之前) 兩個版本,NG 在 OG 的基礎上進行了完全的重構,是目前使用最為廣泛的版本。下麵的 ...
  • https://wangde.xin/images/article/mysql/mysql_com.png ...
  • DDL的全稱Data Definition Language,即數據定義語言 DDL的語法有:create、alter、drop、rename、truncate。對此做一個詳細的解釋: create (創建) create 可以創建資料庫 create 可以創建表格 創建表格的語法:方括弧的表示可以 ...
  • https://www.oracle.com/technetwork/cn/topics/index-088165-zhs.html 下載地址Orion是Oracle提供的IO性能測試工具,運行該工具不需要安裝oracle database軟體或創建資料庫。 它可以模擬Oracle資料庫的IO負載,... ...
  • 最近遇到了這個案例,官方文檔已有詳盡的分析、介紹,特轉載在此,方便以後查看! Full UNDO Tablespace In 10gR2 and above (文檔 ID 413732.1) 轉到底部 In this Document Symptoms Changes Cause Solution ... ...
  • 寫了一個bat命令,定期去清理一些SQL Server的Dump文件,然後配置成SQL Server作業,作業執行時報許可權錯誤,具體錯誤信息如下所示: Message Executed as user: NT Service\SQLSERVERAGENT. The process could not... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...