Как читать DJI H264 FPV Feed как объект OpenCV Mat? - PullRequest
1 голос
/ 24 мая 2019

TDLR: Все разработчики DJI выиграют от декодирования необработанных байтовых массивов видеопотока H264 в формат, совместимый с OpenCV.

Я потратил много времени на поиски решения для чтения FPV-канала DJI как объекта OpenCV Mat.Я, вероятно, упускаю из виду что-то фундаментальное, так как я не слишком знаком с кодировкой / декодированием изображений.

Будущие разработчики, которые сталкиваются с этим, скорее всего, столкнутся с кучей тех же проблем, что и у меня.Было бы замечательно, если бы DJI-разработчики могли использовать opencv напрямую, без необходимости использования сторонней библиотеки.

Я готов использовать ffmpeg или JavaCV, если это необходимо, но для большинства разработчиков Android это довольно сложно, так как мы собираемсяиспользовать cpp, ndk, терминал для тестирования и т. д. Это похоже на перебор.Оба варианта кажутся довольно трудоемкими. Это преобразование JavaCV H264 кажется излишне сложным.Я нашел это из этого соответствующего вопроса .

Я считаю, что проблема заключается в том, что нам нужно декодировать как байтовый массив длины 6 (информационный массив), так и байтовый массив с информацией о текущем кадре одновременно.

По сути, FPV-канал DJI поставляется в нескольких форматах.

  1. Необработанный H264 (MPEG4) в VideoFeeder. VideoDataListener
    // 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);
            }
        }
    };

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
                    }
                }
            });
        }
    }

Редактировать: Для тех, кто хочет увидеть функцию DJI YUV to 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;
    }

DJI также, похоже, имеет функцию «getRgbaData», но буквально нет ни одного примера онлайн или от DJI.Идем дальше, и Google "DJI getRgbaData" ... Есть только ссылка на документацию API, которая объясняет самоочевидные параметры и возвращаемые значения, но больше ничего.Я не мог понять, где это вызывать, и там нет функции обратного вызова, как в случае с YUV.Вы не можете вызывать его из байтового массива h264b напрямую, но, возможно, вы можете получить его из данных yuv.

Вариант 1 намного предпочтительнее варианта 2, так как формат YUV имеет проблемы с качеством.Вариант 3 также может включать в себя декодер.

Вот скриншот, который производит собственное преобразование YUV от DJI.WalletPhoneYuv

Я рассмотрел несколько вещей о том, как улучшить YUV, удалить зеленый и желтый цвета и еще много чего, но на этом этапе, если DJI не может это сделатьверно, я не хочу вкладывать туда ресурсы.

Что касается варианта 1, я знаю, что есть FFMPEG и JavaCV, которые кажутся хорошими вариантами, если мне нужно идти по пути декодирования видео.

Более того, насколько я понимаю, OpenCV не может обрабатывать чтение и запись видеофайлов без FFMPEG, но я не пытаюсь читать видеофайл, я пытаюсь прочитать байт H264 / MPEG4 []массив.Следующий код, похоже, дает положительные результаты.

    /* 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
        }
    }

Rows = 4, Columns> 30k.Я получаю много значений RGB, которые кажутся действительными, такие как красный = 113, зеленый = 75, синий = 90, альфа = 220 в качестве выдуманного примера;однако я получаю тонну 0,0,0,0 значений.Это должно быть немного хорошо, так как у черных 0,0,0 (хотя я бы подумал, что альфа будет выше), и у меня есть черный объект на моем изображении.Я также, кажется, не получаю никаких значений белого 255, 255, 255, хотя есть также много белой области.Я не регистрирую весь байт, чтобы он мог быть там, но я еще не видел его.

Однако, когда я пытаюсь вычислить контуры из этого изображения, я почти всегда получаю, что моменты (центрх, у) точно в центре изображения.Эта ошибка не имеет ничего общего с моим цветным фильтром или алгоритмом контуров, так как я написал скрипт на python и проверил, правильно ли я реализовал его в Android, прочитав неподвижное изображение и получив одинаковое количество контуров, позиции и т. Д. В обоих 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

Мои вопросы:

I.Это правильный формат для чтения байта h264 как мат?Предполагая, что форматом является RGBA, это означает, что строка = 4 и столбцы = byte []. Length и CvType.CV_8UC4.У меня правильные высота и ширина?Что-то говорит мне, что высота и ширина YUV отключены.Я получил несколько значимых результатов, но контуры были точно в центре, как и в H264.

II.OpenCV обрабатывает MP4 в Android, как это?Если нет, нужно ли нам использовать FFMPEG или JavaCV?

III.Размер int имеет к этому какое-то отношение?Почему размер int иногда равен 6, а иногда - от 2400 до 6000?Я слышал о разнице между информацией об этом кадре и информацией о следующем кадре, но я просто недостаточно осведомлен, чтобы знать, как применить это здесь.

Я начинаю думать, что именно здесьвопрос лжи.Поскольку мне нужно получить 6-байтовый массив для информации о следующем кадре, возможно, мой модуль 30 неверен.Так должен ли я передавать 29-й или 31-й кадр как байт формата для каждого кадра?Как это сделать в opencv или мы обречены использовать сложный ffmpeg?Как мне присоединиться к соседним кадрам / байтовым массивам?

IV.Могу ли я исправить это с помощью 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 decoder выглядит так сложно?Это заставляет меня быть осторожным с желанием попробовать ffmpeg или Javacv и просто придерживаться декодера Android или opencv.

VI.На каком этапе я должен изменить размеры фреймов, чтобы ускорить вычисления?

Редактировать: Поддержка DJI вернулась ко мне и подтвердила, что у них нет образцов для выполнения того, что я описал.Это время для сообщества, чтобы сделать это доступным для всех!

После дальнейших исследований я не думаю, что opencv сможет справиться с этим, поскольку Android OpenCV SDK не имеет функциональности для видеофайлов / URL (кроме доморощенного кодека MJPEG).

Так есть ли в Android способ конвертировать в mjpeg или аналогичный для чтения?В моем приложении мне нужно только 1 или 2 кадра в секунду, поэтому, возможно, я могу сохранить изображение в формате JPEG.

Но для приложений реального времени нам, вероятно, потребуется написать собственный декодер.Пожалуйста, помогите, чтобы мы могли сделать это доступным для всех!Этот вопрос кажется многообещающим:

1 Ответ

1 голос
/ 29 мая 2019

Прежде всего H264 и h264 различны. Его также можно смешивать с h264 H264 x264 X264. В прошлый раз, когда я использую, я помню, я использую опцию h264 для устройства DJI. Убедитесь, что вы выбрали правильный кодек

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 
...