Skip to content

Issue with Texture Format Conversion and Accessing GPU Data in Unity

Problem to solve

Texture Format Conversion and Accessing GPU Data in Unity

Intended users

For those who want to use VLC and add OpenCV processing

Proposal (Outline the problem you want to solve)

I'm encountering an issue when trying to perform face detection in Unity using the MinimalPlayback code provided below. The error message I receive is: Graphics.CopyTexture can only copy between same texture format groups (d3d11 base formats: src=0 dst=9). This error occurs at the line Graphics.CopyTexture(cpuAccessibleTexture, tex);. image Interestingly, when I check the format of the textures immediately before this line, both tex and cpuAccessibleTexture appear to have the same format: RGB24.

MinimalPlayback code goes here


using UnityEngine;
using System;
using LibVLCSharp;
using OpenCVForUnity;
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.UnityUtils;
using OpenCVForUnityExample;
using DlibFaceLandmarkDetector;
using DlibFaceLandmarkDetectorExample;
using DlibFaceLandmarkDetector.UnityUtils;
using OpenCVForUnity.UnityUtils.Helper;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;


/// this class serves as an example on how to configure playback in Unity with VLC for Unity using LibVLCSharp.
/// for libvlcsharp usage documentation, please visit https://code.videolan.org/videolan/LibVLCSharp/-/blob/master/docs/home.md
public class MinimalPlayback : MonoBehaviour
{
    LibVLC _libVLC;
    MediaPlayer _mediaPlayer;
    const int seekTimeDelta = 5000;
    Texture2D tex = null;
    bool playing;
    public string URL;
    //def : http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
    FaceLandmarkDetector faceLandmarkDetector;
    string dlibShapePredictorFileName = "DlibFaceLandmarkDetector/sp_human_face_68.dat";
    string dlibShapePredictorFilePath;
    //public TextureProcessingExample textureProcessor;
    Texture2D cpuAccessibleTexture = null;
    RenderTexture renderTexture = null;


    void Awake()
    {
        LibVLCSharp.Core.Initialize(Application.dataPath);

        _libVLC = new LibVLC("--no-osd", "--verbose=2");
        dlibShapePredictorFilePath = DlibFaceLandmarkDetector.UnityUtils.Utils.getFilePath(dlibShapePredictorFileName);
        faceLandmarkDetector = new FaceLandmarkDetector(dlibShapePredictorFilePath);
        Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
        //_libVLC.Log += (s, e) => UnityEngine.Debug.Log(e.FormattedLog); // enable this for logs in the editor

        PlayPause();
    }
    public void SeekForward()
    {
        Debug.Log("[VLC] Seeking forward !");
        _mediaPlayer.SetTime(_mediaPlayer.Time + seekTimeDelta);
    }

    public void SeekBackward()
    {
        Debug.Log("[VLC] Seeking backward !");
        _mediaPlayer.SetTime(_mediaPlayer.Time - seekTimeDelta);
    }

    void OnDisable()
    {
        _mediaPlayer?.Stop();
        _mediaPlayer?.Dispose();
        _mediaPlayer = null;

        _libVLC?.Dispose();
        _libVLC = null;
    }

    public void PlayPause()
    {
        Debug.Log("[VLC] Toggling Play Pause !");
        if (_mediaPlayer == null)
        {
            _mediaPlayer = new MediaPlayer(_libVLC);
        }
        if (_mediaPlayer.IsPlaying)
        {
            _mediaPlayer.Pause();
        }
        else
        {
            playing = true;

            if (_mediaPlayer.Media == null)
            {
                // playing remote media
                _mediaPlayer.Media = new Media(_libVLC, new Uri(URL));
            }

            _mediaPlayer.Play();
        }
    }

    public void Stop()
    {
        Debug.Log("[VLC] Stopping Player !");

        playing = false;
        _mediaPlayer?.Stop();

        // there is no need to dispose every time you stop, but you should do so when you're done using the mediaplayer and this is how:
        // _mediaPlayer?.Dispose(); 
        // _mediaPlayer = null;
        GetComponent<Renderer>().material.mainTexture = null;
        tex = null;
    }

    void OnDestroy()
    {
        if (renderTexture != null)
        {
            renderTexture.Release();
        }
    }
    void Update()
    {
        if (!playing) return;

        if (tex == null)
        {
            // If received size is not null, it and scale the texture
            uint i_videoHeight = 0;
            uint i_videoWidth = 0;

            _mediaPlayer.Size(0, ref i_videoWidth, ref i_videoHeight);
            var texptr = _mediaPlayer.GetTexture(out bool updated);
            if (i_videoWidth != 0 && i_videoHeight != 0 && updated && texptr != IntPtr.Zero)
            {
                Debug.Log("Creating texture with height " + i_videoHeight + " and width " + i_videoWidth);
                tex = Texture2D.CreateExternalTexture((int)i_videoWidth,
                    (int)i_videoHeight,
                    TextureFormat.RGB24,
                    false,
                    true,
                    texptr);
                // RenderTextureの作成
                renderTexture = new RenderTexture(tex.width, tex.height, 24, RenderTextureFormat.ARGB32);

                // CPUアクセス可能なTexture2Dの作成
                cpuAccessibleTexture = new Texture2D(tex.width, tex.height, TextureFormat.RGB24, false);
                Debug.Log("cpuAccessibleTexture format1: " + cpuAccessibleTexture.format);

                GetComponent<Renderer>().material.mainTexture = tex;
            }
        }
        else if (tex != null)
        {
            var texptr = _mediaPlayer.GetTexture(out bool updated);
            if (updated)
            {
                tex.UpdateExternalTexture(texptr);
                // Blit the pixels on texture to the RenderTexture
                Graphics.Blit(tex, renderTexture);
                Debug.Log("cpuAccessibleTexture format2: " + cpuAccessibleTexture.format);

                // Set the current RenderTexture to be active
                RenderTexture.active = renderTexture;

                // Read the active RenderTexture into the existing Texture2D
                cpuAccessibleTexture.ReadPixels(new UnityEngine.Rect(0, 0, tex.width, tex.height), 0, 0);
                cpuAccessibleTexture.Apply();
                Debug.Log("cpuAccessibleTexture format3: " + cpuAccessibleTexture.format);

                // Texture2Dからピクセルデータを取得します
                Color32[] pixels = cpuAccessibleTexture.GetPixels32();

                // Color32配列をbyte配列に変換します
                GCHandle pinPixels = GCHandle.Alloc(pixels, GCHandleType.Pinned);
                IntPtr ptrPixels = pinPixels.AddrOfPinnedObject();

                // byte配列からMatを作成します
                Mat rgbaMat = new Mat(tex.height, tex.width, CvType.CV_8UC3, ptrPixels);
                Debug.Log("Success Mat conversion");
                // GCHandleを解放します
                pinPixels.Free();

                // Clean up RenderTexture
                RenderTexture.active = null;
                if (renderTexture != null)
                {
                    renderTexture.Release();
                }
                // Create a new Mat object
                //Mat rgbaMat = new Mat(tex.height, tex.width, CvType.CV_8UC4);

                // Convert the Texture2D to Mat ここでクラッシュ
                //OpenCVForUnity.UnityUtils.Utils.texture2DToMat(tex, rgbaMat, true);

                // ... (Your OpenCV code here)
                OpenCVForUnityUtils.SetImage(faceLandmarkDetector, rgbaMat);
                Debug.Log("faceLandmarkDetector");
                //detect face rects
                List<UnityEngine.Rect> detectResult = faceLandmarkDetector.Detect();

                foreach (var rect in detectResult)
                {
                    //detect landmark points
                    List<Vector2> points = faceLandmarkDetector.DetectLandmark(rect);

                    //draw landmark points
                    OpenCVForUnityUtils.DrawFaceLandmark(rgbaMat, points, new Scalar(0, 255, 0), 2);

                    //draw face rect
                    OpenCVForUnityUtils.DrawFaceRect(rgbaMat, rect, new Scalar(255, 0, 0), 2);
                }

                // Convert the processed Mat back to Texture2D
                OpenCVForUnity.UnityUtils.Utils.matToTexture2D(rgbaMat, cpuAccessibleTexture);
                Debug.Log("Success matToTexture2D(rgbaMat, cpuAccessibleTexture);");
                //OpenCVForUnity.UnityUtils.Utils.matToTexture2D(rgbaMat, tex);
                Debug.Log("tex format: " + tex.format);
                Debug.Log("cpuAccessibleTexture format4: " + cpuAccessibleTexture.format);

                Graphics.CopyTexture(cpuAccessibleTexture, tex);
                RenderTexture.active = null;

            }
        }
    }
}

I would like to avoid data exchange with the GPU if possible, as it tends to degrade performance. However, when I attempt to retrieve a texture from an external video source (such as RTSP streaming) using the code provided below, I am unable to access the texture data. It seems clear that Texture2D.CreateExternalTexture is the appropriate method for retrieving a texture from an external video source.

void Update()
    {
        if(!playing) return;

        if (tex == null)
        {
            // If received size is not null, it and scale the texture
            uint i_videoHeight = 0;
            uint i_videoWidth = 0;

            _mediaPlayer.Size(0, ref i_videoWidth, ref i_videoHeight);
            var texptr = _mediaPlayer.GetTexture(out bool updated);
            if (i_videoWidth != 0 && i_videoHeight != 0 && updated && texptr != IntPtr.Zero)
            {
                Debug.Log("Creating texture with height " + i_videoHeight + " and width " + i_videoWidth);

                tex = new Texture2D((int)i_videoWidth, (int)i_videoHeight, TextureFormat.RGB24, false);
        
                GetComponent<Renderer>().material.mainTexture = tex;
            }
        }
        else if (tex != null)
        {
            var texptr = _mediaPlayer.GetTexture(out bool updated);
            if (updated)
            {
                //tex.UpdateExternalTexture(texptr);
                tex.UpdateExternalTexture(texptr);
                
                // Create a new Mat object
                
                Mat rgbaMat = new Mat(tex.height, tex.width, CvType.CV_8UC4);
                
                // Convert the Texture2D to Mat ここでクラッシュ
                OpenCVForUnity.UnityUtils.Utils.texture2DToMat(tex, rgbaMat, true);

                OpenCVForUnityUtils.SetImage(faceLandmarkDetector, rgbaMat);
                Debug.Log("faceLandmarkDetector");
                //detect face rects
                List<UnityEngine.Rect> detectResult = faceLandmarkDetector.Detect();

                foreach (var rect in detectResult)
                {
                //detect landmark points
                List<Vector2> points = faceLandmarkDetector.DetectLandmark(rect);

                //draw landmark points
                OpenCVForUnityUtils.DrawFaceLandmark(rgbaMat, points, new Scalar(0, 255, 0), 2);

                //draw face rect
                OpenCVForUnityUtils.DrawFaceRect(rgbaMat, rect, new Scalar(255, 0, 0), 2);
                }

                // Convert the processed Mat back to Texture2D
                OpenCVForUnity.UnityUtils.Utils.matToTexture2D(rgbaMat, tex);
                Debug.Log("Success matToTexture2D(rgbaMat, cpuAccessibleTexture);");
                


            }
        }
    }

Documentation

Ultimately, I want to use Texture2D.CreateExternalTexture to convert GPU data to a Mat object. However, I can't retrieve RTSP video using a CPU-accessible new Texture2D, and I can't access the data using Texture2D.CreateExternalTexture. I'm wondering if it would be possible to achieve this using a programming language like CUDA.

Additionally, I noticed that ffmpeg provides a helper class for retrieving and converting a texture from an external RTSP source to a Mat object. However, this method introduces significant latency, so I decided to use VLC instead. I haven't been able to find a similar helper class for VLC. Is there a better way to accomplish this?

Version

  • Windows 11
  • Unity 2022.3.1f1
  • OpenCV for Unity 2.5.4
  • vlc-unity-windows-trial