diff --git a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/Hair Segmentation.unity b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/Hair Segmentation.unity index 07344ef0e..af967f8d2 100644 --- a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/Hair Segmentation.unity +++ b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/Hair Segmentation.unity @@ -242,12 +242,18 @@ PrefabInstance: objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 5713b959e3c77a58fb258133fc8e4aef, type: 3} ---- !u!224 &107400409 stripped -RectTransform: - m_CorrespondingSourceObject: {fileID: 3259285889726014650, guid: 5713b959e3c77a58fb258133fc8e4aef, +--- !u!114 &107400409 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 3259285889726014649, guid: 5713b959e3c77a58fb258133fc8e4aef, type: 3} m_PrefabInstance: {fileID: 107400408} m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!4 &107400410 stripped Transform: m_CorrespondingSourceObject: {fileID: 7074087083388479136, guid: 5713b959e3c77a58fb258133fc8e4aef, @@ -275,8 +281,6 @@ MonoBehaviour: annotation: {fileID: 1452360049} _maskWidth: 512 _maskHeight: 512 - _minAlpha: 0.9 - _maxAlpha: 1 --- !u!114 &107400413 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 2020366215714292840, guid: 5713b959e3c77a58fb258133fc8e4aef, @@ -508,12 +512,27 @@ PrefabInstance: type: 3} propertyPath: screen value: - objectReference: {fileID: 1625703806} + objectReference: {fileID: 0} - target: {fileID: 5850198979010014858, guid: 24d349d9c78db0f3d94d4657029f4c5e, type: 3} propertyPath: _screen value: - objectReference: {fileID: 1625703806} + objectReference: {fileID: 107400409} + - target: {fileID: 5850198979010014858, guid: 24d349d9c78db0f3d94d4657029f4c5e, + type: 3} + propertyPath: _maskShader + value: + objectReference: {fileID: 4800000, guid: 7adfa9ebe754989caa35dbb2c5526d1c, type: 3} + - target: {fileID: 5850198979010014858, guid: 24d349d9c78db0f3d94d4657029f4c5e, + type: 3} + propertyPath: _maskTexture + value: + objectReference: {fileID: 0} + - target: {fileID: 5850198979010014858, guid: 24d349d9c78db0f3d94d4657029f4c5e, + type: 3} + propertyPath: _minConfidence + value: 0.275 + objectReference: {fileID: 0} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 24d349d9c78db0f3d94d4657029f4c5e, type: 3} --- !u!114 &1452360049 stripped @@ -528,78 +547,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: f993d16dcccfcecb6892ad3cc2ea76c8, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &1625703804 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1625703805} - - component: {fileID: 1625703807} - - component: {fileID: 1625703806} - m_Layer: 5 - m_Name: HairMask Screen - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1625703805 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1625703804} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 107400409} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1625703806 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1625703804} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 0} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Texture: {fileID: 0} - m_UVRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 ---- !u!222 &1625703807 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1625703804} - m_CullTransparentMesh: 1 --- !u!1 &1806680259 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/HairSegmentationGraph.cs b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/HairSegmentationGraph.cs index 4d2cfb04b..76d34d346 100644 --- a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/HairSegmentationGraph.cs +++ b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/HairSegmentationGraph.cs @@ -100,9 +100,26 @@ private SidePacket BuildSidePacket(ImageSource imageSource) var sidePacket = new SidePacket(); SetImageTransformationOptions(sidePacket, imageSource); - var outputRotation = imageSource.isHorizontallyFlipped ? imageSource.rotation.Reverse() : imageSource.rotation; + + // TODO: refactoring + // The orientation of the output image must match that of the input image. + var isInverted = CoordinateSystem.ImageCoordinate.IsInverted(imageSource.rotation); + var outputRotation = imageSource.rotation; + var outputHorizontallyFlipped = !isInverted && imageSource.isHorizontallyFlipped; + var outputVerticallyFlipped = imageSource.isVerticallyFlipped ^ (isInverted && imageSource.isHorizontallyFlipped); + + if ((outputHorizontallyFlipped && outputVerticallyFlipped) || outputRotation == RotationAngle.Rotation180) + { + outputRotation = outputRotation.Add(RotationAngle.Rotation180); + outputHorizontallyFlipped = !outputHorizontallyFlipped; + outputVerticallyFlipped = !outputVerticallyFlipped; + } + sidePacket.Emplace("output_rotation", new IntPacket((int)outputRotation)); + sidePacket.Emplace("output_horizontally_flipped", new BoolPacket(outputHorizontallyFlipped)); + sidePacket.Emplace("output_vertically_flipped", new BoolPacket(outputVerticallyFlipped)); + Logger.LogDebug($"output_rotation = {outputRotation}, output_horizontally_flipped = {outputHorizontallyFlipped}, output_vertically_flipped = {outputVerticallyFlipped}"); return sidePacket; } } diff --git a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_cpu.txt b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_cpu.txt index 23a17748a..9ff92b230 100644 --- a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_cpu.txt +++ b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_cpu.txt @@ -165,5 +165,7 @@ node: { calculator: "ImageTransformationCalculator" input_stream: "IMAGE:hair_mask_rotated" input_side_packet: "ROTATION_DEGREES:output_rotation" + input_side_packet: "FLIP_HORIZONTALLY:output_horizontally_flipped" + input_side_packet: "FLIP_VERTICALLY:output_vertically_flipped" output_stream: "IMAGE:hair_mask" -} \ No newline at end of file +} diff --git a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_gpu.txt b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_gpu.txt index 17b103a28..18bcaf1c3 100644 --- a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_gpu.txt +++ b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_gpu.txt @@ -171,6 +171,8 @@ node: { calculator: "ImageTransformationCalculator" input_stream: "IMAGE_GPU:hair_mask_gpu" input_side_packet: "ROTATION_DEGREES:output_rotation" + input_side_packet: "FLIP_HORIZONTALLY:output_horizontally_flipped" + input_side_packet: "FLIP_VERTICALLY:output_vertically_flipped" output_stream: "IMAGE_GPU:hair_mask_unrotated_gpu" } diff --git a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_opengles.txt b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_opengles.txt index 34553df0a..ca6a849ce 100644 --- a/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_opengles.txt +++ b/Assets/Mediapipe/Samples/Scenes/Hair Segmentation/hair_segmentation_opengles.txt @@ -164,6 +164,8 @@ node: { calculator: "ImageTransformationCalculator" input_stream: "IMAGE_GPU:hair_mask_gpu" input_side_packet: "ROTATION_DEGREES:output_rotation" + input_side_packet: "FLIP_HORIZONTALLY:output_horizontally_flipped" + input_side_packet: "FLIP_VERTICALLY:output_vertically_flipped" output_stream: "IMAGE_GPU:hair_mask_unrotated_gpu" } diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/Formats/ImageFrame.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/Formats/ImageFrame.cs index 1ce5d5ebf..043e157be 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/Formats/ImageFrame.cs +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Framework/Formats/ImageFrame.cs @@ -59,6 +59,112 @@ private static void ReleasePixelData(IntPtr ptr) // Do nothing (pixelData is moved) } + /// + /// The number of channels for a . + /// If channels don't make sense in the , returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// + public static int NumberOfChannelsForFormat(ImageFormat.Types.Format format) + { + switch (format) + { + case ImageFormat.Types.Format.Srgb: + case ImageFormat.Types.Format.Srgb48: + return 3; + case ImageFormat.Types.Format.Srgba: + case ImageFormat.Types.Format.Srgba64: + case ImageFormat.Types.Format.Sbgra: + return 4; + case ImageFormat.Types.Format.Gray8: + case ImageFormat.Types.Format.Gray16: + return 1; + case ImageFormat.Types.Format.Vec32F1: + return 1; + case ImageFormat.Types.Format.Vec32F2: + return 2; + case ImageFormat.Types.Format.Lab8: + return 3; + case ImageFormat.Types.Format.Ycbcr420P: + case ImageFormat.Types.Format.Ycbcr420P10: + case ImageFormat.Types.Format.Unknown: + default: + return 0; + } + } + + /// + /// The channel size for a . + /// If channels don't make sense in the , returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// + public static int ChannelSizeForFormat(ImageFormat.Types.Format format) + { + switch (format) + { + case ImageFormat.Types.Format.Srgb: + case ImageFormat.Types.Format.Srgba: + case ImageFormat.Types.Format.Sbgra: + return sizeof(byte); + case ImageFormat.Types.Format.Srgb48: + case ImageFormat.Types.Format.Srgba64: + return sizeof(ushort); + case ImageFormat.Types.Format.Gray8: + return sizeof(byte); + case ImageFormat.Types.Format.Gray16: + return sizeof(ushort); + case ImageFormat.Types.Format.Vec32F1: + case ImageFormat.Types.Format.Vec32F2: + // sizeof float may be wrong since it's platform-dependent, but we assume that it's constant across all supported platforms. + return sizeof(float); + case ImageFormat.Types.Format.Lab8: + return sizeof(byte); + case ImageFormat.Types.Format.Ycbcr420P: + case ImageFormat.Types.Format.Ycbcr420P10: + case ImageFormat.Types.Format.Unknown: + default: + return 0; + } + } + + /// + /// The depth of each channel in bytes for a . + /// If channels don't make sense in the , returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// + public static int ByteDepthForFormat(ImageFormat.Types.Format format) + { + switch (format) + { + case ImageFormat.Types.Format.Srgb: + case ImageFormat.Types.Format.Srgba: + case ImageFormat.Types.Format.Sbgra: + return 1; + case ImageFormat.Types.Format.Srgb48: + case ImageFormat.Types.Format.Srgba64: + return 2; + case ImageFormat.Types.Format.Gray8: + return 1; + case ImageFormat.Types.Format.Gray16: + return 2; + case ImageFormat.Types.Format.Vec32F1: + case ImageFormat.Types.Format.Vec32F2: + return 4; + case ImageFormat.Types.Format.Lab8: + return 1; + case ImageFormat.Types.Format.Ycbcr420P: + case ImageFormat.Types.Format.Ycbcr420P10: + case ImageFormat.Types.Format.Unknown: + default: + return 0; + } + } + public bool IsEmpty() { return SafeNativeMethods.mp_ImageFrame__IsEmpty(mpPtr); @@ -92,28 +198,40 @@ public int Height() return SafeNativeMethods.mp_ImageFrame__Height(mpPtr); } + /// + /// The channel size. + /// If channels don't make sense, returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// public int ChannelSize() { - var code = SafeNativeMethods.mp_ImageFrame__ChannelSize(mpPtr, out var value); - - GC.KeepAlive(this); - return ValueOrFormatException(code, value); + return ChannelSizeForFormat(Format()); } + /// + /// The Number of channels. + /// If channels don't make sense, returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// public int NumberOfChannels() { - var code = SafeNativeMethods.mp_ImageFrame__NumberOfChannels(mpPtr, out var value); - - GC.KeepAlive(this); - return ValueOrFormatException(code, value); + return NumberOfChannelsForFormat(Format()); } + /// + /// The depth of each image channel in bytes. + /// If channels don't make sense, returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// public int ByteDepth() { - var code = SafeNativeMethods.mp_ImageFrame__ByteDepth(mpPtr, out var value); - - GC.KeepAlive(this); - return ValueOrFormatException(code, value); + return ByteDepthForFormat(Format()); } public int WidthStep() @@ -128,15 +246,19 @@ public IntPtr MutablePixelData() public int PixelDataSize() { - return SafeNativeMethods.mp_ImageFrame__PixelDataSize(mpPtr); + return Height() * WidthStep(); } + /// + /// The total size the pixel data would take if it was stored contiguously (which may not be the case). + /// If channels don't make sense, returns 0. + /// + /// + /// Unlike the original implementation, this API won't signal SIGABRT. + /// public int PixelDataSizeStoredContiguously() { - var code = SafeNativeMethods.mp_ImageFrame__PixelDataSizeStoredContiguously(mpPtr, out var value); - - GC.KeepAlive(this); - return ValueOrFormatException(code, value); + return Width() * Height() * ByteDepth() * NumberOfChannels(); } public void SetToZero() @@ -196,63 +318,6 @@ public Color32[] GetPixels32(bool flipVertically = false) return GetPixels32(flipVertically, new Color32[Width() * Height()]); } - /// - /// Get the value of a specific channel only. - /// It's useful when only one channel is used (e.g. Hair Segmentation mask). - /// - /// - /// Specify from which channel the data will be retrieved. - /// For example, if the format is RGB, 0 means R channel, 1 means G channel, and 2 means B channel. - /// - /// - /// The array to which the output data will be written. - /// - public byte[] GetChannel(int channelNumber, bool flipVertically, byte[] colors) - { - var format = Format(); - -#pragma warning disable IDE0010 - switch (format) - { - case ImageFormat.Types.Format.Srgb: - { - if (channelNumber < 0 || channelNumber > 3) - { - throw new ArgumentException($"There are only 3 channels, but No. {channelNumber} is specified"); - } - ReadChannel(MutablePixelData(), channelNumber, 3, Width(), Height(), WidthStep(), flipVertically, colors); - return colors; - } - case ImageFormat.Types.Format.Srgba: - { - if (channelNumber < 0 || channelNumber > 4) - { - throw new ArgumentException($"There are only 4 channels, but No. {channelNumber} is specified"); - } - ReadChannel(MutablePixelData(), channelNumber, 4, Width(), Height(), WidthStep(), flipVertically, colors); - return colors; - } - default: - { - throw new NotImplementedException($"Currently only SRGB and SRGBA format are supported: {format}"); - } - } -#pragma warning restore IDE0010 - } - - /// - /// Get the value of a specific channel only. - /// It's useful when only one channel is used (e.g. Hair Segmentation mask). - /// - /// - /// Specify from which channel the data will be retrieved. - /// For example, if the format is RGB, 0 means R channel, 1 means G channel, and 2 means B channel. - /// - public byte[] GetChannel(int channelNumber, bool flipVertically) - { - return GetChannel(channelNumber, flipVertically, new byte[Width() * Height()]); - } - private delegate MpReturnCode CopyToBufferHandler(IntPtr ptr, IntPtr buffer, int bufferSize); private T[] CopyToBuffer(CopyToBufferHandler handler, int bufferSize) where T : unmanaged @@ -271,19 +336,6 @@ private T[] CopyToBuffer(CopyToBufferHandler handler, int bufferSize) where T return buffer; } - private T ValueOrFormatException(MpReturnCode code, T value) - { - try - { - code.Assert(); - return value; - } - catch (MediaPipeException) - { - throw new FormatException($"Invalid image format: {Format()}"); - } - } - /// /// In the source array, pixels are laid out left to right, top to bottom, /// but in the returned array, left to right, top to bottom. @@ -395,57 +447,5 @@ private static void ReadSRGBAByteArray(IntPtr ptr, int width, int height, int wi } } } - - /// - /// In the source array, pixels are laid out left to right, top to bottom, - /// but in the returned array, left to right, top to bottom. - /// - private static void ReadChannel(IntPtr ptr, int channelNumber, int channelCount, int width, int height, int widthStep, bool flipVertically, byte[] colors) - { - if (colors.Length != width * height) - { - throw new ArgumentException("colors length is invalid"); - } - var padding = widthStep - (channelCount * width); - - unsafe - { - fixed (byte* dest = colors) - { - var pSrc = (byte*)ptr.ToPointer(); - pSrc += channelNumber; - - if (flipVertically) - { - var pDest = dest + colors.Length - 1; - - for (var i = 0; i < height; i++) - { - for (var j = 0; j < width; j++) - { - *pDest-- = *pSrc; - pSrc += channelCount; - } - pSrc += padding; - } - } - else - { - var pDest = dest + (width * (height - 1)); - - for (var i = 0; i < height; i++) - { - for (var j = 0; j < width; j++) - { - *pDest++ = *pSrc; - pSrc += channelCount; - } - pSrc += padding; - pDest -= 2 * width; - } - } - } - } - } } } diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/PInvoke/NativeMethods/Framework/Format/ImageFrame_Safe.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/PInvoke/NativeMethods/Framework/Format/ImageFrame_Safe.cs index 202454f1e..ff051301f 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/PInvoke/NativeMethods/Framework/Format/ImageFrame_Safe.cs +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/PInvoke/NativeMethods/Framework/Format/ImageFrame_Safe.cs @@ -33,27 +33,12 @@ public static extern MpReturnCode mp_ImageFrame__IsAligned__ui( [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] public static extern int mp_ImageFrame__Height(IntPtr imageFrame); - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] - public static extern MpReturnCode mp_ImageFrame__ChannelSize(IntPtr imageFrame, out int value); - - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] - public static extern MpReturnCode mp_ImageFrame__NumberOfChannels(IntPtr imageFrame, out int value); - - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] - public static extern MpReturnCode mp_ImageFrame__ByteDepth(IntPtr imageFrame, out int value); - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] public static extern int mp_ImageFrame__WidthStep(IntPtr imageFrame); [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] public static extern IntPtr mp_ImageFrame__MutablePixelData(IntPtr imageFrame); - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] - public static extern int mp_ImageFrame__PixelDataSize(IntPtr imageFrame); - - [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] - public static extern MpReturnCode mp_ImageFrame__PixelDataSizeStoredContiguously(IntPtr imageFrame, out int value); - #region StatusOr [Pure, DllImport(MediaPipeLibrary, ExactSpelling = true)] [return: MarshalAs(UnmanagedType.I1)] diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotation.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotation.cs index 7baa15b32..cececef97 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotation.cs +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotation.cs @@ -5,8 +5,9 @@ // https://opensource.org/licenses/MIT. using System; -using System.Linq; +using System.Runtime.InteropServices; using UnityEngine; +using UnityEngine.Rendering; using UnityEngine.UI; namespace Mediapipe.Unity @@ -18,121 +19,102 @@ namespace Mediapipe.Unity public class MaskAnnotation : HierarchicalAnnotation { [SerializeField] private RawImage _screen; + [SerializeField] private Shader _maskShader; + [SerializeField] private Texture2D _maskTexture; [SerializeField] private Color _color = Color.blue; + [SerializeField, Range(0, 1)] private float _minConfidence = 0.9f; - private Texture2D _texture; - private Color32[] _colors; + private Material _material; + private GraphicsBuffer _maskBuffer; - public void InitScreen() + private void OnEnable() { - var rect = GetAnnotationLayer().rect; - var width = (int)rect.width; - var height = (int)rect.height; - - var transparentColor = new Color32((byte)(255 * _color.r), (byte)(255 * _color.g), (byte)(255 * _color.b), 0); - _colors = Enumerable.Repeat(transparentColor, width * height).ToArray(); + ApplyMaskTexture(_maskTexture, _color); + ApplyMinConfidence(_minConfidence); + } - _texture = new Texture2D(width, height, TextureFormat.RGBA32, false); - _texture.SetPixels32(_colors); - _screen.texture = _texture; - _screen.color = new Color(1, 1, 1, 1); + private void OnDisable() + { + ApplyMinConfidence(1.1f); // larger than the maximum value (1.0). } - public void SetColor(Color color) + private void OnValidate() { - _color = color; - ApplyColor(_color); + ApplyMaskTexture(_maskTexture, _color); + ApplyMinConfidence(_minConfidence); } - public void Draw(byte[] mask, int width, int height, float minAlpha = 0.9f, float maxAlpha = 1.0f) + private void OnDestroy() { - if (mask.Length != width * height) + if (_maskBuffer != null) { - throw new ArgumentException("mask size must equal width * height"); + _maskBuffer.Release(); } + } - ResetAlpha(); - var alphaCoeff = Mathf.Clamp(maxAlpha, 0.0f, 1.0f); - var threshold = (byte)(255 * minAlpha); - var wInterval = (float)_texture.width / width; - var hInterval = (float)_texture.height / height; + public void InitScreen(int width, int height) + { + _screen.color = new Color(1, 1, 1, 1); - unsafe + _material = new Material(_maskShader) { - fixed (byte* ptr = mask) - { - var maskPtr = ptr; - - for (var i = 0; i < height; i++) - { - for (var j = 0; j < width; j++) - { - if (*maskPtr >= threshold) - { - var alpha = (byte)((*maskPtr) * alphaCoeff); - SetColorAlpha(GetNearestRange(j, wInterval, _texture.width), GetNearestRange(i, hInterval, _texture.height), alpha); - } - maskPtr++; - } - } - } - } - _texture.SetPixels32(_colors); - _texture.Apply(); + renderQueue = (int)RenderQueue.Transparent + }; + + _material.SetTexture("_MainTex", _screen.texture); + ApplyMaskTexture(_maskTexture, _color); + _material.SetInt("_Width", width); + _material.SetInt("_Height", height); + ApplyMinConfidence(_minConfidence); + InitMaskBuffer(width, height); + + _screen.material = _material; } - private void SetColorAlpha((int, int) xRange, (int, int) yRange, byte alpha) + public void Draw(float[] mask, int width, int height) { - unsafe + if (mask.Length != width * height) { - fixed (Color32* ptr = _colors) - { - var rowPtr = ptr + (yRange.Item1 * _texture.width); - - for (var i = yRange.Item1; i <= yRange.Item2; i++) - { - var colorPtr = rowPtr + xRange.Item1; - for (var j = xRange.Item1; j <= xRange.Item2; j++) - { - colorPtr++->a = alpha; - } - rowPtr += _texture.width; - } - } + throw new ArgumentException("mask size must equal width * height"); } + + _maskBuffer.SetData(mask); } - private (int, int) GetNearestRange(int p, float interval, int max) + private Texture2D CreateMonoColorTexture(Color color) { - var start = (int)Math.Ceiling((p - 0.5) * interval); - var end = (int)Math.Floor((p + 0.5f) * interval); + var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false); + var textureColor = new Color32((byte)(255 * color.r), (byte)(255 * color.g), (byte)(255 * color.b), 255); + texture.SetPixels32(new Color32[] { textureColor }); + texture.Apply(); - return (Mathf.Max(0, start), Mathf.Min(end, max - 1)); + return texture; } - private void ApplyColor(Color color) + private void InitMaskBuffer(int width, int height) { - if (_colors == null) { return; } - - var r = (byte)(255 * color.r); - var g = (byte)(255 * color.g); - var b = (byte)(255 * color.b); - - for (var i = 0; i < _colors.Length; i++) + if (_maskBuffer != null) { - _colors[i].r = r; - _colors[i].g = g; - _colors[i].b = b; + _maskBuffer.Release(); } + var stride = Marshal.SizeOf(typeof(float)); + _maskBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, width * height, stride); + _material.SetBuffer("_MaskBuffer", _maskBuffer); } - private void ResetAlpha() + private void ApplyMaskTexture(Texture maskTexture, Color color) { - if (_colors == null) { return; } + if (_material != null) + { + _material.SetTexture("_MaskTex", maskTexture == null ? CreateMonoColorTexture(color) : maskTexture); + } + } - for (var i = 0; i < _colors.Length; i++) + private void ApplyMinConfidence(float minConfidence) + { + if (_material != null) { - _colors[i].a = 0; + _material.SetFloat("_MinConfidence", minConfidence); } } } diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotationController.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotationController.cs index 61239c23e..941b00cc3 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotationController.cs +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Annotation/MaskAnnotationController.cs @@ -12,16 +12,14 @@ public class MaskAnnotationController : AnnotationController { [SerializeField] private int _maskWidth = 512; [SerializeField] private int _maskHeight = 512; - [SerializeField, Range(0, 1)] private float _minAlpha = 0.9f; - [SerializeField, Range(0, 1)] private float _maxAlpha = 1.0f; private ImageFrame _currentTarget; - private byte[] _maskArray; + private float[] _maskArray; public void InitScreen() { - _maskArray = new byte[_maskWidth * _maskHeight]; - annotation.InitScreen(); + _maskArray = new float[_maskWidth * _maskHeight]; + annotation.InitScreen(_maskWidth, _maskHeight); } public void DrawNow(ImageFrame target) @@ -41,14 +39,15 @@ private void UpdateMaskArray(ImageFrame imageFrame) { if (imageFrame != null) { - var _ = imageFrame.GetChannel(0, isMirrored, _maskArray); + // NOTE: assume that the image is transformed properly by calculators. + var _ = imageFrame.TryReadChannelNormalized(0, _maskArray); } } protected override void SyncNow() { isStale = false; - annotation.Draw(_maskArray, _maskWidth, _maskHeight, _minAlpha, _maxAlpha); + annotation.Draw(_maskArray, _maskWidth, _maskHeight); } } } diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs new file mode 100644 index 000000000..bf2e9f58a --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs @@ -0,0 +1,420 @@ +// Copyright (c) 2021 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using System; + +namespace Mediapipe.Unity +{ + public static class ImageFrameExtension + { + /// + /// Read the specific channel data only. + /// It's useful when only one channel is used (e.g. Hair Segmentation mask). + /// + /// + /// true if the channel data is read successfully; otherwise false. + /// + /// + /// Specify from which channel (0-indexed) the data will be retrieved. + /// For example, if the format is RGB, 0 means R channel, 1 means G channel, and 2 means B channel. + /// + /// + /// The array to which the output data will be written. + /// + /// + /// Set true if the is flipped horizontally. + /// + /// + /// Set true if the is flipped vertically. + /// + public static bool TryReadChannel(this ImageFrame imageFrame, int channelNumber, byte[] channelData, bool isHorizontallyFlipped = false, bool isVerticallyFlipped = false) + { + var format = imageFrame.Format(); + var channelCount = ImageFrame.NumberOfChannelsForFormat(format); + if (!IsChannelNumberValid(channelCount, channelNumber)) + { + return false; + } + +#pragma warning disable IDE0010 + switch (format) + { + case ImageFormat.Types.Format.Srgb: + case ImageFormat.Types.Format.Srgba: + case ImageFormat.Types.Format.Sbgra: + case ImageFormat.Types.Format.Gray8: + case ImageFormat.Types.Format.Lab8: + return TryReadChannel(imageFrame, channelCount, channelNumber, channelData, isHorizontallyFlipped, isVerticallyFlipped); + default: + Logger.LogWarning("The channel data is not stored in bytes"); + return false; + } +#pragma warning restore IDE0010 + } + + /// + /// Read the specific channel data only. + /// It's useful when only one channel is used (e.g. Hair Segmentation mask). + /// + /// + /// true if the channel data is read successfully; otherwise false. + /// + /// + /// Specify from which channel (0-indexed) the data will be retrieved. + /// For example, if the format is RGB, 0 means R channel, 1 means G channel, and 2 means B channel. + /// + /// + /// The array to which the output data will be written. + /// + /// + /// Set true if the is flipped horizontally. + /// + /// + /// Set true if the is flipped vertically. + /// + public static bool TryReadChannel(this ImageFrame imageFrame, int channelNumber, ushort[] channelData, bool isHorizontallyFlipped = false, bool isVerticallyFlipped = false) + { + var format = imageFrame.Format(); + var channelCount = ImageFrame.NumberOfChannelsForFormat(format); + if (!IsChannelNumberValid(channelCount, channelNumber)) + { + return false; + } + +#pragma warning disable IDE0010 + switch (format) + { + case ImageFormat.Types.Format.Srgb48: + case ImageFormat.Types.Format.Srgba64: + case ImageFormat.Types.Format.Gray16: + return TryReadChannel(imageFrame, channelCount, channelNumber, channelData, isHorizontallyFlipped, isVerticallyFlipped); + default: + Logger.LogWarning("The channel data is not stored in ushorts"); + return false; + } +#pragma warning restore IDE0010 + } + + /// + /// Read the specific channel data only. + /// It's useful when only one channel is used (e.g. Selfie Segmentation mask). + /// + /// + /// true if the channel data is read successfully; otherwise false. + /// + /// + /// Specify from which channel (0-indexed) the data will be retrieved. + /// + /// + /// The array to which the output data will be written. + /// + /// + /// Set true if the is flipped horizontally. + /// + /// + /// Set true if the is flipped vertically. + /// + public static bool TryReadChannel(this ImageFrame imageFrame, int channelNumber, float[] channelData, bool isHorizontallyFlipped = false, bool isVerticallyFlipped = false) + { + var format = imageFrame.Format(); + var channelCount = ImageFrame.NumberOfChannelsForFormat(format); + if (!IsChannelNumberValid(channelCount, channelNumber)) + { + return false; + } + +#pragma warning disable IDE0010 + switch (format) + { + case ImageFormat.Types.Format.Vec32F1: + case ImageFormat.Types.Format.Vec32F2: + return TryReadChannel(imageFrame, channelCount, channelNumber, channelData, isHorizontallyFlipped, isVerticallyFlipped); + default: + Logger.LogWarning("The channel data is not stored in floats"); + return false; + } +#pragma warning restore IDE0010 + } + + /// + /// Read the specific channel data only. + /// Each value in will be normalized to [0.0, 1.0]. + /// + /// + /// true if the channel data is read successfully; otherwise false. + /// + /// + /// Specify from which channel (0-indexed) the data will be retrieved. + /// + /// + /// The array to which the output data will be written. + /// + /// + /// Set true if the is flipped horizontally. + /// + /// + /// Set true if the is flipped vertically. + /// + public static bool TryReadChannelNormalized(this ImageFrame imageFrame, int channelNumber, float[] normalizedChannelData, bool isHorizontallyFlipped = false, bool isVerticallyFlipped = false) + { + var format = imageFrame.Format(); + var channelCount = ImageFrame.NumberOfChannelsForFormat(format); + if (!IsChannelNumberValid(channelCount, channelNumber)) + { + return false; + } + +#pragma warning disable IDE0010 + switch (format) + { + case ImageFormat.Types.Format.Srgb: + case ImageFormat.Types.Format.Srgba: + case ImageFormat.Types.Format.Sbgra: + case ImageFormat.Types.Format.Gray8: + case ImageFormat.Types.Format.Lab8: + return TryReadChannel(imageFrame, channelCount, channelNumber, v => (float)v / ((1 << 8) - 1), normalizedChannelData, isHorizontallyFlipped, isVerticallyFlipped); + case ImageFormat.Types.Format.Srgb48: + case ImageFormat.Types.Format.Srgba64: + case ImageFormat.Types.Format.Gray16: + return TryReadChannel(imageFrame, channelCount, channelNumber, v => (float)v / ((1 << 16) - 1), normalizedChannelData, isHorizontallyFlipped, isVerticallyFlipped); + case ImageFormat.Types.Format.Vec32F1: + case ImageFormat.Types.Format.Vec32F2: + return TryReadChannel(imageFrame, channelCount, channelNumber, normalizedChannelData, isHorizontallyFlipped, isVerticallyFlipped); + default: + Logger.LogWarning("Channels don't make sense in the current context"); + return false; + } +#pragma warning restore IDE0010 + } + + private static bool TryReadChannel(ImageFrame imageFrame, int channelCount, int channelNumber, T[] channelData, bool isHorizontallyFlipped, bool isVerticallyFlipped) where T : unmanaged + { + var width = imageFrame.Width(); + var height = imageFrame.Height(); + var length = width * height; + + if (channelData.Length != length) + { + Logger.LogWarning($"The length of channelData () does not equal {length}"); + return false; + } + + var widthStep = imageFrame.WidthStep(); + var byteDepth = imageFrame.ByteDepth(); + + unsafe + { + fixed (T* dest = channelData) + { + // NOTE: We cannot assume that the pixel data is aligned properly. + var pLine = (byte*)imageFrame.MutablePixelData(); + pLine += byteDepth * channelNumber; + + if (isVerticallyFlipped) + { + if (isHorizontallyFlipped) + { + // The first element is at bottom-right. + var pDest = dest + width - 1; + + for (var i = 0; i < height; i++) + { + var pSrc = (T*)pLine; + for (var j = 0; j < width; j++) + { + *pDest-- = *pSrc; + pSrc += channelCount; + } + pLine += widthStep; + pDest += 2 * width; + } + } + else + { + // The first element is at bottom-left. + // NOTE: In the Unity coordinate system, the image can be considered as not flipped. + var pDest = dest; + + for (var i = 0; i < height; i++) + { + var pSrc = (T*)pLine; + for (var j = 0; j < width; j++) + { + *pDest++ = *pSrc; + pSrc += channelCount; + } + pLine += widthStep; + } + } + } + else + { + if (isHorizontallyFlipped) + { + // The first element is at top-right. + var pDest = dest + length - 1; + + for (var i = 0; i < height; i++) + { + var pSrc = (T*)pLine; + for (var j = 0; j < width; j++) + { + *pDest-- = *pSrc; + pSrc += channelCount; + } + pLine += widthStep; + } + } + else + { + // The first element is at top-left (the image is not flipped at all). + var pDest = dest + (width * (height - 1)); + + for (var i = 0; i < height; i++) + { + var pSrc = (T*)pLine; + for (var j = 0; j < width; j++) + { + *pDest++ = *pSrc; + pSrc += channelCount; + } + pLine += widthStep; + pDest -= 2 * width; + } + } + } + } + } + + return true; + } + + private static bool TryReadChannel(ImageFrame imageFrame, int channelCount, int channelNumber, Func transformer, + TDst[] channelData, bool isHorizontallyFlipped, bool isVerticallyFlipped) where TSrc : unmanaged where TDst : unmanaged + { + var width = imageFrame.Width(); + var height = imageFrame.Height(); + var length = width * height; + + if (channelData.Length != length) + { + Logger.LogWarning($"The length of channelData () does not equal {length}"); + return false; + } + + var widthStep = imageFrame.WidthStep(); + var byteDepth = imageFrame.ByteDepth(); + + unsafe + { + fixed (TDst* dest = channelData) + { + // NOTE: We cannot assume that the pixel data is aligned properly. + var pLine = (byte*)imageFrame.MutablePixelData(); + pLine += byteDepth * channelNumber; + + if (isVerticallyFlipped) + { + if (isHorizontallyFlipped) + { + // The first element is at bottom-right. + var pDest = dest + width - 1; + + for (var i = 0; i < height; i++) + { + var pSrc = (TSrc*)pLine; + for (var j = 0; j < width; j++) + { + *pDest-- = transformer(*pSrc); + pSrc += channelCount; + } + pLine += widthStep; + pDest += 2 * width; + } + } + else + { + // The first element is at bottom-left. + // NOTE: In the Unity coordinate system, the image can be considered as not flipped. + var pDest = dest; + + for (var i = 0; i < height; i++) + { + var pSrc = (TSrc*)pLine; + for (var j = 0; j < width; j++) + { + *pDest++ = transformer(*pSrc); + pSrc += channelCount; + } + pLine += widthStep; + } + } + } + else + { + if (isHorizontallyFlipped) + { + // The first element is at top-right. + var pDest = dest + length - 1; + + for (var i = 0; i < height; i++) + { + var pSrc = (TSrc*)pLine; + for (var j = 0; j < width; j++) + { + *pDest-- = transformer(*pSrc); + pSrc += channelCount; + } + pLine += widthStep; + } + } + else + { + // The first element is at top-left (the image is not flipped at all). + var pDest = dest + (width * (height - 1)); + + for (var i = 0; i < height; i++) + { + var pSrc = (TSrc*)pLine; + for (var j = 0; j < width; j++) + { + *pDest++ = transformer(*pSrc); + pSrc += channelCount; + } + pLine += widthStep; + pDest -= 2 * width; + } + } + } + } + } + + return true; + } + + private static bool IsChannelNumberValid(int channelCount, int channelNumber) + { + if (channelNumber < 0) + { + Logger.LogWarning($"{channelNumber} must be >= 0"); + return false; + } + + if (channelCount == 0) + { + Logger.LogWarning("Channels don't make sense in the current context"); + return false; + } + + if (channelNumber >= channelCount) + { + Logger.LogWarning($"channelNumber must be <= {channelCount - 1} since there are only {channelCount} channels, but {channelNumber} is given"); + return false; + } + return true; + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs.meta b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs.meta new file mode 100644 index 000000000..524953389 --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Scripts/Unity/Extension/ImageFrameExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3d2ed92677011f5249a32983f023f216 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Shaders/Convergence Shader.shader b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/Convergence Shader.shader index 1171f0423..d9b0fc585 100644 --- a/Packages/com.github.homuler.mediapipe/Runtime/Shaders/Convergence Shader.shader +++ b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/Convergence Shader.shader @@ -1,4 +1,4 @@ -Shader "Unlit/Convergence Shader" +Shader "Unlit/MediaPipe/Convergence Shader" { Properties { diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader new file mode 100644 index 000000000..ad46ab16e --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader @@ -0,0 +1,75 @@ +Shader "Unlit/MediaPipe/Mask Shader" +{ + Properties + { + _MainTex ("Main Texture", 2D) = "" {} + _MaskTex ("Mask Texture", 2D) = "blue" {} + _Width ("Mask Width", Int) = 0 + _Height ("Mask Height", Int) = 0 + _MinConfidence ("Min Confidence", Range(0.0, 1.0)) = 0.9 + } + + SubShader + { + Tags { "RenderType"="Transparent" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + // make fog work + #pragma multi_compile_fog + + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + UNITY_FOG_COORDS(1) + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + UNITY_TRANSFER_FOG(o,o.vertex); + return o; + } + + sampler2D _MaskTex; + + int _Width; + int _Height; + float _MinConfidence; + uniform StructuredBuffer _MaskBuffer; + + fixed4 frag (v2f i) : SV_Target + { + // sample the texture + fixed4 mainCol = tex2D(_MainTex, i.uv); + fixed4 maskCol = tex2D(_MaskTex, i.uv); + int idx = int(i.uv.y * _Height) * _Width + int(i.uv.x * _Width); + float mask = _MaskBuffer[idx]; + fixed4 col = lerp(mainCol, lerp(mainCol, maskCol, mask), step(_MinConfidence, mask)); + + // apply fog + UNITY_APPLY_FOG(i.fogCoord, col); + return col; + } + ENDCG + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader.meta b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader.meta new file mode 100644 index 000000000..c9e3a9dad --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Runtime/Shaders/MaskShader.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 7adfa9ebe754989caa35dbb2c5526d1c +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Framework/Format/ImageFrameTest.cs b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Framework/Format/ImageFrameTest.cs index 156da7a06..bc45b22a0 100644 --- a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Framework/Format/ImageFrameTest.cs +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Framework/Format/ImageFrameTest.cs @@ -15,26 +15,24 @@ namespace Tests public class ImageFrameTest { #region Constructor - [Test, SignalAbort] + [Test] public void Ctor_ShouldInstantiateImageFrame_When_CalledWithNoArguments() { using (var imageFrame = new ImageFrame()) { -#pragma warning disable IDE0058 Assert.AreEqual(imageFrame.Format(), ImageFormat.Types.Format.Unknown); Assert.AreEqual(imageFrame.Width(), 0); Assert.AreEqual(imageFrame.Height(), 0); - Assert.Throws(() => { imageFrame.ChannelSize(); }); - Assert.Throws(() => { imageFrame.NumberOfChannels(); }); - Assert.Throws(() => { imageFrame.ByteDepth(); }); + Assert.AreEqual(imageFrame.ChannelSize(), 0); + Assert.AreEqual(imageFrame.NumberOfChannels(), 0); + Assert.AreEqual(imageFrame.ByteDepth(), 0); Assert.AreEqual(imageFrame.WidthStep(), 0); Assert.AreEqual(imageFrame.PixelDataSize(), 0); - Assert.Throws(() => { imageFrame.PixelDataSizeStoredContiguously(); }); + Assert.AreEqual(imageFrame.PixelDataSizeStoredContiguously(), 0); Assert.True(imageFrame.IsEmpty()); Assert.False(imageFrame.IsContiguous()); Assert.False(imageFrame.IsAligned(16)); Assert.AreEqual(imageFrame.MutablePixelData(), IntPtr.Zero); -#pragma warning restore IDE0058 } } diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension.meta b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension.meta new file mode 100644 index 000000000..e69c0776c --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65e6f6718f2bed55c97d7a509412a25f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs new file mode 100644 index 000000000..9bd21b5ef --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs @@ -0,0 +1,1091 @@ +// Copyright (c) 2021 homuler +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +using Mediapipe; +using Mediapipe.Unity; + +using NUnit.Framework; +using Unity.Collections; + +using System.Linq; + +namespace Tests +{ + public class ImageFrameExtensionTest + { + #region TryReadChannel(byte) + [Test] + public void TryReadChannelByte_ShouldReturnFalse_When_ChannelNumberIsNegative() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(-1, new byte[] { })); + } + } + + [Test] + public void TryReadChannelByte_ShouldReturnFalse_When_TheChannelDataIsNotStoredInBytes() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new byte[] { })); + } + } + + [Test] + public void TryReadChannelByte_ShouldReturnFalse_When_ChannelNumberEqualsTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(3, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(4, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(4, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(1, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(3, new byte[] { })); + } + } + + [Test] + public void TryReadChannelByte_ShouldReturnTrue_When_ChannelNumberIsLessThanTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(2, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(3, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(3, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(0, new byte[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(2, new byte[] { })); + } + } + + [Test] + public void TryReadChannelByte_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgb() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 16 - 3 * 3 = 7 + 1, 2, 3, 33, 34, 35, 65, 66, 67, 0, 0, 0, 0, 0, 0, 0, + 9, 10, 11, 41, 42, 43, 73, 74, 75, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new byte[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new byte[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new byte[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new byte[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new byte[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new byte[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new byte[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new byte[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new byte[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new byte[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new byte[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new byte[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new byte[] { 67, 35, 3, 75, 43, 11 }); + + } + } + + [Test] + public void TryReadChannelByte_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgba() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 16 - 3 * 4 = 4 + 1, 2, 3, 4, 33, 34, 35, 36, 65, 66, 67, 68, 0, 0, 0, 0, + 9, 10, 11, 12, 41, 42, 43, 44, 73, 74, 75, 76, 0, 0, 0, 0, + }; + var result = new byte[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new byte[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new byte[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new byte[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new byte[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new byte[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new byte[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new byte[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new byte[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new byte[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new byte[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new byte[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new byte[] { 67, 35, 3, 75, 43, 11 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, false)); + Assert.AreEqual(result, new byte[] { 12, 44, 76, 4, 36, 68 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, true)); + Assert.AreEqual(result, new byte[] { 4, 36, 68, 12, 44, 76 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, false)); + Assert.AreEqual(result, new byte[] { 76, 44, 12, 68, 36, 4 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, true)); + Assert.AreEqual(result, new byte[] { 68, 36, 4, 76, 44, 12 }); + } + } + + [Test] + public void TryReadChannelByte_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSbgra() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 16 - 4 * 3 = 4 + 1, 2, 3, 4, 33, 34, 35, 36, 65, 66, 67, 68, 0, 0, 0, 0, + 9, 10, 11, 12, 41, 42, 43, 44, 73, 74, 75, 76, 0, 0, 0, 0, + }; + var result = new byte[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new byte[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new byte[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new byte[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new byte[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new byte[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new byte[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new byte[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new byte[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new byte[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new byte[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new byte[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new byte[] { 67, 35, 3, 75, 43, 11 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, false)); + Assert.AreEqual(result, new byte[] { 12, 44, 76, 4, 36, 68 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, true)); + Assert.AreEqual(result, new byte[] { 4, 36, 68, 12, 44, 76 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, false)); + Assert.AreEqual(result, new byte[] { 76, 44, 12, 68, 36, 4 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, true)); + Assert.AreEqual(result, new byte[] { 68, 36, 4, 76, 44, 12 }); + } + } + + [Test] + public void TryReadChannelByte_ShouldReadTheSpecifiedChannelData_When_TheFormatIsGray8() + { + var bytes = new byte[] { + // padding is 16 - 3 * 1 = 13 + 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new byte[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new byte[] { 4, 5, 6, 1, 2, 3 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new byte[] { 1, 2, 3, 4, 5, 6 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new byte[] { 6, 5, 4, 3, 2, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new byte[] { 3, 2, 1, 6, 5, 4 }); + } + } + + [Test] + public void TryReadChannelByte_ShouldReadTheSpecifiedChannelData_When_TheFormatIsLab8() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 16 - 3 * 3 = 7 + 1, 2, 3, 33, 34, 35, 65, 66, 67, 0, 0, 0, 0, 0, 0, 0, + 9, 10, 11, 41, 42, 43, 73, 74, 75, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new byte[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new byte[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new byte[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new byte[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new byte[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new byte[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new byte[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new byte[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new byte[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new byte[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new byte[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new byte[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new byte[] { 67, 35, 3, 75, 43, 11 }); + } + } + #endregion + + #region TryReadChannel(ushort) + [Test] + public void TryReadChannelUshort_ShouldReturnFalse_When_ChannelNumberIsNegative() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(-1, new ushort[] { })); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReturnFalse_When_TheChannelDataIsNotStoredInUshorts() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new ushort[] { })); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReturnFalse_When_ChannelNumberEqualsTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(3, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(4, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(1, new ushort[] { })); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReturnTrue_When_ChannelNumberIsLessThanTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(2, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(3, new ushort[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(0, new ushort[] { })); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgb48() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 24 - 2 * 3 * 3 = 6 + 1, 0, 2, 0, 3, 0, 33, 0, 34, 0, 35, 0, 65, 0, 66, 0, 67, 0, 0, 0, 0, 0, 0, 0, + 9, 0, 10, 0, 11, 0, 41, 0, 42, 0, 43, 0, 73, 0, 74, 0, 75, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new ushort[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 3, 2, 24, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new ushort[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new ushort[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new ushort[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new ushort[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new ushort[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new ushort[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new ushort[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new ushort[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new ushort[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new ushort[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new ushort[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new ushort[] { 67, 35, 3, 75, 43, 11 }); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgba64() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 24 - 2 * 3 * 4 = 0 + 1, 0, 2, 0, 3, 0, 4, 0, 33, 0, 34, 0, 35, 0, 36, 0, 65, 0, 66, 0, 67, 0, 68, 0, + 9, 0, 10, 0, 11, 0, 12, 0, 41, 0, 42, 0, 43, 0, 44, 0, 73, 0, 74, 0, 75, 0, 76, 0, + }; + var result = new ushort[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 3, 2, 24, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new ushort[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new ushort[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new ushort[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new ushort[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new ushort[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new ushort[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new ushort[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new ushort[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, false)); + Assert.AreEqual(result, new ushort[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannel(2, result, false, true)); + Assert.AreEqual(result, new ushort[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, false)); + Assert.AreEqual(result, new ushort[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannel(2, result, true, true)); + Assert.AreEqual(result, new ushort[] { 67, 35, 3, 75, 43, 11 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, false)); + Assert.AreEqual(result, new ushort[] { 12, 44, 76, 4, 36, 68 }); + + Assert.True(imageFrame.TryReadChannel(3, result, false, true)); + Assert.AreEqual(result, new ushort[] { 4, 36, 68, 12, 44, 76 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, false)); + Assert.AreEqual(result, new ushort[] { 76, 44, 12, 68, 36, 4 }); + + Assert.True(imageFrame.TryReadChannel(3, result, true, true)); + Assert.AreEqual(result, new ushort[] { 68, 36, 4, 76, 44, 12 }); + } + } + + [Test] + public void TryReadChannelUshort_ShouldReadTheSpecifiedChannelData_When_TheFormatIsGray16() + { + var bytes = new byte[] { + // padding is 16 - 2 * 3 = 10 + 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 5, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new ushort[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new ushort[] { 4, 5, 6, 1, 2, 3 }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new ushort[] { 1, 2, 3, 4, 5, 6 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new ushort[] { 6, 5, 4, 3, 2, 1 }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new ushort[] { 3, 2, 1, 6, 5, 4 }); + } + } + #endregion + + #region TryReadChannel(float) + [Test] + public void TryReadChannelFloat_ShouldReturnFalse_When_ChannelNumberIsNegative() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(-1, new float[] { })); + } + } + + [Test] + public void TryReadChannelFloat_ShouldReturnFalse_When_TheChannelDataIsNotStoredInFloats() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(4, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(0, new float[] { })); + } + } + + [Test] + public void TryReadChannelFloat_ShouldReturnFalse_When_ChannelNumberEqualsTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(1, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannel(2, new float[] { })); + } + } + + [Test] + public void TryReadChannelFloat_ShouldReturnTrue_When_ChannelNumberIsLessThanTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannel(1, new float[] { })); + } + } + + [Test] + public void TryReadChannelFloat_ShouldReadTheSpecifiedChannelData_When_TheFormatIsVec32F1() + { + var floats = new float[] { + // padding is 16 - 3 * 4 = 4 + 1.0f, 2.0f, 3.0f, 0, + 4.0f, 5.0f, 6.0f, 0, + }; + var bytes = FloatsToBytes(floats); + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new float[] { 4.0f, 5.0f, 6.0f, 1.0f, 2.0f, 3.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new float[] { 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new float[] { 3.0f, 2.0f, 1.0f, 6.0f, 5.0f, 4.0f }); + } + } + + [Test] + public void TryReadChannelFloat_ShouldReadTheSpecifiedChannelData_When_TheFormatIsVec32F2() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var floats = new float[] { + // padding is 32 - 2 * 3 * 4 = 8 + 1.0f, 2.0f, 33.0f, 34.0f, 65.0f, 66.0f, 0, 0, + 9.0f, 10.0f, 41.0f, 42.0f, 73.0f, 74.0f, 0, 0, + }; + var bytes = FloatsToBytes(floats); + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 3, 2, 32, bytes)) + { + Assert.True(imageFrame.TryReadChannel(0, result, false, false)); + Assert.AreEqual(result, new float[] { 9.0f, 41.0f, 73.0f, 1.0f, 33.0f, 65.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, false, true)); + Assert.AreEqual(result, new float[] { 1.0f, 33.0f, 65.0f, 9.0f, 41.0f, 73.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, false)); + Assert.AreEqual(result, new float[] { 73.0f, 41.0f, 9.0f, 65.0f, 33.0f, 1.0f }); + + Assert.True(imageFrame.TryReadChannel(0, result, true, true)); + Assert.AreEqual(result, new float[] { 65.0f, 33.0f, 1.0f, 73.0f, 41.0f, 9.0f }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, false)); + Assert.AreEqual(result, new float[] { 10.0f, 42.0f, 74.0f, 2.0f, 34.0f, 66.0f }); + + Assert.True(imageFrame.TryReadChannel(1, result, false, true)); + Assert.AreEqual(result, new float[] { 2.0f, 34.0f, 66.0f, 10.0f, 42.0f, 74.0f }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, false)); + Assert.AreEqual(result, new float[] { 74.0f, 42.0f, 10.0f, 66.0f, 34.0f, 2.0f }); + + Assert.True(imageFrame.TryReadChannel(1, result, true, true)); + Assert.AreEqual(result, new float[] { 66.0f, 34.0f, 2.0f, 74.0f, 42.0f, 10.0f }); + } + } + #endregion + + #region TryReadChannelNormalized + [Test] + public void TryReadChannelNormalized_ShouldReturnFalse_When_ChannelNumberIsNegative() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(-1, new float[] { })); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReturnFalse_When_ChannelNumberEqualsTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(4, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(4, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(4, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(1, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(1, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(1, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(2, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.False(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReturnTrue_When_ChannelNumberIsLessThanTheNumberOfChannels() + { + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(2, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Sbgra, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgb48, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(2, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(3, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F1, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Vec32F2, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(1, new float[] { })); + } + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Lab8, 0, 0, 0, new byte[] { })) + { + Assert.True(imageFrame.TryReadChannelNormalized(2, new float[] { })); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgba() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 16 - 3 * 4 = 4 + 1, 2, 3, 4, 33, 34, 35, 36, 65, 66, 67, 68, 0, 0, 0, 0, + 9, 10, 11, 12, 41, 42, 43, 44, 73, 74, 75, 76, 0, 0, 0, 0, + }; + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, false)); + AssertNormalized(result, new byte[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, true)); + AssertNormalized(result, new byte[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, false)); + AssertNormalized(result, new byte[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, true)); + AssertNormalized(result, new byte[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, false, false)); + AssertNormalized(result, new byte[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, false, true)); + AssertNormalized(result, new byte[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, true, false)); + AssertNormalized(result, new byte[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, true, true)); + AssertNormalized(result, new byte[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, false, false)); + AssertNormalized(result, new byte[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, false, true)); + AssertNormalized(result, new byte[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, true, false)); + AssertNormalized(result, new byte[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, true, true)); + AssertNormalized(result, new byte[] { 67, 35, 3, 75, 43, 11 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, false, false)); + AssertNormalized(result, new byte[] { 12, 44, 76, 4, 36, 68 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, false, true)); + AssertNormalized(result, new byte[] { 4, 36, 68, 12, 44, 76 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, true, false)); + AssertNormalized(result, new byte[] { 76, 44, 12, 68, 36, 4 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, true, true)); + AssertNormalized(result, new byte[] { 68, 36, 4, 76, 44, 12 }); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReadTheSpecifiedChannelData_When_TheFormatIsGray8() + { + var bytes = new byte[] { + // padding is 16 - 3 * 1 = 13 + 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray8, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, false)); + AssertNormalized(result, new byte[] { 4, 5, 6, 1, 2, 3 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, true)); + AssertNormalized(result, new byte[] { 1, 2, 3, 4, 5, 6 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, false)); + AssertNormalized(result, new byte[] { 6, 5, 4, 3, 2, 1 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, true)); + AssertNormalized(result, new byte[] { 3, 2, 1, 6, 5, 4 }); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReadTheSpecifiedChannelData_When_TheFormatIsSrgba64() + { + // (w, h, c) -> w << 5 + h << 3 + (c + 1) + var bytes = new byte[] { + // padding is 24 - 2 * 3 * 4 = 0 + 1, 0, 2, 0, 3, 0, 4, 0, 33, 0, 34, 0, 35, 0, 36, 0, 65, 0, 66, 0, 67, 0, 68, 0, + 9, 0, 10, 0, 11, 0, 12, 0, 41, 0, 42, 0, 43, 0, 44, 0, 73, 0, 74, 0, 75, 0, 76, 0, + }; + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Srgba64, 3, 2, 24, bytes)) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, false)); + AssertNormalized(result, new ushort[] { 9, 41, 73, 1, 33, 65 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, true)); + AssertNormalized(result, new ushort[] { 1, 33, 65, 9, 41, 73 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, false)); + AssertNormalized(result, new ushort[] { 73, 41, 9, 65, 33, 1 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, true)); + AssertNormalized(result, new ushort[] { 65, 33, 1, 73, 41, 9 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, false, false)); + AssertNormalized(result, new ushort[] { 10, 42, 74, 2, 34, 66 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, false, true)); + AssertNormalized(result, new ushort[] { 2, 34, 66, 10, 42, 74 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, true, false)); + AssertNormalized(result, new ushort[] { 74, 42, 10, 66, 34, 2 }); + + Assert.True(imageFrame.TryReadChannelNormalized(1, result, true, true)); + AssertNormalized(result, new ushort[] { 66, 34, 2, 74, 42, 10 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, false, false)); + AssertNormalized(result, new ushort[] { 11, 43, 75, 3, 35, 67 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, false, true)); + AssertNormalized(result, new ushort[] { 3, 35, 67, 11, 43, 75 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, true, false)); + AssertNormalized(result, new ushort[] { 75, 43, 11, 67, 35, 3 }); + + Assert.True(imageFrame.TryReadChannelNormalized(2, result, true, true)); + AssertNormalized(result, new ushort[] { 67, 35, 3, 75, 43, 11 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, false, false)); + AssertNormalized(result, new ushort[] { 12, 44, 76, 4, 36, 68 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, false, true)); + AssertNormalized(result, new ushort[] { 4, 36, 68, 12, 44, 76 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, true, false)); + AssertNormalized(result, new ushort[] { 76, 44, 12, 68, 36, 4 }); + + Assert.True(imageFrame.TryReadChannelNormalized(3, result, true, true)); + AssertNormalized(result, new ushort[] { 68, 36, 4, 76, 44, 12 }); + } + } + + [Test] + public void TryReadChannelNormalized_ShouldReadTheSpecifiedChannelData_When_TheFormatIsGray16() + { + var bytes = new byte[] { + // padding is 16 - 2 * 3 = 10 + 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 5, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + var result = new float[6]; + + using (var imageFrame = BuildImageFrame(ImageFormat.Types.Format.Gray16, 3, 2, 16, bytes)) + { + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, false)); + AssertNormalized(result, new ushort[] { 4, 5, 6, 1, 2, 3 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, false, true)); + AssertNormalized(result, new ushort[] { 1, 2, 3, 4, 5, 6 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, false)); + AssertNormalized(result, new ushort[] { 6, 5, 4, 3, 2, 1 }); + + Assert.True(imageFrame.TryReadChannelNormalized(0, result, true, true)); + AssertNormalized(result, new ushort[] { 3, 2, 1, 6, 5, 4 }); + } + } + #endregion + + private ImageFrame BuildImageFrame(ImageFormat.Types.Format format, int width, int height, int widthStep, byte[] pixelData) + { + var array = new NativeArray(pixelData.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + array.CopyFrom(pixelData); + + return new ImageFrame(format, width, height, widthStep, array); + } + + private byte[] FloatsToBytes(float[] array) + { + var bytes = new byte[array.Length * 4]; + + unsafe + { + fixed (float* pArray = array) + { + var pByte = (byte*)pArray; + for (var i = 0; i < 4 * array.Length; i++) + { + bytes[i] = *pByte++; + } + } + } + return bytes; + } + + private void AssertNormalized(float[] result, byte[] expectedUnnormalized) + { + Assert.True(result.All((v) => v >= 0.0f && v <= 1.0f)); + AreAlmostEqual(result, expectedUnnormalized.Select((v) => (float)v / 255).ToArray(), 1e-6); + } + + private void AssertNormalized(float[] result, ushort[] expectedUnnormalized) + { + Assert.True(result.All((v) => v >= 0.0f && v <= 1.0f)); + AreAlmostEqual(result, expectedUnnormalized.Select((v) => (float)v / 65525).ToArray(), 1e-6); + } + + private void AreAlmostEqual(float[] xs, float[] ys, double threshold) + { + Assert.AreEqual(xs.Length, ys.Length); + Assert.True(xs.Zip(ys, (x, y) => x - y).All((diff) => UnityEngine.Mathf.Abs(diff) < threshold)); + } + } +} diff --git a/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs.meta b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs.meta new file mode 100644 index 000000000..b8a26e32b --- /dev/null +++ b/Packages/com.github.homuler.mediapipe/Tests/EditMode/Unity/Extension/ImageFrameExtensionTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46cfb5995c4d2b93899f4920ae0b016a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/mediapipe_api/framework/formats/image_frame.cc b/mediapipe_api/framework/formats/image_frame.cc index cbd42f7dc..07f54c0dd 100644 --- a/mediapipe_api/framework/formats/image_frame.cc +++ b/mediapipe_api/framework/formats/image_frame.cc @@ -62,40 +62,10 @@ int mp_ImageFrame__Width(mediapipe::ImageFrame* image_frame) { return image_fram int mp_ImageFrame__Height(mediapipe::ImageFrame* image_frame) { return image_frame->Height(); } -MpReturnCode mp_ImageFrame__ChannelSize(mediapipe::ImageFrame* image_frame, int* value_out) { - TRY_ALL - *value_out = image_frame->ChannelSize(); - RETURN_CODE(MpReturnCode::Success); - CATCH_ALL -} - -MpReturnCode mp_ImageFrame__NumberOfChannels(mediapipe::ImageFrame* image_frame, int* value_out) { - TRY_ALL - *value_out = image_frame->NumberOfChannels(); - RETURN_CODE(MpReturnCode::Success); - CATCH_ALL -} - -MpReturnCode mp_ImageFrame__ByteDepth(mediapipe::ImageFrame* image_frame, int* value_out) { - TRY_ALL - *value_out = image_frame->ByteDepth(); - RETURN_CODE(MpReturnCode::Success); - CATCH_ALL -} - int mp_ImageFrame__WidthStep(mediapipe::ImageFrame* image_frame) { return image_frame->WidthStep(); } uint8* mp_ImageFrame__MutablePixelData(mediapipe::ImageFrame* image_frame) { return image_frame->MutablePixelData(); } -int mp_ImageFrame__PixelDataSize(mediapipe::ImageFrame* image_frame) { return image_frame->PixelDataSize(); } - -MpReturnCode mp_ImageFrame__PixelDataSizeStoredContiguously(mediapipe::ImageFrame* image_frame, int* value_out) { - TRY_ALL - *value_out = image_frame->PixelDataSizeStoredContiguously(); - RETURN_CODE(MpReturnCode::Success); - CATCH_ALL -} - MpReturnCode mp_ImageFrame__CopyToBuffer__Pui8_i(mediapipe::ImageFrame* image_frame, uint8* buffer, int buffer_size) { TRY_ALL image_frame->CopyToBuffer(buffer, buffer_size); diff --git a/mediapipe_api/framework/formats/image_frame.h b/mediapipe_api/framework/formats/image_frame.h index 4d0b1ed62..26fa62520 100644 --- a/mediapipe_api/framework/formats/image_frame.h +++ b/mediapipe_api/framework/formats/image_frame.h @@ -35,13 +35,8 @@ MP_CAPI(MpReturnCode) mp_ImageFrame__IsAligned__ui(mediapipe::ImageFrame* image_ MP_CAPI(mediapipe::ImageFormat::Format) mp_ImageFrame__Format(mediapipe::ImageFrame* image_frame); MP_CAPI(int) mp_ImageFrame__Width(mediapipe::ImageFrame* image_frame); MP_CAPI(int) mp_ImageFrame__Height(mediapipe::ImageFrame* image_frame); -MP_CAPI(MpReturnCode) mp_ImageFrame__ChannelSize(mediapipe::ImageFrame* image_frame, int* value_out); -MP_CAPI(MpReturnCode) mp_ImageFrame__NumberOfChannels(mediapipe::ImageFrame* image_frame, int* value_out); -MP_CAPI(MpReturnCode) mp_ImageFrame__ByteDepth(mediapipe::ImageFrame* image_frame, int* value_out); MP_CAPI(int) mp_ImageFrame__WidthStep(mediapipe::ImageFrame* image_frame); MP_CAPI(uint8*) mp_ImageFrame__MutablePixelData(mediapipe::ImageFrame* image_frame); -MP_CAPI(int) mp_ImageFrame__PixelDataSize(mediapipe::ImageFrame* image_frame); -MP_CAPI(MpReturnCode) mp_ImageFrame__PixelDataSizeStoredContiguously(mediapipe::ImageFrame* image_frame, int* value_out); MP_CAPI(MpReturnCode) mp_ImageFrame__CopyToBuffer__Pui8_i(mediapipe::ImageFrame* image_frame, uint8* buffer, int buffer_size); MP_CAPI(MpReturnCode) mp_ImageFrame__CopyToBuffer__Pui16_i(mediapipe::ImageFrame* image_frame, uint16* buffer, int buffer_size); MP_CAPI(MpReturnCode) mp_ImageFrame__CopyToBuffer__Pf_i(mediapipe::ImageFrame* image_frame, float* buffer, int buffer_size);