记个笔记,如果大佬有更好的方式可以指出,谢谢
先说下我因为一个VR项目需要保存设备头盔上的灰度摄像头的画面,因为显示是设备的SDK中提供的,显示出来的是正常的灰度图,就想当然的通过常规的方法保存图片。结果发现保存下来的JPG图是纯黑的图,保存的PNG和TGA都是大部分透明的图(现在写笔记没有设备所以就附上图片了,后续有空再补上吧)后面我先说下原因,最后会放上我的解决方案和代码。
接下来说下我碰到的是什么原因造成这样的,原因是设备安卓层传上来的画面数据是Alpha8的,通过Texture2D加载后再unity中显示会正常显示灰度画面,但是实际上每一帧图片的每个像素点的RGB都是(1,1,1)或者(0,0,0),只有每一个像素点的Alpha值是不一样的,因此保存JPG格式时因为没有透明通道所以不保存alpha值,所以显示的是全黑的,当保存有透明通道的PNG或者TGA时就变成都是透明图了。
解决方案很简单,但是需要了解灰度图是怎么产生的,这边简单说下,普通的图片基本都是RGB(红绿蓝)三基色组成或者RGBA(红绿蓝透明值)三基色加一个透明值组成,灰度图其实就是让RGB三种颜色的数值相等就行,所以常见的生成灰度图有三种方法:1、最大值法,2、平均值法,3、加权平均值法,这里就不扩展了讲了,感兴趣的可以自己查查这资料还是很多的。
前面说了这么多其实就是让我们生成一张我们能保存的灰度图就行了,把从安卓层传上来的Alpha值赋给RGB三个颜色,Alpha值等于1就能生成一张我们引擎中看到的灰度图了。下面直接说我的做法。
第一种做法,保存时修改图片的每一个像素点数据,再进行保存,代码如下,但是这个做法如果图片分辨率太高会卡死主线程,建议下面协程、异步或者多线程处理。(这种做法很消耗时间,不建议使用)
IEnumerator SaveImage(Texture2D texture2D)
{
// 因为"WaitForEndOfFrame"在OnGUI之后执行
// 所以我们只在渲染完成之后才读取屏幕上的画面
yield return new WaitForEndOfFrame();
// 创建一个屏幕大小的纹理,RGB24 位格(24位格没有透明通道,32位的有)
Texture2D jpge = new Texture2D(texture2D.width, texture2D.height, TextureFormat.ARGB32, false);
for (int i = 0; i < texture2D.height; i++)
{
for (int j = 0; j < texture2D.width; j++)
{
Color newColor = new Color();
newColor.r = newColor.g = newColor.b = texture2D.GetPixel(i, j).a;
newColor.a = 1;
jpge.SetPixel(i,j, newColor);
}
//因为处理每个像素点,数据太多在处理完前会一直在这两个循环内,因此为了不卡死程序需要等一帧再继续操作(甚至在里面那个循环中也可以加,或者异步或者多线程处理)
yield return new WaitForEndOfFrame();
}
// 保存前面对纹理的修改
jpge.Apply();
//拿去PNG的图片字节数组
byte[] bytes = jpge.EncodeToPNG();
//自己分装的导出文件方法
FilesTool.ExportFile(bytes, Application.persistentDataPath, "test.png");
}
第二种是通过Shader叠加来生成一张想要的灰度图,方法也很简单,拿到图片数据后通过Graphics.Blit(pngTexture, GrayMaterial);方法叠加一个材质球再保存图片即可,下面是代码
IEnumerator SaveImage(Texture texture)
{
// 因为"WaitForEndOfFrame"在OnGUI之后执行
// 所以我们只在渲染完成之后才读取屏幕上的画面
yield return new WaitForEndOfFrame();
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
RenderTexture currentRT = RenderTexture.active;
RenderTexture renderTexture = RenderTexture.GetTemporary(texture.width, texture.height, 32);
Graphics.Blit(texture, renderTexture, testimage.material);
RenderTexture.active = renderTexture;
texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
texture2D.Apply();
RenderTexture.active = currentRT;
RenderTexture.ReleaseTemporary(renderTexture);
yield return texture2d;
byte[] bytes = pngTexture.EncodeToPNG();
FilesTool.ExportFile(bytes, Application.persistentDataPath, "test.png");
}
下面是shader的代码,其实和灰度图shader一样,只不过在处理rgba数值时修改一下就行。
Shader "Unlit/GrayShader"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
灰度图的处理
// float gray = dot(col.rgb, float3(0.299, 0.587, 0.114));
// col.r = gray;
// col.g = gray;
// col.b = gray;
// col.a = 1;
//根据我们需求把alpha赋给rgb然后alpha等于1既可
float gray = col.a;
col.r = gray;
col.g = gray;
col.b = gray;
col.a = 1;
return col;
}
ENDCG
}
}
}
本篇内容到这里就结束了,如果有大佬有更好的方法欢迎评论私信提供。谢谢
最后放一个我封装好的导出文件的方法,也是前面保存图片时用到的ExportFile方法
/// <summary>
/// 创建文件
/// </summary>
/// <param name="FilePath">文件路径</param>
/// <param name="IfHaveFileIsCreate">当文件存在时是否重新创建</param>
/// <returns>返回bool,true为创建成功,false没有创建</returns>
public static bool CreateFile(string FilePath, bool IfHaveFileIsCreate = false)
{
if (!Directory.Exists(FilePath))
{
Directory.CreateDirectory(FilePath);
return true;
}
else
{
if (IfHaveFileIsCreate)
{
Directory.CreateDirectory(FilePath);
return true;
}
else
{
return false;
}
}
}
/// <summary>
/// 导出文件
/// </summary>
/// <param name="fileContent">文件内容</param>
/// <param name="filePath">文件保存路径</param>
/// <param name="fileName">文件名以及后缀</param>
/// <returns>返回bool,true导出成功,false导出失败</returns>
public static bool ExportFile(byte[] fileContent, string filePath, string fileName)
{
try
{
CreateFile(filePath);
var path = Path.Combine(filePath, fileName);
using (FileStream nFile = new FileStream(path, FileMode.Create))
{
nFile.Write(fileContent, 0, fileContent.Length);
return true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"导出失败: {ex.Message}");
return false;
}
}