Table of Contents

Создание расширения для ввода изображений

Перед началом

Создание класса источника данных внешнего кадра

Наследуйте от ExternalImageStreamFrameSource, чтобы создать расширение для ввода изображений. Это подкласс MonoBehaviour, и имя файла должно соответствовать имени класса.

Например:

public class MyFrameSource : ExternalImageStreamFrameSource
{
}

Пример Workflow_FrameSource_ExternalImageStream представляет собой реализацию расширения для ввода изображений, использующего в качестве входных данных видео, записанное на ARCore на мобильном устройстве. Это видео было захвачено через обратный вызов камеры на ARCore (Pixel2) (не запись экрана).

Определение устройства

Переопределите IsCameraUnderControl и верните true.

Переопределите IsHMD для определения, является ли устройство гарнитурой.

Например, установите false при использовании видео в качестве ввода.

protected override bool IsHMD => false;

Переопределите Display для определения дисплея устройства.

Например, если приложение работает только на телефоне, используйте Display.DefaultSystemDisplay, чье значение вращения автоматически изменяется в соответствии с текущим состоянием отображения операционной системы.

protected override IDisplay Display => easyar.Display.DefaultSystemDisplay;

Доступность

Переопределите IsAvailable, чтобы определить, доступно ли устройство.

Например, при использовании видео в качестве ввода оно всегда доступно:

protected override Optional<bool> IsAvailable => true;

Если IsAvailable не может быть определено во время сборки сессии, переопределите корутину CheckAvailability(), чтобы заблокировать процесс сборки до тех пор, пока не станет ясно, доступно ли устройство.

Виртуальная камера

Переопределите Camera, чтобы предоставить виртуальную камеру.

Например, иногда можно использовать Camera.main в качестве виртуальной камеры для сессии:

protected override Camera Camera => Camera.main;

Физическая камера

Используйте тип FrameSourceCamera для переопределения DeviceCameras с целью предоставления информации о физических камерах устройства. Эти данные используются при вводе кадров с камеры. Должны быть созданы, когда CameraFrameStarted имеет значение true.

Например, используя видео из образца Workflow_FrameSource_ExternalImageStream:

private FrameSourceCamera deviceCamera;
protected override List<FrameSourceCamera> DeviceCameras => new List<FrameSourceCamera> { deviceCamera };

{
    var size = new Vector2Int(640, 360);
    var cameraType = CameraDeviceType.Back;
    var cameraOrientation = 90;
    deviceCamera = new FrameSourceCamera(cameraType, cameraOrientation, size, new Vector2(30, 30));
    started = true;
}
Осторожно

Несколько входных параметров здесь должны быть настроены в соответствии с фактически используемым видео. Параметры в приведенном выше коде подходят только для видео из образца.

Переопределите CameraFrameStarted, чтобы указать начало ввода кадров с камеры.

Например:

protected override bool CameraFrameStarted => started;

Запуск и остановка сессии

Переопределите OnSessionStart(ARSession) и выполните инициализацию, специфичную для AR. Убедитесь, что сначала вызывается base.OnSessionStart.

Например:

protected override void OnSessionStart(ARSession session)
{
    base.OnSessionStart(session);
    ...
}

Это подходящее место для включения камеры устройства, особенно если она не предназначена для постоянной работы. Также здесь можно получить калибровочные данные, которые не изменяются в течение жизненного цикла. Иногда может потребоваться подождать, пока устройство будет готово или данные обновятся, прежде чем их можно будет получить.

Также здесь можно запустить цикл ввода данных. Этот цикл также можно написать в Update() или другом методе, особенно если данные нужно получать в определенный момент порядка выполнения Unity. Не вводите данные, пока сессия не будет готова (ready).

При необходимости можно игнорировать процесс запуска и проверять данные при каждом обновлении; это полностью зависит от конкретных требований.

Например, при использовании видео в качестве ввода здесь можно начать воспроизведение видео и запустить цикл ввода данных:

protected override void OnSessionStart(ARSession session)
{
    base.OnSessionStart(session);
    ...
    player.Play();
    StartCoroutine(VideoDataToInputFrames());
}

Переопределите OnSessionStop() и освободите ресурсы. Убедитесь, что вызывается base.OnSessionStop.

Например, при использовании видео в качестве ввода здесь можно остановить воспроизведение видео и освободить связанные ресурсы:

protected override void OnSessionStop()
{
    base.OnSessionStop();

    StopAllCoroutines();
    player.Stop();
    if (renderTexture) { Destroy(renderTexture); }
    cameraParameters?.Dispose();
    cameraParameters = null;
    frameIndex = -1;
    started = false;
    deviceCamera?.Dispose();
    deviceCamera = null;
}

Получение данных кадров камеры с устройства или из файла

Изображения можно получать из любого источника: системной камеры, USB-камеры, видеофайла, сети и т.д. Главное — преобразовать данные в формат, требуемый для Image. Способ получения данных с этих устройств или из файлов различается, поэтому необходимо обращаться к соответствующей документации.

Например, при использовании видео в качестве входа можно использовать Texture2D.ReadPixels(Rect, int, int, bool) для получения данных кадра камеры из RenderTexture видеоплеера, а затем скопировать данные из Texture2D.GetRawTextureData() в Buffer:

void VideoDataToInputFrames()
{
    ...
    RenderTexture.active = renderTexture;
    var pixelSize = new Vector2Int((int)player.width, (int)player.height);
    var texture = new Texture2D(pixelSize.x, pixelSize.y, TextureFormat.RGB24, false);
    texture.ReadPixels(new Rect(0, 0, pixelSize.x, pixelSize.y), 0, 0);
    texture.Apply();
    RenderTexture.active = null;
    ...
    CopyRawTextureData(buffer, texture.GetRawTextureData<byte>(), pixelSize);
} 

static unsafe void CopyRawTextureData(Buffer buffer, Unity.Collections.NativeArray<byte> data, Vector2Int size)
{
    int oneLineLength = size.x * 3;
    int totalLength = oneLineLength * size.y;
    var ptr = new IntPtr(data.GetUnsafeReadOnlyPtr());
    for (int i = 0; i < size.y; i++)
    {
        buffer.tryCopyFrom(ptr, oneLineLength * i, totalLength - oneLineLength * (i + 1), oneLineLength);
    }
}
Осторожно

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

Параллельно с получением изображения необходимо получить калибровочные данные камеры (или эквивалентной ей) и создать экземпляр CameraParameters.

Если исходные данные поступают из обратного вызова камеры мобильного телефона и не были обрезаны вручную, то можно напрямую использовать калибровочные данные камеры телефона. При получении данных через интерфейсы типа ARCore или ARKit, обратитесь к их документации для получения внутренних параметров камеры. Если используемая AR-функция — это отслеживание изображений или объектов, в этом случае также можно использовать CameraParameters.createWithDefaultIntrinsics(Vec2I, CameraDeviceType, int) для создания внутренних параметров камеры. Это может незначительно повлиять на эффективность алгоритмов, но обычно влияние невелико.

Если данные поступают из других источников, таких как USB-камеры или видеофайлы, не сгенерированные из обратного вызова камеры, то необходимо выполнить калибровку камеры или видеофреймов для получения корректных внутренних параметров.

Осторожно

Данные из обратного вызова камеры нельзя обрезать. После обрезки необходимо пересчитать внутренние параметры. Если данные получены из изображений, захваченных через экранную запись и т.п., обычно нельзя использовать калибровочные данные камеры телефона. В этом случае также требуется калибровка камеры или видеофреймов для получения корректных внутренних параметров.

Некорректные внутренние параметры приведут к некорректной работе AR-функций. Распространенные проблемы: виртуальный контент не выравнивается с реальными объектами, отслеживание AR часто не запускается или легко теряется.

Например, для видео, используемого в примере Workflow_FrameSource_ExternalImageStream, соответствующие внутренние параметры камеры и процесс создания CameraParameters выглядят так:

var size = new Vector2Int(640, 360);
var cameraType = CameraDeviceType.Back;
var cameraOrientation = 90;
cameraParameters = new CameraParameters(size.ToEasyARVector(), new Vec2F(506.085f, 505.3105f), new Vec2F(318.1032f, 177.6514f), cameraType, cameraOrientation);
Осторожно

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

Ввод данных кадров камеры

После получения обновления данных кадров камеры вызовите HandleCameraFrameData(double, Image, CameraParameters), чтобы ввести данные кадров камеры.

Например, при использовании видео в качестве ввода реализация выглядит следующим образом:

IEnumerator VideoDataToInputFrames()
{
    yield return new WaitUntil(() => player.isPrepared);
    var pixelSize = new Vector2Int((int)player.width, (int)player.height);
    ...
    yield return new WaitUntil(() => player.isPlaying && player.frame >= 0);
    while (true)
    {
        yield return null;
        if (frameIndex == player.frame) { continue; }
        frameIndex = player.frame;
        ...
        var pixelFormat = PixelFormat.RGB888;
        var bufferO = TryAcquireBuffer(pixelSize.x * pixelSize.y * 3);
        if (bufferO.OnNone) { continue; }

        var buffer = bufferO.Value;
        CopyRawTextureData(buffer, texture.GetRawTextureData<byte>(), pixelSize);

        using (buffer)
        using (var image = Image.create(buffer, pixelFormat, pixelSize.x, pixelSize.y, pixelSize.x, pixelSize.y))
        {
            HandleCameraFrameData(player.time, image, cameraParameters);
        }
    }
}
Осторожно

Не забудьте выполнить Dispose() или освободить Image, Buffer и другие связанные данные с помощью механизмов вроде using после использования. В противном случае произойдет серьезная утечка памяти, и получение буфера из buffer pool также может завершиться неудачей.

Связанные темы