В этом примере я буду использовать библиотеки 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, когда вы закончите использовать их.