How to efficiently create a multi-row photo collage from an array of images in Swift

后端 未结 4 520
梦如初夏
梦如初夏 2020-12-17 17:01

Problem

I am building a collage of photos from an array of images that I am placing onto a tableview. I want to make the images wrap when the number

4条回答
  •  借酒劲吻你
    2020-12-17 17:21

    To build the collage in memory, and to be as efficient as possible, I'd suggest looking into Core Image. You can combine multiple CIFilters to create your output image.

    You could apply CIAffineTransform filters to each of your images to line them up (cropping them to size with CICrop if necessary), then combine them using CISourceOverCompositing filters. Core Image doesn't process anything until you ask for the output; and because it's all happening in the GPU, it's fast and efficient.

    Here's a bit of code. I tried to keep it as close to your example as possible for the sake of understanding. It's not necessarily how I'd structure the code were I to use core image from scratch.

    class func collageImage (rect: CGRect, images: [UIImage]) -> UIImage {
    
        let maxImagesPerRow = 3
        var maxSide : CGFloat = 0.0
    
        if images.count >= maxImagesPerRow {
            maxSide = max(rect.width / CGFloat(maxImagesPerRow), rect.height / CGFloat(maxImagesPerRow))
        } else {
            maxSide = max(rect.width / CGFloat(images.count), rect.height / CGFloat(images.count))
        }
    
        var index = 0
        var currentRow = 1
        var xtransform:CGFloat = 0.0
        var ytransform:CGFloat = 0.0
        var smallRect:CGRect = CGRectZero
    
        var composite: CIImage? // used to hold the composite of the images
    
        for img in images {
    
            let x = ++index % maxImagesPerRow //row should change when modulus is 0
    
            //row changes when modulus of counter returns zero @ maxImagesPerRow
            if x == 0 {
    
                //last column of current row
                smallRect = CGRectMake(xtransform, ytransform, maxSide, maxSide)
    
                //reset for new row
                ++currentRow
                xtransform = 0.0
                ytransform = (maxSide * CGFloat(currentRow - 1))
    
            } else {
    
                //not a new row
                smallRect = CGRectMake(xtransform, ytransform, maxSide, maxSide)
                xtransform += CGFloat(maxSide)
            }
    
            // Note, this section could be done with a single transform and perhaps increase the
            // efficiency a bit, but I wanted it to be explicit.
            //
            // It will also use the CI coordinate system which is bottom up, so you can translate
            // if the order of your collage matters.
            //
            // Also, note that this happens on the GPU, and these translation steps don't happen
            // as they are called... they happen at once when the image is rendered. CIImage can 
            // be thought of as a recipe for the final image.
            //
            // Finally, you an use core image filters for this and perhaps make it more efficient.
            // This version relies on the convenience methods for applying transforms, etc, but 
            // under the hood they use CIFilters
            var ci = CIImage(image: img)!
    
            ci = ci.imageByApplyingTransform(CGAffineTransformMakeScale(maxSide / img.size.width, maxSide / img.size.height))
            ci = ci.imageByApplyingTransform(CGAffineTransformMakeTranslation(smallRect.origin.x, smallRect.origin.y))!
    
            if composite == nil {
    
                composite = ci
    
            } else {
    
                composite = ci.imageByCompositingOverImage(composite!)
            }
        }
    
        let cgIntermediate = CIContext(options: nil).createCGImage(composite!, fromRect: composite!.extent())
        let finalRenderedComposite = UIImage(CGImage: cgIntermediate)!
    
        return finalRenderedComposite
    }
    

    You may find that your CIImage is rotated incorrectly. You can correct it with code like the following:

    var transform = CGAffineTransformIdentity
    
    switch ci.imageOrientation {
    
    case UIImageOrientation.Up:
        fallthrough
    case UIImageOrientation.UpMirrored:
        println("no translation necessary. I am ignoring the mirrored cases because in my code that is ok.")
    case UIImageOrientation.Down:
        fallthrough
    case UIImageOrientation.DownMirrored:
        transform = CGAffineTransformTranslate(transform, ci.size.width, ci.size.height)
        transform = CGAffineTransformRotate(transform, CGFloat(M_PI))
    case UIImageOrientation.Left:
        fallthrough
    case UIImageOrientation.LeftMirrored:
        transform = CGAffineTransformTranslate(transform, ci.size.width, 0)
        transform = CGAffineTransformRotate(transform, CGFloat(M_PI_2))
    case UIImageOrientation.Right:
        fallthrough
    case UIImageOrientation.RightMirrored:
        transform = CGAffineTransformTranslate(transform, 0, ci.size.height)
        transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
    }
    
    ci = ci.imageByApplyingTransform(transform)
    

    Note that this code ignores fixing several mirrored cases. I'll leave that as an exercise up to you, but the gist of it is here.

    If you've optimized your Core Image processing, then at this point any slowdown you see is probably due to transforming your CIImage into a UIImage; that's because your image has to make the transition from the GPU to the CPU. If you want to skip this step in order to display the results to the user, you can. Simply render your results to a GLKView directly. You can always transition to a UIImage or CGImage at the point the user wants to save the collage.

    // this would happen during setup
    let eaglContext = EAGLContext(API: .OpenGLES2)
    glView.context = eaglContext
    
    let ciContext = CIContext(EAGLContext: glView.context)
    
    // this would happen whenever you want to put your CIImage on screen
    if glView.context != EAGLContext.currentContext() {
        EAGLContext.setCurrentContext(glView.context)
    }
    
    let result = ViewController.collageImage(glView.bounds, images: images)
    
    glView.bindDrawable()
    ciContext.drawImage(result, inRect: glView.bounds, fromRect: result.extent())
    glView.display()
    

提交回复
热议问题