Search code examples
c#unity-game-enginetextures

Save a RawImage to file


So I set up a RawImage that captures the image from a VR headset's pass-through camera.

Now that the picture is correctly displayed, I'd like to save it ...

public class CameraCaptureBehaviour : MonoBehaviour
{
    private const string Tag = "[CAMERA_CAPTURE]";
    private const int CaptureFrequency = 500;
    [field:SerializeField] private RawImage CameraImage { get; set; }

    private int count = 0;

    private void Update()
    {
        if (++count % CaptureFrequency != 0) return;
        AndroidHelper.LogCat("Trying to get image", Tag);
        try
        {
            SaveImage();
        }
        catch (Exception e)
        {
            AndroidHelper.LogCat(e.Message, Tag);
        }
    }

    private void SaveImage()
    {
        var texture = CameraImage.texture;
        if (texture == null)
        {
            AndroidHelper.LogCat("No camera image found.", Tag);
            return;
        }

        var texture2d = (Texture2D) texture;
        var imageBytes = texture2d.EncodeToJPG();
        
        AndroidHelper.LogCat($"imageBytes is null: {imageBytes == null}", Tag);
        if (imageBytes == null) return;
        
        var filename = DateTime.Now.ToString("yyyyMMddHHmmssffff") + ".jpg";
        var filepath = Path.Combine(Application.persistentDataPath, filename);
        AndroidHelper.LogCat($"{imageBytes.Length} to {filepath}", Tag);
        try
        {
            File.WriteAllBytes(filepath, imageBytes);
            AndroidHelper.LogCat("SAVED", Tag);
        }
        catch (Exception e)
        {
            AndroidHelper.LogCat(e.Message, Tag);
        }
    }
}

which gives me

[CAMERA_CAPTURE] Trying to get image
[CAMERA_CAPTURE] imageBytes is null: False
[CAMERA_CAPTURE] 0 to /storage/emulated/0/Android/data/my.app/files/202203290841482532.png
[CAMERA_CAPTURE] SAVED

I have no idea why the image should be empty.

Let's try the Color32 thing suggested by Convert Color32 array to byte array to send over network :

public class CameraCaptureBehaviour : MonoBehaviour
{
    private const string Tag = "[CAMERA_CAPTURE]";
    private const int CaptureFrequency = 500;
    [field:SerializeField] private RawImage CameraImage { get; set; }

    private int count = 0;

    private void Update()
    {
        if (++count % CaptureFrequency != 0) return;
        AndroidHelper.LogCat("Trying to get image", Tag);
        try
        {
            SaveImage();
        }
        catch (Exception e)
        {
            AndroidHelper.LogCat(e.Message, Tag);
        }
    }

    private void SaveImage()
    {
        var texture = CameraImage.texture;
        if (texture == null)
        {
            AndroidHelper.LogCat("No camera image found.", Tag);
            return;
        }

        var texture2d = (Texture2D) texture;

        var color32 = new Color32Array();
        color32.colors = texture2d.GetPixels32();
        var imageBytes = color32.byteArray;
        
        AndroidHelper.LogCat($"imageBytes is null: {imageBytes == null}", Tag);
        if (imageBytes == null) return;
        
        var filename = DateTime.Now.ToString("yyyyMMddHHmmssffff") + ".jpg";
        var filepath = Path.Combine(Application.persistentDataPath, filename);
        AndroidHelper.LogCat($"{imageBytes.Length} to {filepath}", Tag);
        try
        {
            File.WriteAllBytes(filepath, imageBytes);
            AndroidHelper.LogCat("SAVED", Tag);
        }
        catch (Exception e)
        {
            AndroidHelper.LogCat(e.Message, Tag);
        }
    }
}

[StructLayout(LayoutKind.Explicit)]
public struct Color32Array
{
    [FieldOffset(0)]
    public byte[] byteArray;

    [FieldOffset(0)]
    public Color32[] colors;
}

which gives me

[CAMERA_CAPTURE] Trying to get image
[CAMERA_CAPTURE] Texture '' is not configured correctly to allow GetPixels

I've also found https://answers.unity.com/questions/1503328/save-rawimage-texture-to-file.html which - like all questions that sound exactly what you need but were asked several years ago - is completely devoid of useful information.

I just want to save the bloody picture I'm looking at in the goggles, that can't be so darn difficult?!

Some help would be appreciated.

How do I save a RawImage?

(Note: texture.GetType().FullName gives me UnityEngine.Texture2D)

(Note: I checked the texture size after I retrieved it, and it's 600x600 so the picture isn't just 0 pixels large)


Solution

  • Blit your image to a RenderTexture, then use that to create a new Texture2D

    private static Texture2D GetReadableTexture2d(Texture texture)
    {
        var tmp = RenderTexture.GetTemporary(
            texture.width,
            texture.height,
            0,
            RenderTextureFormat.Default,
            RenderTextureReadWrite.Linear
        );
        Graphics.Blit(texture, tmp);
    
        var previousRenderTexture = RenderTexture.active;
        RenderTexture.active = tmp;
    
        var texture2d = new Texture2D(texture.width, texture.height);
        texture2d.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
        texture2d.Apply();
    
        RenderTexture.active = previousRenderTexture;
        RenderTexture.ReleaseTemporary(tmp);
        return texture2d;
    }
    

    The resulting Texture2D can then be EncodeToPNG'd and saved:

    public void SaveImage()
    {
        SaveImage(GetReadableTexture2d(CameraImage.texture));
    }
    
    private static void SaveImage(Texture2D texture)
    {
        var imageBytes = texture.EncodeToPNG();
    
        if (imageBytes.Length == 0)
        {
            AndroidHelper.LogCat($"Trying to save empty picture. Abort.", Tag);
            return;
        }
    
        var filename = DateTime.Now.ToString("yyyyMMddHHmmssffff") + ".png";
        var filepath = Path.Combine(Application.persistentDataPath, filename);
        AndroidHelper.LogCat($"{imageBytes.Length} to {filepath}", Tag);
        try
        {
            File.WriteAllBytes(filepath, imageBytes);
            AndroidHelper.LogCat("SAVED", Tag);
        }
        catch (Exception e)
        {
            AndroidHelper.LogCat(e.Message, Tag);
        }
    }
    

    Not sure if there's an easier way, but this one seems to work, at least.