绘制CIImage速度太慢

11

我正在创建一个需要对图像实时应用滤镜的应用程序。将 UIImage 转换为 CIImage,并应用滤镜都非常快,但是将创建的 CIImage 转换回 CGImageRef 并显示图像需要太长时间(1/5 秒,如果编辑需要实时,则实际上相当长)。

图像大约有 2500 x 2500 像素,这很可能是问题的一部分。

目前,我正在使用:

let image: CIImage //CIImage with applied filters
let eagl = EAGLContext(API: EAGLRenderingAPI.OpenGLES2)
let context = CIContext(EAGLContext: eagl, options: [kCIContextWorkingColorSpace : NSNull()])

//this line takes too long for real-time processing
let cg: CGImage = context.createCGImage(image, fromRect: image.extent)

我已经研究了使用EAGLContext.drawImage()

context.drawImage(image, inRect: destinationRect, fromRect: image.extent)

然而,我找不到任何确切的文档说明如何完成这个过程,或者是否有更快的方法。

是否有更快速的方式将 CIImage 显示到屏幕上(无论是在 UIImageView 中还是直接在 CALayer 上)?我希望避免过度降低图像质量,因为这可能会被用户注意到。

4个回答

16

考虑使用Metal和MTKView进行显示可能是值得的。

您需要一个Metal设备,可以使用MTLCreateSystemDefaultDevice()创建。这将用于创建命令队列和Core Image上下文。这两个对象都是持久的且实例化成本很高,因此最好只创建一次:

lazy var commandQueue: MTLCommandQueue =
{
    return self.device!.newCommandQueue()
}()

lazy var ciContext: CIContext =
{
    return CIContext(MTLDevice: self.device!)
}()

您还需要一个色彩空间:

let colorSpace = CGColorSpaceCreateDeviceRGB()!

在渲染CIImage时,您需要创建一个短暂的命令缓冲区:

let commandBuffer = commandQueue.commandBuffer()

您需要将 CIImage(我们称之为 image)渲染到一个 MTKViewcurrentDrawable?.texture中。如果它绑定到了 targetTexture,则渲染语法如下:

    ciContext.render(image,
        toMTLTexture: targetTexture,
        commandBuffer: commandBuffer,
        bounds: image.extent,
        colorSpace: colorSpace)

    commandBuffer.presentDrawable(currentDrawable!)

    commandBuffer.commit()

我有一个工作版本在这里

希望这能帮到你!

Simon


我发现这是最高效的解决方案。请注意,在我的代码中,由于我没有对MTKView进行子类化,因此在调用commit()之后必须立即在MTKView上调用draw(),并且还必须将视图的device设置为创建的设备,并将其framebufferOnly设置为false - Ash
值得一提的是,还应该关注CIRenderDestinationCIContext上的startTask方法。 - Flex Monkey
我注意到这个函数有一个令人烦恼的问题,我正在努力解决它,那就是如果视图调整大小,背景可绘制对象的纹理尺寸似乎总是落后于新尺寸,我无法找到一种方法来强制它正确更新。目前,您自己的视图使用拉伸来抵消此问题,但我正在创建一个需要高质量输出的图形应用程序,并且希望图像以包含视图的确切大小呈现。 - Ash
1
好的,我有一个解决方案 - 你可能想把它放到你自己的链接版本中。只需将renderImage()函数重命名为override func draw(),在其末尾调用super.draw(),并在imagedidSet中将needsDisplay设置为true而不是调用旧函数。 - Ash
在调用commandBuffer.commit()之后一定要调用draw(),否则你的控制台会出现大量错误信息,应用程序也会在几秒钟内冻结,然后才能恢复! - Chewie The Chorkie
显示剩余2条评论

8

我最终使用了context.drawImage(image, inRect: destinationRect, fromRect: image.extent)方法。以下是我创建的图像视图类

import Foundation
//GLKit must be linked and imported
import GLKit

class CIImageView: GLKView{
    var image: CIImage?
    var ciContext: CIContext?

    //initialize with the frame, and CIImage to be displayed
    //(or nil, if the image will be set using .setRenderImage)
    init(frame: CGRect, image: CIImage?){
        super.init(frame: frame, context: EAGLContext(API: EAGLRenderingAPI.OpenGLES2))

        self.image = image
        //Set the current context to the EAGLContext created in the super.init call
        EAGLContext.setCurrentContext(self.context)
        //create a CIContext from the EAGLContext
        self.ciContext = CIContext(EAGLContext: self.context)
    }

    //for usage in Storyboards
    required init?(coder aDecoder: NSCoder){
        super.init(coder: aDecoder)

        self.context = EAGLContext(API: EAGLRenderingAPI.OpenGLES2)
        EAGLContext.setCurrentContext(self.context)
        self.ciContext = CIContext(EAGLContext: self.context)
    }

    //set the current image to image
    func setRenderImage(image: CIImage){
        self.image = image

        //tell the processor that the view needs to be redrawn using drawRect()
        self.setNeedsDisplay()
    }

    //called automatically when the view is drawn
    override func drawRect(rect: CGRect){
        //unwrap the current CIImage
        if let image = self.image{
            //multiply the frame by the screen's scale (ratio of points : pixels),
            //because the following .drawImage() call uses pixels, not points
            let scale = UIScreen.mainScreen().scale
            let newFrame = CGRectMake(rect.minX, rect.minY, rect.width * scale, rect.height * scale)

            //draw the image
            self.ciContext?.drawImage(
                image,
                inRect: newFrame,
                fromRect: image.extent
             )
        }
    }   
}

然后,要使用它,只需:
let myFrame: CGRect //frame in self.view where the image should be displayed
let myImage: CIImage //CIImage with applied filters

let imageView: CIImageView = CIImageView(frame: myFrame, image: myImage)
self.view.addSubview(imageView)

在将UIImage转换为CIImage之前,将其调整大小以适应屏幕大小也有助于提高速度,特别是在处理高质量图像的情况下。但是请确保在实际保存图像时使用全尺寸图像。

就这样!然后,要更新视图中的图像

imageView.setRenderImage(newCIImage)
//note that imageView.image = newCIImage won't work because
//the view won't be redrawn

2
你可以在image属性上使用didSet:var image: CIImage? { didSet { self.setNeedsDisplay() } }这样整个setRenderImage方法就变得过时了。 - Marcin Zbijowski
您必须在init?(coder:)中设置上下文(就像您在init(frame)中所做的那样),以便从Storyboard中使其正常工作。 - Jovan Stankovic
既然自iOS 12以来GLKView已被弃用,那么现在这是否推荐使用呢? - Michael Forrest
@MichaelForrest 当然不推荐这样做。你需要使用Metal代替。 - Kegham K.

2
您可以使用GlkView并像您说的那样使用context.drawImage()进行渲染:
let glView = GLKView(frame: superview.bounds, context: EAGLContext(API: .OpenGLES2))
let context = CIContext(EAGLContext: glView.context)

在处理后,渲染图像:
glView.bindDrawable()
context.drawImage(image, inRect: destinationRect, fromRect: image.extent)
glView.display()

0

这是一张相当大的图片,所以这肯定是其中的一部分。我建议使用GPUImage来进行单个图像滤镜处理。你可以完全跳过使用CoreImage。

let inputImage:UIImage  = //... some image
let stillImageSource = GPUImagePicture(image: inputImage)
let filter = GPUImageSepiaFilter()
stillImageSource.addTarget(filter)
filter.useNextFrameForImageCapture()
stillImageSource.processImage()

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