// %BANNER_BEGIN%
// ---------------------------------------------------------------------
// %COPYRIGHT_BEGIN%
// Copyright (c) 2022 Magic Leap, Inc. All Rights Reserved.
// Use of this file is governed by the Software License Agreement, located here: https://www.magicleap.com/software-license-agreement-ml2
// Terms and conditions applicable to third-party materials accompanying this distribution may also be found in the top-level NOTICE file appearing herein.
// %COPYRIGHT_END%
// ---------------------------------------------------------------------
// %BANNER_END%
// Disabling deprecated warning for the internal project
#pragma warning disable 618
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace UnityEngine.XR.MagicLeap
{
///
/// APIs to access the depth camera data.
/// This is an experimental API which may be modified or removed without any prior notice.
/// The API only supports reading data from the depth camera. Apps cannot modify the camera settings, support for the same may be added in a future release.
///
public partial class MLDepthCamera : MLAutoAPISingleton
{
public const int FrameTypeCount = 2;
///
/// Depth Camera modes
/// Future release may add support to other modes.
///
public enum Stream
{
None = 0,
///
/// Long range mode
/// Under normal operations long range mode has a maximum frequency of 5fps and a range of up to 5m, in some cases this can go as far 7.5m.
///
LongRange = 1 << 0,
///
/// Short range mode
/// Under normal operations short range stream has a maximum frequency of 60fps and a range from 0.2m up to 0.9m.
///
ShortRange = 1 << 1
}
///
/// Depth Camera frame capture types
///
public enum FrameType
{
///
/// Frame captured using mode.
///
LongRange,
///
/// Frame captured using mode.
///
ShortRange
}
///
/// Enumeration of possible frame rates
///
public enum FrameRate
{
FPS_1,
FPS_5,
FPS_25,
FPS_30,
FPS_50,
FPS_60
}
///
/// Flags used to specify what kind of data to request from Depth Camera
///
[Flags]
public enum CaptureFlags
{
///
/// Enable DepthImage. See for more details.
///
DepthImage = 1 << 0,
///
/// Enable ConfidenceBuffer. See for more details.
///
Confidence = 1 << 1,
///
/// Enable DepthFlagsBuffer. See for more details.
///
DepthFlags = 1 << 2,
///
/// Enable AmbientRawDepthImage. See for more details.
///
AmbientRawDepthImage = 1 << 3,
///
/// Enable RawDepthImage. See for more details.
///
RawDepthImage = 1 << 4
}
///
/// Flags to select data requested from depth camera.
///
[Flags]
public enum DepthFlags
{
///
/// Indicates that there is no additional flag data for this pixel.
///
Valid = 0 << 0,
///
/// This bit is set to one to indicate that one or more flags from below have
/// been set. Depending on the use case the application can correlate the
/// flag data and corresponding pixel data to determine how to handle the pixel
///
Invalid = 1 << 0,
///
/// The pixel intensity is either below the min or the max threshold value.
///
Saturated = 1 << 1,
///
/// Inconsistent data received when capturing frames. This can happen due to
/// fast motion.
///
Inconsistent = 1 << 2,
///
/// Pixel has very low signal to noise ratio. One example of when this can
/// happen is for pixels in far end of the range.
///
LowSignal = 1 << 3,
///
/// This typically happens when there is step jump in the distance of adjoining
/// pixels in the scene. Example: When you open a door looking into the room the
/// edges along the door's edges can cause flying pixels.
///
FlyingPixel = 1 << 4,
///
/// If this bit is on it indicates that the corresponding pixel may not be within
/// the projector's illumination cone.
///
Masked = 1 << 5,
///
/// This bit will be set when there is high noise.
///
SBI = 1 << 8,
///
/// This could happen when there is another light source apart from the depth
/// camera projector. This could also lead to .
///
StrayLight = 1 << 9,
///
/// If a small group of is sorrunded by a set of
/// then this bit will be set to 1.
///
ConnectedComponent = 1 << 10
}
public struct StreamConfig
{
///
/// Flags to configure the depth data.
///
public uint Flags;
///
/// Exposure in microseconds.
///
public uint Exposure;
///
/// Frame Rate.
///
public FrameRate FrameRateConfig;
}
///
/// Depth Camera Settings
/// API Level 29
///
public struct Settings
{
///
/// The system may not be able to service all the requested streams at any
/// given time. This parameter is treated as a hint and data will be
/// provided for the requested streams if available.
///
public Stream Streams;
///
/// Controls for each of the depth camera streams.
/// Only controls for streams enabled via streams field will be read.
/// Use MLDepthCamera.FrameType enumeration for indexing.
///
public StreamConfig[] StreamConfig;
}
///
/// Depth camera intrinsic parameters.
/// API Level 292
///
public struct Intrinsics
{
///
/// Camera Width
///
public uint Width;
///
/// Camera Height
///
public uint Height;
///
/// Camera Focal Length
///
public Vector2 FocalLength;
///
/// Camera Principal Point
///
public Vector2 PrincipalPoint;
///
/// Field of View in degrees
///
public float FoV;
///
/// Set of distortion coefficients.
/// The distortion coefficients are arranged in the following order: [k1, k2, p1, p2, k3]
///
public DistortionCoefficients Distortion;
///
/// Convenience method to retrieve a list of the Distortion coefficient values in the correct order.
///
///
public List GetDistortionList()
{
return new List() { Distortion.K1, Distortion.K2, Distortion.P1, Distortion.P2, Distortion.K3 };
}
}
///
/// The distortion coefficients are arranged in the following order: [k1, k2, p1, p2, k3]
///
public readonly struct DistortionCoefficients
{
///
/// Distortion coefficient k1
///
public readonly double K1;
///
/// Distortion coefficient k2
///
public readonly double K2;
///
/// Distortion coefficient p1
///
public readonly double P1;
///
/// Distortion coefficient p2
///
public readonly double P2;
///
/// Distortion coefficient k3
///
public readonly double K3;
public DistortionCoefficients(double[] coefficients)
{
if (coefficients == null || coefficients.Length < 5)
{
throw new ArgumentException("DistortionCoefficients constructor must receive an array of 5 values and the array cannot be null.");
}
K1 = coefficients[0];
K2 = coefficients[1];
P1 = coefficients[2];
P2 = coefficients[3];
K3 = coefficients[4];
}
}
///
/// Per-plane info for each depth camera frame.
/// API Level 29
///
public struct FrameBuffer
{
///
/// Width of the buffer in pixels.
///
public uint Width;
///
/// Height of the buffer in pixels.
///
public uint Height;
///
/// Stride of the buffer in bytes.
///
public uint Stride;
///
/// Number of bytes used to represent a single value.
///
public uint BytesPerUnit;
///
/// Buffer data.
///
public byte[] Data;
public override string ToString()
{
return $"[FrameBuffer W: {Width}, H: {Height}, Stride: {Stride}, BPU: {BytesPerUnit}, Data: {(Data != null ? Data.Length + " bytes" : "null")}]";
}
}
///
/// Structure to encapsulate a possible configuration for a single stream.
/// Can be used to understand possible values for a specific StreamConfig element in MLDepthCameraSettings.
/// The capabilities supported by the depth camera can be queried with InternalGetCapabilities().
///
public struct StreamCapability
{
///
/// Stream for which this capability can be applied.
///
public Stream Stream;
///
/// Minimum sensor exposure in microseconds.
///
public uint MinExposure;
///
/// Maximum sensor exposure in microseconds.
///
public uint MaxExposure;
///
/// Frame rate.
///
public FrameRate FrameRateCapability;
}
///
/// The settings the Depth Camera is currently configured with.
///
public static Settings CurrentSettings { get; private set; }
///
/// Sets the current settings of Depth Camera.
///
///
public static void SetSettings(Settings settings) => CurrentSettings = settings;
public static bool IsConnected { get; private set; }
private bool connectionPaused;
protected override MLResult.Code StartAPI() => MLResult.Code.Ok;
protected override MLResult.Code StopAPI()
{
var result = MLResult.Code.Ok;
if (IsConnected)
{
result = InternalDisconnect().Result;
}
return result;
}
protected override void OnApplicationPause(bool pauseStatus)
{
base.OnApplicationPause(pauseStatus);
if (pauseStatus)
{
if (IsConnected)
{
InternalDisconnect(true);
}
}
else
{
if (connectionPaused)
{
InternalConnect(CurrentSettings);
}
}
}
///
/// Connect to depth camera.
/// API Level 29
/// permissions com.magicleap.permission.DEPTH_CAMERA (protection level: dangerous)
///
///
/// MLResult.Code.InvalidParam: One of the parameters is invalid.
/// MLResult.Code.Ok: Connected to camera device(s) successfully.
/// MLResult.Code.PermissionDenied: Necessary permission is missing.
/// MLResult.Code.LicenseError: Necessary license is missing.
/// MLResult.Code.UnspecifiedFailure: The operation failed with an unspecified error.
///
public static MLResult Connect() => Instance.InternalConnect(CurrentSettings);
///
/// Disconnect from depth camera.
/// API Level 29
/// permissions None
///
///
/// MLResult.Code.InvalidParam: The camera's handle was invalid.
/// MLResult.Code.Ok: Disconnected camera successfully.
/// MLResult.Code.UnspecifiedFailure: Failed to disconnect camera for some unknown reason.
///
public static MLResult Disconnect() => Instance.InternalDisconnect();
///
/// Update the depth camera settings.
/// API Level 29
/// permissions None
///
/// New for the depth camera.
///
/// MLResult.Code.InvalidParam: The camera's handle was invalid.
/// MLResult.Code.Ok: Settings updated successfully.
/// MLResult.Code.UnspecifiedFailure: Failed due to internal error.
///
public static MLResult UpdateSettings(Settings settings) => Instance.InternalUpdateSettings(settings);
///
/// Poll for Frames.
/// Returns a object referencing the latest available frame data, if any.
/// This is a blocking call. API is not thread safe.
/// If there are no new depth data frames for a given duration (duration determined by the system) then the API will return .
/// API Level 29
/// permissions None
///
/// Timeout in milliseconds.
/// Depth camera data. Will be null if no valid data is available when called.
///
/// MLResult.Code.InvalidParam: The camera's handle was invalid.
/// MLResult.Code.Ok: Depth camera data fetched successfully.
/// MLResult.Code.Timeout: No frame available within time limit.
/// MLResult.Code.UnspecifiedFailure: Failed due to internal error.
///
public static MLResult GetLatestDepthData(ulong timeoutMs, out Data data) => Instance.InternalGetLatestDepthData(timeoutMs, out data);
public static MLResult GetCapabilities(out StreamCapability[] capabilities) => Instance.InternalGetCapabilities(out capabilities);
#region internal
private MLResult InternalConnect(Settings settings)
{
if (!MLResult.DidNativeCallSucceed(MLPermissions.CheckPermission(MLPermission.DepthCamera).Result))
{
MLPluginLog.Error($"{nameof(MLDepthCamera)} requires missing permission {MLPermission.DepthCamera}");
return MLResult.Create(MLResult.Code.PermissionDenied);
}
var camSettings = NativeBindings.MLDepthCameraSettings.Init();
camSettings.Streams = (uint)settings.Streams;
camSettings.StreamConfig = settings.StreamConfig;
var resultCode = NativeBindings.MLDepthCameraConnect(in camSettings, out Handle);
if (MLResult.DidNativeCallSucceed(resultCode, nameof(NativeBindings.MLDepthCameraConnect)))
{
IsConnected = true;
}
return MLResult.Create(resultCode);
}
private MLResult InternalDisconnect(bool paused = false)
{
var resultCode = NativeBindings.MLDepthCameraDisconnect(Handle);
MLResult.DidNativeCallSucceed(resultCode, nameof(NativeBindings.MLDepthCameraDisconnect));
connectionPaused = paused;
if (!connectionPaused)
{
IsConnected = false;
}
return MLResult.Create(resultCode);
}
private MLResult InternalUpdateSettings(Settings settings)
{
var depthCamSettings = NativeBindings.MLDepthCameraSettings.Init();
depthCamSettings.Streams = (uint)settings.Streams;
depthCamSettings.StreamConfig = settings.StreamConfig;
var resultCode = NativeBindings.MLDepthCameraUpdateSettings(Handle, in depthCamSettings);
if (MLResult.DidNativeCallSucceed(resultCode, nameof(NativeBindings.MLDepthCameraUpdateSettings)))
{
CurrentSettings = settings;
}
return MLResult.Create(resultCode);
}
private MLResult InternalGetLatestDepthData(ulong timeoutMs, out Data data)
{
var depthCamData = NativeBindings.MLDepthCameraData.Init();
var resultCode = NativeBindings.MLDepthCameraGetLatestDepthData(Handle, timeoutMs, out depthCamData);
// in this case, a Timeout is an acceptable result that we don't need to log as an error to the console.
bool resultIsOk = (resultCode == MLResult.Code.Ok || resultCode == MLResult.Code.Timeout);
if (!MLResult.DidNativeCallSucceed(resultCode, nameof(NativeBindings.MLDepthCameraGetLatestDepthData), showError: !resultIsOk))
{
data = null;
}
else
{
FrameBuffer CreateFromPtr(IntPtr ptr)
{
var result = new FrameBuffer();
if (ptr == IntPtr.Zero)
{
return result;
}
var plane = (NativeBindings.MLDepthCameraFrameBuffer)Marshal.PtrToStructure(ptr, typeof(NativeBindings.MLDepthCameraFrameBuffer));
byte[] bytes = null;
if (plane.Data != IntPtr.Zero)
{
bytes = new byte[plane.Size];
Marshal.Copy(plane.Data, bytes, 0, bytes.Length);
}
result = new FrameBuffer()
{
Width = plane.Width,
Height = plane.Height,
Stride = plane.Stride,
BytesPerUnit = plane.BytesPerUnit,
Data = bytes
};
return result;
}
MarshalUnmananagedArrayToStructArray(depthCamData.Frames, depthCamData.FrameCount, out NativeBindings.MLDepthCameraFrame[] managedArray);
// TODO: Revisit this section if mixed mode (LR+SR) is supported. Currently the array should only have 1 element so it should be safe to assume only the first is needed for now.
var frame = managedArray[0];
var depthMap = CreateFromPtr(frame.DepthImageFrameBufferPtr);
var confidenceMap = CreateFromPtr(frame.ConfidenceBufferFrameBufferPtr);
var depthFlags = CreateFromPtr(frame.DepthFlagsBufferFrameBufferPtr);
var aiMap = CreateFromPtr(frame.AmbientRawDepthImageFrameBufferPtr);
var depthImage = CreateFromPtr(frame.RawDepthImageFrameBufferPtr);
data = new Data()
{
FrameNumber = frame.FrameNumber,
FrameTimestamp = frame.FrameTimestamp,
FrameType = frame.FrameType,
Position = Native.MLConvert.ToUnity(frame.CameraPose.Position),
Rotation = Native.MLConvert.ToUnity(frame.CameraPose.Rotation),
Intrinsics = NativeBindings.MLDepthCameraIntrinsics.ToManaged(frame.Intrinsics),
DepthImage = (depthMap.Data != null) ? depthMap : null,
ConfidenceBuffer = (confidenceMap.Data != null) ? confidenceMap : null,
DepthFlagsBuffer = (depthFlags.Data != null) ? depthFlags : null,
AmbientRawDepthImage = (aiMap.Data != null) ? aiMap : null,
RawDepthImage = (depthImage.Data != null) ? depthImage : null
};
// CAPI specifies that Release should be called exactly once for each successful call to GetLatest
// Since we return a managed "copy" of the data in the out parameter, then we can immediately do this rather than require the application developer to do it themselves.
var releaseResult = NativeBindings.MLDepthCameraReleaseDepthData(Handle, ref depthCamData);
MLResult.DidNativeCallSucceed(releaseResult, nameof(NativeBindings.MLDepthCameraReleaseDepthData));
}
return MLResult.Create(resultCode);
}
private MLResult InternalGetCapabilities(out StreamCapability[] capabilities)
{
var filter = NativeBindings.MLDepthCameraCapabilityFilter.Init();
filter.Streams = (uint)CurrentSettings.Streams;
var resultCode = NativeBindings.MLDepthCameraGetCapabilities(Handle, ref filter, out NativeBindings.MLDepthCameraCapabilityList outCaps);
MLResult.DidNativeCallSucceed(resultCode, nameof(NativeBindings.MLDepthCameraGetCapabilities));
MarshalUnmananagedArrayToStructArray(outCaps.Capabilities, outCaps.Size, out NativeBindings.MLDepthCameraCapability[] managedCapabilitiesArray);
capabilities = new StreamCapability[outCaps.Size];
for (int i = 0; i < capabilities.Length; i++)
{
MarshalUnmananagedArrayToStructArray(managedCapabilitiesArray[i].StreamCapabilities, managedCapabilitiesArray[i].Size, out NativeBindings.MLDepthCameraStreamCapability[] managedStreamCapabilitiesArray);
// TODO: Revisit this section if mixed mode (LR+SR) is supported. Currently the array should only have 1 element so it should be safe to assume only the first is needed for now.
capabilities[i] = new StreamCapability();
capabilities[i].Stream = managedStreamCapabilitiesArray[0].Stream;
capabilities[i].MinExposure = managedStreamCapabilitiesArray[0].MinExposure;
capabilities[i].MaxExposure = managedStreamCapabilitiesArray[0].MaxExposure;
capabilities[i].FrameRateCapability = managedStreamCapabilitiesArray[0].FrameRateCapability;
}
var releaseResult = NativeBindings.MLDepthCameraReleaseCapabilities(Handle, ref outCaps);
MLResult.DidNativeCallSucceed(releaseResult, nameof(NativeBindings.MLDepthCameraReleaseCapabilities));
return MLResult.Create(resultCode);
}
#endregion
private void MarshalUnmananagedArrayToStructArray(IntPtr unmanagedArray, int length, out T[] mangagedArray)
{
var size = Marshal.SizeOf(typeof(T));
mangagedArray = new T[length];
for (int i = 0; i < length; i++)
{
IntPtr ins = new IntPtr(unmanagedArray.ToInt64() + i * size);
mangagedArray[i] = Marshal.PtrToStructure(ins);
}
}
}
}