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 無人機截取取圖像幀的介紹,獲取圖像幀之後就可進行各式各樣的圖像任務了。
小菜鳥一個,大家一起學習交流咯。