Given a CIImage, what is the fastest way to write image data to disk?
Asked Answered
S

2

8

I'm working with PhotoKit and have implemented filters users can apply to photos in their Photo Library. I am currently obtaining the image, applying a filter, returning the edited version as a CIImage, then I convert the CIImage into NSData using UIImageJPEGRepresentation so I may write that out to disk. While this works beautifully, when users attempt to edit really large (like 30 MB) photos it can take upwards of 30 seconds for this to occur, with 98% of the time spent on UIImageJPEGRepresentation (stat obtained from Instruments).

I am looking for a more efficient way to save this edited photo to disk without compromising quality, if possible.

I understand UIImagePNGRepresentation may result in improved quality, but this is even slower than the JPEG representation.

This is what I am currently doing, on the default priority thread (qos_class_utility):

func jpegRepresentationOfImage(image: CIImage) -> NSData {
    let eaglContext = EAGLContext(API: .OpenGLES2)
    let ciContext = CIContext(EAGLContext: eaglContext)

    let outputImageRef = ciContext.createCGImage(image, fromRect: image.extent())
    let uiImage = UIImage(CGImage: outputImageRef, scale: 1.0, orientation: UIImageOrientation.Up)

    return UIImageJPEGRepresentation(uiImage, 0.9) //this takes upwards of 20-30 seconds with large photos!
}

//writing out to disk:
var error: NSError?
let success = jpegData.writeToURL(contentEditingOutput.renderedContentURL, options: NSDataWritingOptions.AtomicWrite, error: &error)
Syncopated answered 12/1, 2015 at 6:23 Comment(1)
Brad noticed the slowdown it seems github.com/BradLarson/GPUImageHecate
S
8

I would suggest passing the CGImage directly to ImageIO using CGImageDestination. You can pass a dictionary to CGImageDestinationAddImage to indicate the compression quality, image orientation, etc.

CFDataRef save_cgimage_to_jpeg (CGImageRef image)
{
    CFMutableDataRef cfdata = CFDataCreateMutable(nil,0);
    CGImageDestinationRef dest = CGImageDestinationCreateWithData(data, CFSTR("public.jpeg"), 1, NULL);
    CGImageDestinationAddImage(dest, image, NULL);
    if(!CGImageDestinationFinalize(dest))
        ; // error
    CFRelease(dest);
    return cfdata
}
Specious answered 1/2, 2015 at 12:18 Comment(2)
That does seem quite a bit faster, by at least 8 seconds. 22 seconds is still a long time but I suppose that's to be expected when the image is large.Syncopated
Have you tried changing the thread priority? Utility is one of the lowest. You could try using the .UserInitiated or .UserInteractive QoS modes in an NSOperationQueue if your goal is timely executionRandalrandall
L
10

Try using the CIContext method writeJPEGRepresentation (available iOS 10) as follows to eschew UIImage and make the writing faster.

extension CIImage {

    @objc func saveJPEG(_ name:String, inDirectoryURL:URL? = nil, quality:CGFloat = 1.0) -> String? {
        
        var destinationURL = inDirectoryURL
        
        if destinationURL == nil {
            destinationURL = try? FileManager.default.url(for:.documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        }
        
        if var destinationURL = destinationURL {
            
            destinationURL = destinationURL.appendingPathComponent(name)
            
            if let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) {
                
                do {

                    let context = CIContext()

                    try context.writeJPEGRepresentation(of: self, to: destinationURL, colorSpace: colorSpace, options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption : quality])
                    
                    return destinationURL.path
                    
                } catch {
                    return nil
                }
            }
        }
        
        return nil
    }
}

The @objc keyword enables you to call the method in Objective-C as:

NSString* path = [image saveJPEG:@"image.jpg" inDirectoryURL:url quality:1.0];

For PNG there is a similar method writePNGRepresentation for iOS 11:

   if #available(iOS 11.0, *) {

        if let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) {
                
            do {
                let format = CIFormat.RGBA8
                    
                try context.writePNGRepresentation(of: self, to: destinationURL, format: format, colorSpace: colorSpace)
                    
                return destinationURL.path
                    
            } catch {
                return nil
            }   
        }
    }
Lilas answered 28/12, 2018 at 19:43 Comment(1)
By the way, you don't need to save it to disk first if you just need Data as output. Just call context.jpegRepresentation(of:colorSpace:options:).Mcclimans
S
8

I would suggest passing the CGImage directly to ImageIO using CGImageDestination. You can pass a dictionary to CGImageDestinationAddImage to indicate the compression quality, image orientation, etc.

CFDataRef save_cgimage_to_jpeg (CGImageRef image)
{
    CFMutableDataRef cfdata = CFDataCreateMutable(nil,0);
    CGImageDestinationRef dest = CGImageDestinationCreateWithData(data, CFSTR("public.jpeg"), 1, NULL);
    CGImageDestinationAddImage(dest, image, NULL);
    if(!CGImageDestinationFinalize(dest))
        ; // error
    CFRelease(dest);
    return cfdata
}
Specious answered 1/2, 2015 at 12:18 Comment(2)
That does seem quite a bit faster, by at least 8 seconds. 22 seconds is still a long time but I suppose that's to be expected when the image is large.Syncopated
Have you tried changing the thread priority? Utility is one of the lowest. You could try using the .UserInitiated or .UserInteractive QoS modes in an NSOperationQueue if your goal is timely executionRandalrandall

© 2022 - 2025 — McMap. All rights reserved.