1

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)

User1291
  • 7,664
  • 8
  • 51
  • 108

3 Answers3

1

You can call texture2d.EncodeToJPG to a texture2D, you got that right. You can NOT however just cast any texture into a texture2D. Well you can, but things won't necessarily work out, as you figured out your self ;)

To convert a RENDERtexture into a Texture2D, you can do the following:

RenderTexture.active = texture;
Texture2D texture2d = new Texture2D(texture.width, texture.height);
texture2d.ReadPixels(new Rect(0, 0, texture.width, texture.height),0, 0);
texture2d.Apply();
RenderTexture.active = null;

If this doesn't work, you can try Graphics.CopyTexture() as derHugo pointed out, or google for the way to convert the specific Texture type you have into a Texture2D

Daveloper
  • 748
  • 4
  • 13
  • Hm ... your approach saves a black image with a few colour pixels across it. Which is still better than the `CopyTexture` which saves just a grey image... – User1291 Mar 29 '22 at 08:52
  • you have to wait for next frame `'yield new waitfornextframe()` – ina Jul 27 '22 at 07:30
0

I don't think you can simply cast the RawImage.texture to a Texture2D. Try to rather actually copy it over like e.g.

var texture2d = new Texture2D(texture.width, texture.hight);
Graphics.CopyTexture(texture, texture2d);    

see Graphics.CopyTexture

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • I'm afraid `CopyTexture` just ends up saving a 6kb completely grey picture. – User1291 Mar 29 '22 at 08:53
  • @User1291 note that the texture format needs to match .. if you e.g. use RGBA in your raw image texture then you also need to additionally pass that format into the texture2D constructor – derHugo Mar 29 '22 at 10:16
  • Is CopyTexture just doing the blit? – ina Jul 27 '22 at 07:32
  • @ina it is similar. `Blit` can only render into a `RenderTexture` .. here you want the result to be a `Texture2D` so you can get the pixels out of it – derHugo Jul 27 '22 at 07:48
0

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.

User1291
  • 7,664
  • 8
  • 51
  • 108