Camera capture timestamp delta

Our async camera capture returns a timestamp; here's an example seen in logcat:

2024/02/11 18:34:27.864 19388 19405 Debug ml_camera_client captured image buffer 0 timestamp = 3356834169448

If we poll the timestamp (using 'resultExtras.VCamTimestamp' or 'resultExtras.VCamTimestamp.Value'), the timestamp differs quite a bit from that given by 'ml_camera_client' above:

Log($"Camera Timestamp: {resultExtras.VCamTimestamp.Value}"):

2024/02/11 18:34:27.881 19388 19408 Info Unity [Revok] Camera Timestamp: 3363421750000

Shouldn't those timestamps be identical?

Environment:

  • Magic Leap SDK v1.5.0
  • Magic Leap Unity 2.0.0
  • Unity 2022.3.13f1 (Win10)
  • We follow these config notes

The system is most likely logging logging the frame in micro or nanoseconds where the unity value is logging the MLTime which is required for the MLCVCameraGetFramePose function. This conversion will be simpler with the addition of the OpenXR ML Pixel Sensor API, that will be able to use nanosecods directly

Thanks @kbabilinski -- the image buffer timestamp may be in system time as you suggest, but it's close enough to the MLTime reported that it still makes me wonder.

I ask because we want to coordinate a captured frame as closely as possible with the camera extrinsics polling.

Currently we're setting a callback which receives the MLTime timestamp MLCamera.resultExtras.VCamTimestamp once a frame is captured, then passes that time to MLCVCamera.GetFramePose.

Is there a more accurate method?

In my experience, the autofocus steps can take ~1 second -- or more! When we call colorCamera.CaptureImageAsync(), I'm not sure at what point the exposure is actually taken in the function -- there may be some processing and frame checking before the method returns with MLCamera.resultExtras.

That is correct, using the callback method and passing the VCamTimestamp is the most efficient way of capturing the camera image and frame pose. Are you running into any issues in particular when using this method?

This was working fine until I updated from v1.8.0 to v2.0.0: now I get the same pose failure as recently posted in this forum topic.

That is, the 'if (poseResult == MLResult.Code.Ok)' evaluation in the following method returns false:

private void OnCaptureRawImageComplete(MLCamera.CameraOutput capturedImage, MLCamera.ResultExtras resultExtras, MLCamera.Metadata metadataHandle)
{
Log("Image capture complete.");

// Adjust the directory path to include 'capture'
string directoryPath = Path.Combine(Application.persistentDataPath, "capture");

// Check if the 'capture' directory exists, create it if needed
if (!Directory.Exists(directoryPath))
{
    Directory.CreateDirectory(directoryPath);
}

// Adjust file paths to use the updated directoryPath
string fileName = $"{resultExtras.VCamTimestamp}.txt";
string filePath = Path.Combine(directoryPath, fileName);

string imagePath = Path.Combine(directoryPath, $"{resultExtras.VCamTimestamp}.png");
File.WriteAllBytes(imagePath, capturedImage.Planes[0].Data);
Log($"Image saved to {imagePath}");

// Attempt to get the camera pose
if (resultExtras.VCamTimestamp > 0)
{
    Matrix4x4 outTransform;
    MLResult poseResult = MLCVCamera.GetFramePose(resultExtras.VCamTimestamp, out outTransform);

    if (poseResult == MLResult.Code.Ok)
    {
        // Prepare the content for the file
        string fileContent = "intrinsics\n";
        fileContent += resultExtras.Intrinsics.HasValue ? resultExtras.Intrinsics.Value.ToString() : "Not Available";

        fileContent += "\nextrinsics\n";
        fileContent += outTransform.ToString();

        // Write to the file
        File.WriteAllText(filePath, fileContent);
        Log($"Data saved to {filePath}");
    }
    else
    {
        Log($"Failed to obtain camera pose: {poseResult}");
    }
}
else
{
    Log("VCamTimestamp is not valid.");
}

}

Thank you for that additional information. Are you using the OpenXR workflow or the Magic Leap XR Plugin?

Magic Leap XR, thanks. We'll continue to test here to isolate the recent change.

When using SDK v 2.0.0 and the XR workflow I was able to get the camera pose using the following sample script : Simple Camera Example | MagicLeap Developer Documentation

I replaced the RawVideoFrameAvailable function with the following snippet

    void RawVideoFrameAvailable(MLCamera.CameraOutput output, MLCamera.ResultExtras extras, MLCameraBase.Metadata metadataHandle)
    {
        if (output.Format == MLCamera.OutputFormat.RGBA_8888)
        {
            //Flips the frame vertically so it does not appear upside down.
            MLCamera.FlipFrameVertically(ref output);
            UpdateRGBTexture(ref _videoTextureRgb, output.Planes[0], _screenRendererRGB);
        }

        MLResult result = MLCVCamera.GetFramePose(extras.VCamTimestamp, out Matrix4x4 outMatrix);
        if (result.IsOk)
        {
            string cameraExtrinsics = "Camera Extrinsics";
            cameraExtrinsics += "Position " + outMatrix.GetPosition();
            cameraExtrinsics += "Rotation " + outMatrix.rotation;
            Debug.Log(cameraExtrinsics);
        }
    }

Thanks @kbabilinski for your notes and working code.

The code you posted is for video capture, but we're using still image capture for better control of autofocus, white balance, et cetera. Also, we need to capture at 4096x3072.

I've posted our code below, which in cross-testing I can confirm works using Unity framework [v1.8.0] but doesn't work on [v2.0.0]. In both Unity projects, we're using MLSDK 1.5.0 and Unity Editor 2022.3.13f1.

Specifically, with [v1.8.0] we are able to call the image prep, save a full-resolution image, and retrieve both intrinsics and extrinsics as text files. When running Unity with ML API [v2.0.0], we receive a NULL image and see the following error in logcat:

Error Camera-Device endConfigure fail Status(-8, EX_SERVICE_SPECIFIC): '3: endConfigure:739: Camera 0: Unsupported set of inputs/outputs provided'

Is there a change in handling ML Cam in the later release?

  1. Strangely, when the capture fails, we still get a zero-byte image file. I added logging, which shows size and data length are zero, but Data returns 'Data available':

Image capture complete.
Number of planes: 1
Planes[0].Size: 0
Planes[0].Data length: 0 bytes
Planes[0].Data: Data available
Image saved to /storage/emulated/0/Android/data/.../236186942000.jpg

  1. In a previous post you were able to crash your ML2 (as I was) by setting MLCamera.CaptureFrameRate._15FPS. That thread suggests this is fixed, but I can confirm it's not: setting 15FPS in the code below will crash the ML2 and require both the device and system to be cycled to recover(!)

  2. Note that we're saving as .jpg, which in another of your posts you note shouldn't work: "The Magic Leap 2's CV Camera stream does not support JPG image capture at this time."

Here's our working code (with[v1.8.0], that is):

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MagicLeap.Core;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.MagicLeap;

//############################
//  ___                 _   
// | . \ ___  _ _  ___ | |__
// |   // ._>| | |/ . \| / /
// |_\_\\___.|__/ \___/|_\_\
//                         
// Magic Leap Revok PoC
// (c) 2024 Plexus Software LLC
//
// This script exports ML2 camera data: images, instrinsics and extrinsics.

public class RGBCamCapture : MonoBehaviour
{
    private static RGBCamCapture _instance;
    public static RGBCamCapture Instance
    {
        get
        {
            if (_instance == null)
            {
                var existingInstance = FindObjectOfType<RGBCamCapture>();
                if (existingInstance != null)
                {
                    _instance = existingInstance;
                }
                else
                {
                    GameObject newGameObject = new GameObject("RGBCamCapture");
                    _instance = newGameObject.AddComponent<RGBCamCapture>();
                }
            }
            return _instance;
        }
    }

    private bool isCameraConnected = false;
    private MLCamera colorCamera;
    private bool cameraDeviceAvailable;
    private readonly MLPermissions.Callbacks permissionCallbacks = new MLPermissions.Callbacks();

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);

        permissionCallbacks.OnPermissionGranted += OnPermissionGranted;
        permissionCallbacks.OnPermissionDenied += OnPermissionDenied;
        permissionCallbacks.OnPermissionDeniedAndDontAskAgain += OnPermissionDenied;
    }

    void Start()
    {
        MLResult result = MLPermissions.RequestPermission(MLPermission.Camera, permissionCallbacks);

        if (!result.IsOk)
        {
            Debug.LogErrorFormat("Error: RGBCamCapture failed to get requested permissions, disabling script. Reason: {0}", result);
            enabled = false;
        }
    }

    void OnDisable()
    {
        permissionCallbacks.OnPermissionGranted -= OnPermissionGranted;
        permissionCallbacks.OnPermissionDenied -= OnPermissionDenied;
        permissionCallbacks.OnPermissionDeniedAndDontAskAgain -= OnPermissionDenied;

        if (colorCamera != null && isCameraConnected)
        {
            DisableMLCamera();
        }
    }

    private void OnPermissionDenied(string permission)
    {
        LogError($"{permission} denied, camera functionality most likley won't work.");
    }

    private void OnPermissionGranted(string permission)
    {
        StartCoroutine(EnableMLCamera());
    }

    private IEnumerator EnableMLCamera()
    {
        while (!cameraDeviceAvailable)
        {
            MLResult result = MLCamera.GetDeviceAvailabilityStatus(MLCamera.Identifier.Main, out cameraDeviceAvailable);
            if (!(result.IsOk && cameraDeviceAvailable))
            {
                yield return new WaitForSeconds(1.0f);
            }
        }

        Log("Camera device available.");
        ConnectCamera();
    }

    private async void ConnectCamera()
    {
        MLCamera.ConnectContext context = MLCamera.ConnectContext.Create();
        context.EnableVideoStabilization = false;
        context.Flags = MLCameraBase.ConnectFlag.CamOnly;

        try
        {
            colorCamera = await MLCamera.CreateAndConnectAsync(context);
            if (colorCamera != null)
            {
                colorCamera.OnRawImageAvailable += OnCaptureRawImageComplete;
                isCameraConnected = true;
                Log("Camera device connected.");
                // Optionally, configure and prepare capture here or elsewhere after connection
                ConfigureAndPrepareCapture();
            }
            else
            {
                LogError("Failed to connect MLCamera: colorCamera is null.");
            }
        }
        catch (System.Exception e)
        {
            LogError($"Failed to connect MLCamera: {e.Message}");
        }
    }

    private void ConfigureAndPrepareCapture()
    {
        // Assuming configuration setup is similar to the async example, without the async/await pattern
        // We set 30FPS capture to avoid problems: https://forum.magicleap.cloud/t/camera-capture-in-unity/2718/13
        MLCamera.CaptureConfig captureConfig = new MLCamera.CaptureConfig()
        {
            StreamConfigs = new[]
            {
                new MLCamera.CaptureStreamConfig()
                {
                    OutputFormat = MLCamera.OutputFormat.JPEG,
                    CaptureType = MLCamera.CaptureType.Image,
                    Width = 1920,
                    Height = 1080
                }
            },
            CaptureFrameRate = MLCamera.CaptureFrameRate._30FPS
        };

        MLResult result = colorCamera.PrepareCapture(captureConfig, out MLCamera.Metadata _);
        if (!result.IsOk)
        {
            LogError("Failed to prepare camera for capture.");
        }
    }

    public async void CaptureImage()
    {
        if (!isCameraConnected || colorCamera == null)
        {
            LogError("[Revok] Camera is not connected - cannot capture image.");
            return;
        }

        Log("[Revok] Preparing for image capture with AE/AWB adjustments.");

        // Attempt to adjust auto-exposure and white balance asynchronously before capture.
        var aeawbResult = await colorCamera.PreCaptureAEAWBAsync();
        if (!aeawbResult.IsOk)
        {
            LogError("[Revok] PreCaptureAEAWB failed.");
            return;
        }

        Log("[Revok] AE/AWB adjustment complete. Capturing image...");

        // Capture the image asynchronously.
        var captureResult = await colorCamera.CaptureImageAsync();
        if (!captureResult.IsOk)
        {
            LogError("[Revok] Image capture failed.");
        }
        else
        {
            Log("[Revok] Image capture successful.");
        }
    }

    private void DisableMLCamera()
    {
        if (colorCamera != null)
        {
            colorCamera.Disconnect();
            isCameraConnected = false;
        }
    }

    public async Task CaptureImageAsync()
    {
        if (isCameraConnected && colorCamera != null)
        {
            Log("[Revok] Preparing for image capture with AE/AWB adjustments.");

            var aeAwbResult = await colorCamera.PreCaptureAEAWBAsync();
            if (!aeAwbResult.IsOk)
            {
                LogError("[Revok] PreCaptureAEAWB failed.");
                return;
            }

            Log("[Revok] AE/AWB adjustment complete. Capturing image...");

            var captureResult = await colorCamera.CaptureImageAsync();
            if (!captureResult.IsOk)
            {
                LogError("[Revok] Image capture failed.");
            }
            else
            {
                Log("[Revok] Image capture successful.");
            }
        }
        else
        {
            LogError("[Revok] Camera is not connected - cannot capture image.");
        }
    }

    private void OnCaptureRawImageComplete(MLCamera.CameraOutput capturedImage, MLCamera.ResultExtras resultExtras, MLCamera.Metadata metadataHandle)
    {
        Log("Image capture complete.");

        // Adjust the directory path to include 'capture'
        string directoryPath = Path.Combine(Application.persistentDataPath, "capture");
        // Check if the 'capture' directory exists, create it if needed
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }

        // Adjust file paths to use the updated directoryPath
        string fileName = $"{resultExtras.VCamTimestamp}.txt";
        string filePath = Path.Combine(directoryPath, fileName);

        string imagePath = Path.Combine(directoryPath, $"{resultExtras.VCamTimestamp}.png");
        File.WriteAllBytes(imagePath, capturedImage.Planes[0].Data);
        Log($"Image saved to {imagePath}");

        // Attempt to get the camera pose
        if (resultExtras.VCamTimestamp > 0)
        {
            Matrix4x4 outTransform;
            MLResult poseResult = MLCVCamera.GetFramePose(resultExtras.VCamTimestamp, out outTransform);

            if (poseResult == MLResult.Code.Ok)
            {
                // Prepare the content for the file
                string fileContent = "intrinsics\n";
                fileContent += resultExtras.Intrinsics.HasValue ? resultExtras.Intrinsics.Value.ToString() : "Not Available";
                fileContent += "\nextrinsics\n";
                fileContent += outTransform.ToString();

                // Write to the file
                File.WriteAllText(filePath, fileContent);
                Log($"Data saved to {filePath}");
            }
            else
            {
                Log($"Failed to obtain camera pose: {poseResult}");
            }
        }
        else
        {
            Log("VCamTimestamp is not valid.");
        }
    }

    private void Log(string message)
    {
        Debug.Log("[Revok] " + message);
    }

    private void LogError(string message)
    {
        Debug.LogError("[Revok] " + message);
    }

}

Thank you for reporting this. I have reached out internally to see what might have caused the issue. For now, you may want to use the 1.12.0 release instead of 2.0.0 as it may be related to our transition to OpenXR.

Regarding the crash when setting the frame rate to 15fps, did you test on a device running the 1.5.0 OS?

Yes, for this our environment is:

  • Magic Leap SDK v1.5.0
  • Magic Leap Unity 1.1.8
  • Unity 2022.3.13f1 (Win10)
  • We follow these config notes