![cover](/upload/封面0103-01.png)
在Unity中频繁保存摄像机画面的功能实现
需求
现在有一个需求是通过摄像机采集画面,然后调用对应的一些图像算法,可能还要提交到服务器处理,得到这个图片上的一些计算结果(比如检测框、关键点)。后续可能还需要这些数据可视化到对应的画面上。
由于需要执行一系列的算法,还有跟服务器通讯,所以从图片到得到结果的时间应该会比较长,因而想要用异步的方式实现。当得到算法结果的时候再将画面与结果一起绘制出来,这时显示的画面就是稍早一些时候的画面,而不是当前的画面。
结论就是需要在执行算法之前把这一帧画面保留下来以便后续使用。
初版实现
关键部分,实现了这样一个代码,获取当前的相机画面的贴图,然后new一个新的贴图来保存最后返回。
至于为什么不直接返回相机的贴图,是因为相机的贴图是一个视频流,会随着画面的更新而更新,而我想要的是保存下来这一帧画面。
public Texture GetCurrentTexture() {
Texture2D resultTex = new Texture2D(webcamTexture.width, webcamTexture.height);
resultTex.SetPixels(webcamTexture.GetPixels());
resultTex.Apply();
return resultTex;
}
然后当我在Update中执行GetCurrentTexture
函数的时候帧率从4、500急剧下降到20多帧,打开Profiler看一下。主要是发现了从开始调用函数起,cpu中Scripts
、Others
的耗时变得很高(发现Others是两个绿色,不知道有什么区别),以及内存的占用也直线上升(伴随着GC的增多)。
所以合理怀疑是频繁地new Texture导致分配内存造成性能的损耗,以及频繁地触发了GC,于是考虑到用对象池看看能不能解决这个问题。
对象池
简单实现了一个对象池,因为分辨率不同的话也无法直接使用,所以根据分辨率不同还需要分开处理,这里用了个字典统一管理。(除了下述关键代码之外,再增加了一个定时去检查每一个队列,每一次逐步减少队列的长度直到为0删除字典中的队列,以免高峰之后浪费太多内存)
private Dictionary<Vector2Int, Queue<Texture2D>> texture2DQueueDict = new();
public Texture2D GetTexture2D(int width, int height)
Vector2Int indexKey = new Vector2Int(width, height);
// 如果存在的话直接返回
if (texture2DQueueDict.ContainsKey(indexKey) && texture2DQueueDict[indexKey].Count > 0)
{
return texture2DQueueDict[indexKey].Dequeue();
}
// 不存在的话创建一个新的
return new Texture2D(width, height);
}
public void ReleaseTexture2D(Texture2D tex)
{
if (tex == null) return;
Vector2Int indexKey = new Vector2Int(tex.width, tex.height);
// 如果不存在这个分辨率的队列先创建一个
if (!texture2DQueueDict.ContainsKey(indexKey))
{
texture2DQueueDict[indexKey] = new Queue<Texture2D>();
}
texture2DQueueDict[indexKey].Enqueue(tex);
}
创建的时候改为用GetTexture2D
,使用完用ReleaseTexture2D
释放。
public Texture GetCurrentTexture() {
Texture2D resultTex = ObjectPoolManager.Instance.GetTexture2D(webcamTexture.width, webcamTexture.height);
resultTex.SetPixels(webcamTexture.GetPixels());
resultTex.Apply();
return resultTex;
}
var tex = imageAcquisitonModule.GetCurrentTexture();
image.texture = tex;
// 释放掉上一张贴图
ObjectPoolManager.Instance.ReleaseTexture2D(lastTex);
lastTex = tex;
但是使用了对象池之后,帧数仍然很低(稍有好转,从二十多到了三十多,以及GC和内存确实少了),看来真正的瓶颈还不在这。
尝试了一下之后发现是resultTex.SetPixels(webcamTexture.GetPixels());
这行代码的执行时间特别长,看来得想办法优化一下这个。
RenderTexture
然后问了一下GPT目前这个方法可能造成的问题,是CPU和GPU的频繁通信(Texture2D在内存,而WebCamTexture在显存)。可以用RenderTexture来代替Texture2D来避免这个数据频繁的传输,RenderTexture的数据是在GPU上的。
修改之后代码如下,使用Blit直接在GPU上传输webcamTexture到resultTex然后返回。经过这个修改之后帧率终于又回到了四五百帧,基本没有问题了。
public Texture GetCurrentTexture() {
RenderTexture resultTex = ObjectPoolManager.Instance.GetRenderTexture(webcamTexture.width, webcamTexture.height);
Graphics.Blit(webcamTexture, resultTex);
return resultTex;
}
另外看了一下官方文档,发现有一个需要注意的点。因为RenderTexture是一个所谓的native engine object
,所以它是不被GC管理的,使用完的话需要手动调用Release()
函数来释放。关于native engine object我没有找到太多的资料,搜了一下找到一篇帖子Unity Discussions | What are all the "native engine object" types in Unity?,以及文档Unity Documentation | Memory in Unity。
另外搜到一片关于高效处理贴图的文章,Unity Blog | Accessing texture data efficiently。里面提到说如果是复制图片的话(GPU内部复制)使用Graphics.CopyTexture
会比Graphics.Blit
还要快一些,需要注意尽量让这个贴图是不可读的(不可读意味着没有在CPU上的副本,如果有CPU上的副本还会再复制CPU的,稍微慢一些),可以结合文档Unity Documentation | Graphics.CopyTexture。