Как использовать изображение с камеры ARCore в OpenCV в приложении Unity для Android? - PullRequest
0 голосов
/ 03 апреля 2019

Я пытаюсь использовать OpenCV для распознавания жестов в моей игре Unity ARCore.Однако с устареванием TextureReaderAPI единственным способом захвата изображения с камеры является использование Frame.CameraImage.AcquireCameraImageBytes().Проблема заключается не только в том, что изображение имеет разрешение 640x480 (это нельзя изменить AFAIK), но и в формате YUV_420_888.Как будто этого было недостаточно, OpenCV не имеет бесплатных пакетов C # / Unity, поэтому, если я не хочу обналичивать 20 $ за платный пакет, мне нужно использовать доступные версии C ++ или python.Как переместить изображение YUV в OpenCV, преобразовать его в цветовое пространство RGB (или HSV), а затем либо выполнить некоторую обработку или вернуть его обратно в Unity?

Ответы [ 3 ]

2 голосов
/ 03 апреля 2019

В этом примере я буду использовать библиотеки C ++ OpenCV и Visual Studio 2017, и я попытаюсь захватить изображение с камеры ARCore, переместить его в OpenCV (настолько эффективно, насколько это возможно), преобразовать его в цветовое пространство RGB, а затем переместить обратно вКод Unity C # и сохраните его в памяти телефона.

Во-первых, нам нужно создать проект динамической библиотеки C ++ для использования с OpenCV.Для этого я настоятельно рекомендую следовать обоим ответам Пьера Барета и Ninjaman494 на этот вопрос: OpenCV + Android + Unity .Процесс довольно прост, и если вы не будете слишком сильно отклоняться от их ответов (то есть вы можете безопасно загрузить более новую версию OpenCV, чем 3.3.1, но будьте осторожны при компиляции для ARM64 вместо ARM и т. Д.), Вам следуетбыть в состоянии вызвать функцию C ++ из C #.

По моему опыту, мне пришлось решить две проблемы - во-первых, если вы сделаете проект частью своего решения C # вместо создания нового решения, Visual Studio сохранитвозиться с вашей конфигурацией, как пытаться скомпилировать версию x86 вместо версии ARM.Чтобы избавить себя от хлопот, создайте совершенно отдельное решение.Другая проблема заключается в том, что некоторые функции не смогли связать меня, что вызвало неопределенную ошибку ссылочного компоновщика (точнее, undefined reference to 'cv::error(int, std::string const&, char const*, char const*, int).Если это происходит, и проблема связана с функцией, которая вам на самом деле не нужна, просто пересоздайте функцию в своем коде - например, если у вас есть проблемы с cv::error, добавьте этот код в конец вашего файла .cpp:

namespace cv {
    __noreturn void error(int a, const String & b, const char * c, const char * d, int e) {
        throw std::string(b);
    }
}

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

Теперь у вас должен бытьрабочий код C ++, который компилируется и может быть запущен из приложения Unity для Android.Однако мы хотим, чтобы OpenCV не возвращал число, а преобразовывал изображение.Поэтому измените свой код на это:

.h файл

extern "C" {
    namespace YOUR_OWN_NAMESPACE
    {
        int ConvertYUV2RGBA(unsigned char *, unsigned char *, int, int);
    }
}

.cpp файл

extern "C" {
    int YOUR_OWN_NAMESPACE::ConvertYUV2RGBA(unsigned char * inputPtr, unsigned char * outputPtr, int width, int height) {

        // Create Mat objects for the YUV and RGB images. For YUV, we need a 
        // height*1.5 x width image, that has one 8-bit channel. We can also tell
        // OpenCV to have this Mat object "encapsulate" an existing array, 
        // which is inputPtr.
        // For RGB image, we need a height x width image, that has three 8-bit
        // channels. Again, we tell OpenCV to encapsulate the outputPtr array.
        // Thanks to specifying existing arrays as data sources, no copying
        // or memory allocation has to be done, and the process is highly
        // effective.
        cv::Mat input_image(height + height / 2, width, CV_8UC1, inputPtr);
        cv::Mat output_image(height, width, CV_8UC3, outputPtr);

        // If any of the images has not loaded, return 1 to signal an error.
        if (input_image.empty() || output_image.empty()) {
            return 1;
        }

        // Convert the image. Now you might have seen people telling you to use 
        // NV21 or 420sp instead of NV12, and BGR instead of RGB. I do not
        // understand why, but this was the correct conversion for me. 
        // If you have any problems with the color in the output image,
        // they are probably caused by incorrect conversion. In that case,
        // I can only recommend you the trial and error method.
        cv::cvtColor(input_image, output_image, cv::COLOR_YUV2RGB_NV12);

        // Now that the result is safely saved in outputPtr, we can return 0.
        return 0;
    }
}

Сейчас, перестройте решение (Ctrl + Shift + B) и скопируйте файл libProjectName.so в папку Unity Plugins/Android, как в связанном ответе.

Далее нужно сохранить изображение из ARCore, переместить его в C ++код и получить его обратно.Давайте добавим это в класс в нашем скрипте C #:

[DllImport("YOUR_OWN_NAMESPACE")]
    public static extern int ConvertYUV2RGBA(IntPtr input, IntPtr output, int width, int height);

Visual Studio предложит вам добавить System.Runtime.InteropServices с помощью предложения - сделайте это.Это позволяет нам использовать функцию C ++ в нашем коде C #.Теперь давайте добавим эту функцию в наш компонент C #:

public Texture2D CameraToTexture()
    {
        // Create the object for the result - this has to be done before the 
        // using {} clause.
        Texture2D result;

        // Use using to make sure that C# disposes of the CameraImageBytes afterwards
        using (CameraImageBytes camBytes = Frame.CameraImage.AcquireCameraImageBytes())
        {

            // If acquiring failed, return null
            if (!camBytes.IsAvailable)
            {
                Debug.LogWarning("camBytes not available");
                return null;
            }

            // To save a YUV_420_888 image, you need 1.5*pixelCount bytes.
            // I will explain later, why.

            byte[] YUVimage = new byte[(int)(camBytes.Width * camBytes.Height * 1.5f)];

            // As CameraImageBytes keep the Y, U and V data in three separate
            // arrays, we need to put them in a single array. This is done using
            // native pointers, which are considered unsafe in C#.
            unsafe
            {
                for (int i = 0; i < camBytes.Width * camBytes.Height; i++)
                {
                    YUVimage[i] = *((byte*)camBytes.Y.ToPointer() + (i * sizeof(byte)));
                }

                for (int i = 0; i < camBytes.Width * camBytes.Height / 4; i++)
                {
                    YUVimage[(camBytes.Width * camBytes.Height) + 2 * i] = *((byte*)camBytes.U.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                    YUVimage[(camBytes.Width * camBytes.Height) + 2 * i + 1] = *((byte*)camBytes.V.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                }
            }

            // Create the output byte array. RGB is three channels, therefore
            // we need 3 times the pixel count
            byte[] RGBimage = new byte[camBytes.Width * camBytes.Height * 3];

            // GCHandles help us "pin" the arrays in the memory, so that we can
            // pass them to the C++ code.
            GCHandle YUVhandle = GCHandle.Alloc(YUVimage, GCHandleType.Pinned);
            GCHandle RGBhandle = GCHandle.Alloc(RGBimage, GCHandleType.Pinned);

            // Call the C++ function that we created.
            int k = ConvertYUV2RGBA(YUVhandle.AddrOfPinnedObject(), RGBhandle.AddrOfPinnedObject(), camBytes.Width, camBytes.Height);

            // If OpenCV conversion failed, return null
            if (k != 0)
            {
                Debug.LogWarning("Color conversion - k != 0");
                return null;
            }

            // Create a new texture object
            result = new Texture2D(camBytes.Width, camBytes.Height, TextureFormat.RGB24, false);

            // Load the RGB array to the texture, send it to GPU
            result.LoadRawTextureData(RGBimage);
            result.Apply();

            // Save the texture as an PNG file. End the using {} clause to
            // dispose of the CameraImageBytes.
            File.WriteAllBytes(Application.persistentDataPath + "/tex.png", result.EncodeToPNG());
        }

        // Return the texture.
        return result;
    }

Чтобы иметь возможность запускать код unsafe, вам также необходимо разрешить его в Unity.Перейдите в настройки проигрывателя (Edit > Project Settings > Player Settings и установите флажок Allow unsafe code).

Теперь вы можете вызывать функцию CameraToTexture (), скажем, каждые 5 секунд из Update (), и изображение с камеры должносохранить как /Android/data/YOUR_APPLICATION_PACKAGE/files/tex.png.Изображение, вероятно, будет ориентировано на ландшафт, даже если вы держите телефон в портретном режиме, но это уже не сложно исправить.Кроме того, вы можете заметить зависание при каждом сохранении изображения - из-за этого я рекомендую вызывать эту функцию в отдельном потоке.Кроме того, самой сложной операцией здесь является сохранение изображения в виде файла PNG, поэтому, если вам это нужно по какой-либо другой причине, у вас все будет в порядке (все же используйте отдельный поток).

Если вы хотитеИзучите формат YUV_420_888, зачем вам нужен массив 1.5 * pixelCount, и почему мы изменили массивы так, как мы это делали, прочитайте https://wiki.videolan.org/YUV/#NV12. На других сайтах, похоже, неверная информация о том, как работает этот формат.

Кроме того, не стесняйтесь комментировать меня по любым вопросам, которые могут у вас возникнуть, и я постараюсь помочь с ними, а также с любыми отзывами о коде и ответе.

ПРИЛОЖЕНИЕ 1: Согласно https://docs.unity3d.com/ScriptReference/Texture2D.LoadRawTextureData.html, Вы должны использовать GetRawTextureData вместо LoadRawTextureData, чтобы предотвратить копирование.Для этого просто закрепите массив, возвращенный GetRawTextureData, вместо массива RGBimage (который вы можете удалить).Также не забудьте вызвать result.Apply ();впоследствии.

ПРИЛОЖЕНИЕ 2: Не забудьте вызвать Free () на обоих GCHandles, когда вы закончите использовать их.

0 голосов
/ 28 июня 2019

Для всех, кто хочет попробовать это с OpencvForUnity:

public Mat getCameraImage()
{
    // Use using to make sure that C# disposes of the CameraImageBytes afterwards
    using (CameraImageBytes camBytes = Frame.CameraImage.AcquireCameraImageBytes())
    {

        // If acquiring failed, return null
        if (!camBytes.IsAvailable)
        {
            Debug.LogWarning("camBytes not available");
            return null;
        }

        // To save a YUV_420_888 image, you need 1.5*pixelCount bytes.
        // I will explain later, why.
        byte[] YUVimage = new byte[(int)(camBytes.Width * camBytes.Height * 1.5f)];

        // As CameraImageBytes keep the Y, U and V data in three separate
        // arrays, we need to put them in a single array. This is done using
        // native pointers, which are considered unsafe in C#.
        unsafe
        {
            for (int i = 0; i < camBytes.Width * camBytes.Height; i++)
            {
                YUVimage[i] = *((byte*)camBytes.Y.ToPointer() + (i * sizeof(byte)));
            }

            for (int i = 0; i < camBytes.Width * camBytes.Height / 4; i++)
            {
                YUVimage[(camBytes.Width * camBytes.Height) + 2 * i] = *((byte*)camBytes.U.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                YUVimage[(camBytes.Width * camBytes.Height) + 2 * i + 1] = *((byte*)camBytes.V.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
            }
        }

        // Create the output byte array. RGB is three channels, therefore
        // we need 3 times the pixel count
        byte[] RGBimage = new byte[camBytes.Width * camBytes.Height * 3];

        // GCHandles help us "pin" the arrays in the memory, so that we can
        // pass them to the C++ code.
        GCHandle pinnedArray = GCHandle.Alloc(YUVimage, GCHandleType.Pinned);
        IntPtr pointer = pinnedArray.AddrOfPinnedObject();


        Mat input = new Mat(camBytes.Height + camBytes.Height / 2, camBytes.Width, CvType.CV_8UC1);
        Mat output = new Mat(camBytes.Height, camBytes.Width, CvType.CV_8UC3);

        Utils.copyToMat(pointer, input);

        Imgproc.cvtColor(input, output, Imgproc.COLOR_YUV2RGB_NV12);

        pinnedArray.Free();

        return output;
    }
}
0 голосов
/ 08 апреля 2019

Я разобрался, как получить изображение CPU с полным разрешением в Arcore 1.8.

Теперь я могу получить полное разрешение камеры с помощью cameraimagebytes.

поместите это в переменные вашего класса:

private ARCoreSession.OnChooseCameraConfigurationDelegate m_OnChoseCameraConfiguration = null;

поместите это в Start ()

m_OnChoseCameraConfiguration = _ChooseCameraConfiguration; ARSessionManager.RegisterChooseCameraConfigurationCallback(m_OnChoseCameraConfiguration); ARSessionManager.enabled = false; ARSessionManager.enabled = true;

Добавьте этот обратный вызов в класс:

private int _ChooseCameraConfiguration(List<CameraConfig> supportedConfigurations) { return supportedConfigurations.Count - 1; }

После того, как вы добавите их, у вас должны быть изображения камеры, возвращающие полное разрешение камеры.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...