如何使用CGImage和直接的CGDataProvider直接更新像素

9

实际问题

有几个答案可以解决我的问题:

  1. 我能否强制CGImage从直接数据提供程序(使用CGDataProviderCreateDirect创建)重新加载其数据,就像CGContextDrawImage一样?或者是否有其他方法可以通过设置self.layer.contents来实现它?
  2. 是否有CGContext配置或技巧可用于使用CGContextDrawImage以至少30 fps稳定地渲染1024x768图像。
  3. 是否有人能够成功地使用CVOpenGLESTextureCacheCreateTextureFromImage进行实时缓冲区更新并使用自己的纹理数据?我认为我的最大问题是创建一个CVImageBuffer,因为我从苹果文档中复制了其他属性。如果有人对此有更多信息,那将太棒了。
  4. 如何将内存中的图像以30 fps的速度传输到屏幕上的任何其他指南。

背景(很多):

我正在处理一个项目,需要实时修改非2次幂图像数据的像素(最低30 fps),并在iOS屏幕上绘制出来。
我的第一个想法是使用OpenGL和“glTexSubimage2D”进行更新,不幸的是这种方式非常慢(iPad上只有6 fps),因为驱动程序每帧都会将我的RGB数据转换为BGR。你建议以BGR格式发送它,我也这么做了,但由于某种原因,您不能使用“GL_BGR”调用“glTexSubImage2D”,真是令人费解。我知道一些缓慢是因为它是非2次幂图像数据,但我的要求规定了这一点。
更多的阅读让我了解到“CVOpenGLESTextureCacheCreateTextureFromImage”,但所有的例子都是使用直接相机输入来获取“CVImageBufferRef”。我尝试使用文档(尚未正式发布,只有头文件注释)从我的图像数据中创建自己的CVImageBuffer,但它无法与此配合使用(在调试器中只有一个空纹理),这使我认为Apple专门构建了这个来处理实时相机数据,并且它在这个领域之外没有得到很多测试,但我不确定。
无论如何,放弃了OpenGL并转向CoreGraphics后,我遇到了这个问题在iPhone上绘制屏幕缓冲区的最快方法,它建议使用由CGDataProviderCreateDirect支持的CGImage,这样当CGImage需要时,可以返回一个指向图像数据的指针,很棒对吧?但是它似乎并没有完全按照广告所说的那样工作。如果我使用CGContextDrawImage,那么一切都正常。我可以修改像素缓冲区,并且每次绘制时,它会像应该做的那样从我的数据提供程序请求图像数据,调用CGDataProviderDirectCallbacks中的方法(注意:如果更新的指针具有与以前相同的地址,则它们似乎内置了一个忽略更新指针的优化)。即使我调用[layer setNeedsDisplay],CGImage也不会像CGContextDrawImage那样从数据提供程序重新加载。在我引用的SO问题中,用户通过每帧创建和销毁来解决问题,这是一个无望的缓慢过程(是的,我试过了),因此现在是真正的问题。
注意:我对所有这些操作进行了分析,知道问题确实是OpenGL的glTexSubImage和CoreGraphics的CGContextDrawImage真正的问题,因此不要给出“进行分析”的答案。
编辑:演示这种技术的源代码现在可以在http://github.com/narpas/image-sequence-streaming上找到。

关于1),CGImageRefs是不可变的。当然,您可以使用修改后的提供程序创建一个新的CGImageRef。在2)中,您现在获得的fps是多少?同时,您需要担心视网膜屏幕吗?那将是2倍的尺寸...过去,我使用AV框架和静态图片(截图)创建了一个“电影”,我认为在Mac上可以获得20-30帧每秒的速度。这里的想法是创建一个压缩的视频对象,在播放时很可能会得到硬件协助。尝试复制您想要的像素数量将会有问题,但我不知道绝对的性能界限。 - David H
我刚刚阅读了您上面链接中的所有评论(“最快的方法...”) - 末尾有一条评论提到使用两个CALayer,每个都由一个图像支持,基本上您提供一个,当它显示时切换另一个的后备图像,然后再切换,以此类推。您无法更新数据提供程序并获得已经用于“重新绘制”的相同CGImageRef - CALayer可能已经缓存了其数据。如果您切换图像,则应从新图像重新获取数据。也许如果您取消设置CGImageRef,然后再次设置它,您可以强制CALayer重新加载一个图像。 - David H
@DavidH 是的,我也这么想,我尝试了使用两个CGImageRef缓冲区交换每一帧,但没有成功...这完全是一个缓存问题,因为如果我使用数据提供程序创建CGImage,然后修改像素,然后分配self.layer.contents,图像将是修改后的缓冲区,但我永远无法在分配后更新它。 - Justin Meiners
在macOS中的新图形API方面,这个问题有任何更新吗? - Rol
@Rol 我现在不涉足苹果生态系统。你是指放弃OpenGL而转向Metal吗?还是其他CoreGraphics的变化? - Justin Meiners
显示剩余9条评论
1个回答

8
感谢Brad Larson和David H在此方面的帮助(请查看我们在评论中的完整讨论)。结果发现,使用OpenGL和CoreVideo与CVOpenGLESTextureCache一起使用是将原始图像推送到屏幕上的最快方法(我知道CoreGraphics不可能是最快的!),在iPad 1上使用全屏1024x768图像时,可以实现60 fps。目前很少有相关文档,因此我会尽可能详细地解释以帮助人们理解:

CVOpenGLESTextureCacheCreateTextureFromImage允许您创建一个OpenGL纹理,该纹理的内存直接映射到您用于创建它的CVImageBuffer。这使您可以创建一个带有原始数据的CVPixelBuffer,并修改从CVPixelBufferGetBaseAddress获取的数据指针。这样可以在OpenGL中立即获得结果,而无需修改或重新上传实际纹理。只需在修改像素之前使用CVPixelBufferLockBaseAddress进行锁定,并在完成后解锁。请注意,此时在iOS模拟器中无法工作,仅在设备上有效,我猜测这是由于VRAM / RAM分配不同,CPU无法直接访问VRAM。Brad建议使用条件编译器检查,在glTexImage2D更新和使用纹理缓存之间切换。

需要注意以下几点(其中一些组合导致它不能正常工作):

  1. 在设备上进行测试。
  2. 确保你的CVPixelBuffer是由kCVPixelBufferIOSurfacePropertiesKey支持的,请参考link的示例(再次感谢Brad)。
  3. 对于OpenGL ES 2.0中的NPOT纹理数据,必须使用GL_CLAMP_TO_EDGE
  4. 使用glBindTexture(CVOpenGLESTextureGetTarget(_cvTexture), CVOpenGLESTextureGetName(_cvTexture));绑定纹理缓存,不要像我一样愚蠢地同时使用CVOpenGLESTextureGetTarget作为参数。
  5. 不要每帧重新创建纹理,只需将图像数据复制到从CVPixelBufferGetBaseAddress获取的指针中以更新纹理。

1
@OHM 这还不完整,但我会尝试在明天或周日清理一些东西。 - Justin Meiners
1
@JustinMeiners 谢谢!你有 GitHub 或其他什么账号吗? - OMH
1
@OMH 感谢您的耐心等待(并激励我完成),我已经在https://github.com/narpas/image-sequence-streaming发布了一个项目。源代码还没有提交,但应该会在明天之内提交上去。 - Justin Meiners
今天是第七天。鞭子响声 =) 你能至少发布上面的一小段代码吗? - aoakenfo
@aoakenfo 哈哈,感谢你的坚持 - 实际代码已经完成并准备好推送了 - 我使用的示例数据集之一是我为另一家公司做的项目使用的。我不能使用它,因为它是他们拥有的 - 我正在创建替代数据。 - Justin Meiners
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接