如何从Swift中的一系列图像中有效地创建多行照片拼贴

Tom*_* C. 13 performance processing-efficiency ios swift

问题

我正在构建一系列照片拼贴画,这些照片是我放在桌面视图上的.当图像数量达到tableview单元格宽度的边界时,我想让图像换行(这样我就可以在拼贴中显示图像行).目前我得到一排.如果需要其他信息,请随时告知.我很可能不会以最有效的方式接近这一点,因为随着阵列中使用的图像数量开始增加而存在延迟.(对此的任何反馈将非常感激).

Nota Bene

我正在创建拼贴图像.它实际上是一个图像.我想通过在内存中创建一个有效的列和行矩阵来安排拼贴 .然后我用图像填充这些图像.最后,我对生成的图像进行快照,并在需要时使用它.算法写入效率不高,只产生一行图像.我需要一个轻量级替代下面使用的算法.在这种情况下,我不相信UICollectionView会是一个有用的替代品.

伪代码

  1. 给定一组图像和一个目标矩形(表示目标视图)
  2. 获取阵列中的图像数量与每行允许的最大数量
  3. 定义一个适当大小的较小矩形来保存图像(以便每行填充目标矩形,即 - 如果一个图像然后填充该行;如果是9个图像那么应该完全填充该行;如果10个图像具有最大值每行9个图像然后第10个开始第二行)
  4. 迭代收集
  5. 将每个矩形从左到右放置在正确的位置,直到达到最后一个图像或每行最大数量; 继续下一行,直到所有图像都适合目标矩形
  6. 当达到每行的最大图像数时,放置图像并设置下一个矩形以显示在连续的行上

使用:Swift 2.0

class func collageImage (rect:CGRect, images:[UIImage]) -> UIImage {

        let maxSide = max(rect.width / CGFloat(images.count), rect.height / CGFloat(images.count))

        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)

        var xtransform:CGFloat = 0.0

        for img in images {
            let smallRect:CGRect = CGRectMake(xtransform, 0.0,maxSide, maxSide)
            let rnd = arc4random_uniform(270) + 15
            //draw in rect
            img.drawInRect(smallRect)
            //rotate img using random angle.
            UIImage.rotateImage(img, radian: CGFloat(rnd))
            xtransform += CGFloat(maxSide * 0.8)
        }

        let outputImage = UIGraphicsGetImageFromCurrentImageContext();

        UIGraphicsEndImageContext();

        return outputImage
    }

    class func rotateImage(src: UIImage, radian:CGFloat) -> UIImage
    {
        //  Calculate the size of the rotated view's containing box for our drawing space
        let rotatedViewBox = UIView(frame: CGRectMake(0,0, src.size.width, src.size.height))

        let t: CGAffineTransform  = CGAffineTransformMakeRotation(radian)

        rotatedViewBox.transform = t
        let rotatedSize = rotatedViewBox.frame.size

        //  Create the bitmap context
        UIGraphicsBeginImageContext(rotatedSize)

        let bitmap:CGContextRef = UIGraphicsGetCurrentContext()

        //  Move the origin to the middle of the image so we will rotate and scale around the center.
        CGContextTranslateCTM(bitmap, rotatedSize.width/2, rotatedSize.height/2);

        //  Rotate the image context
        CGContextRotateCTM(bitmap, radian);

        //  Now, draw the rotated/scaled image into the context
        CGContextScaleCTM(bitmap, 1.0, -1.0);
        CGContextDrawImage(bitmap, CGRectMake(-src.size.width / 2, -src.size.height / 2, src.size.width, src.size.height), src.CGImage)

        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage
    }
Run Code Online (Sandbox Code Playgroud)

备选方案1

我稍微改进了我的解决方案.然而,如上所述,这个会将图像堆叠成列和行; 我的兴趣在于尽可能提高效率.所呈现的是我尝试制作最简单的可行方法.

警告

使用此图像生成的图像是倾斜的,而不是均匀分布在整个tableview单元格中.在tableview单元格上的高效,均匀分布将是最佳的.

偏态分布

class func collageImage (rect:CGRect, images:[UIImage]) -> UIImage {

        let maxSide = max(rect.width / CGFloat(images.count), rect.height / CGFloat(images.count)) //* 0.80
        //let rowHeight = rect.height / CGFloat(images.count) * 0.8
        let maxImagesPerRow = 9
        var index = 0
        var currentRow = 1
        var xtransform:CGFloat = 0.0
        var ytransform:CGFloat = 0.0
        var smallRect:CGRect = CGRectZero

        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)

        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
                //xtransform += CGFloat(maxSide)
                smallRect = CGRectMake(xtransform, ytransform, maxSide, maxSide)

                //reset for new row
                ++currentRow
                xtransform = 0.0
                ytransform = (maxSide * CGFloat(currentRow - 1))

            } else {
                //not a new row
                if xtransform == 0 {
                    //this is first column
                    //draw rect at 0,ytransform
                    smallRect = CGRectMake(xtransform, ytransform, maxSide, maxSide)
                    xtransform += CGFloat(maxSide)
                } else {
                    //not the first column so translate x, ytransform to be reset for new rows only
                    smallRect = CGRectMake(xtransform, ytransform, maxSide, maxSide)
                    xtransform += CGFloat(maxSide)
                }

            }

            //draw in rect
            img.drawInRect(smallRect)

        }

        let outputImage = UIGraphicsGetImageFromCurrentImageContext();

        UIGraphicsEndImageContext();

        return outputImage
    }
Run Code Online (Sandbox Code Playgroud)

备选方案2

下面给出的替代方案缩放图像,使它们总是填充矩形(在我的情况下是tableview单元格).随着添加更多图像,它们被缩放以适合矩形的宽度.当图像满足每行的最大图像数时,它们会换行.这是期望的行为,发生在内存中,相对较快,并且包含在我在UIImage类上扩展的简单类函数中.我仍然对任何能够更快地提供相同功能的算法感兴趣.

Nota Bene:我不相信添加更多UI对于实现上述效果很有用.因此,我正在寻求一种更有效的编码算法.

class func collageImage (rect:CGRect, images:[UIImage]) -> UIImage {

        let maxImagesPerRow = 9
        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

        UIGraphicsBeginImageContextWithOptions(rect.size, false,  UIScreen.mainScreen().scale)

        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)  
            }

            //draw in rect
            img.drawInRect(smallRect)

        }

        let outputImage = UIGraphicsGetImageFromCurrentImageContext();

        UIGraphicsEndImageContext();

        return outputImage
    }
Run Code Online (Sandbox Code Playgroud)

效率测试

Reda Lemeden在这篇博客文章中提供了一些有关如何在Instruments内测试这些CG调用的程序性见解.他还指出了Andy Matuschak(UIKit团队)关于屏幕外渲染的一些特点的一些有趣的笔记.我可能仍然没有正确利用CIImage解决方案,因为初始结果显示解决方案在尝试强制GPU利用率时变慢.

Tra*_*vis 14

为了在内存中构建拼贴,并尽可能高效,我建议调查核心图像.您可以组合多个CIF过滤器来创建输出图像.

您可以将CIAffineTransform过滤器应用于每个图像以对其进行排列(如果需要,使用CICrop将其裁剪为大小),然后使用CISourceOverCompositing过滤器将它们组合在一起.在您要求输出之前,Core Image不会处理任何内容; 因为这一切都发生在GPU上,所以它快速而有效.

这是一些代码.为了理解,我尽量让它尽可能接近你的例子.如果我从头开始使用核心图像,那么我的结构代码并不一定如此.

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
}
Run Code Online (Sandbox Code Playgroud)

您可能会发现您的CIImage旋转不正确.您可以使用以下代码更正它:

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)
Run Code Online (Sandbox Code Playgroud)

请注意,此代码忽略修复多个镜像案例.我会把它作为练习留给你,但要点就在这里.

如果你已经优化了你的核心图像处理,那么在这一点上你看到的任何减速可能是由于你的CIImage变成了UIImage; 那是因为你的图像必须从GPU转换到CPU.如果要跳过此步骤以向用户显示结果,则可以.只需将结果直接渲染到GLKView即可.您可以在用户想要保存拼贴的位置始终转换为UIImage或CGImage.

// 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()
Run Code Online (Sandbox Code Playgroud)