// %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); } } } }