如何从 Swift 中的图像数组高效创建多行照片拼贴

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

问题

我正在从放置在 tableview 上的一组图像构建照片拼贴画。当图像数量达到 tableview 单元格宽度的边界时,我想让图像换行(这将允许我在拼贴中显示图像行)。目前我得到一行。如果需要其他信息,请随时告知。我很可能不会以最有效的方式解决这个问题,因为随着阵列中使用的图像数量开始增加,会出现延迟。 (对此的任何反馈将不胜感激)。

Nota Bene

I am creating a collage image. It is actually one image. I want to arrange the collage by creating an efficent matrix of columns and rows in memory. I then fill these rects with images. Finally I snapshot the resulting image and use it when needed. The algorithm is not efficient as written and produces only a single row of images. I need a lightweight alternative to the algorithm used below. I do not believe UICollectionView will be a useful alternative in this case.

伪代码

  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
    }

选项 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
    }

备选方案 2

下面介绍的替代方案缩放图像,以便它们始终填充矩形(在我的例子中是表格视图单元格)。随着更多图像的添加,它们会被缩放以适合矩形的宽度。当图像达到每行图像的最大数量时,它们会换行。这是期望的行为,发生在内存中,速度相对较快,并且包含在我在 UIImage class 上扩展的简单 class 函数中。我仍然对任何可以更快地提供相同功能的算法感兴趣。

Nota Bene: I do not believe adding more UI is useful to achieve the effects as noted above. Therefore a more efficient coding algorithm is what I am seeking.

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
    }

效率测试

Reda Lemeden 给出了一些关于如何在此 blog post. He also points out some interesting notes from Andy Matuschak (of the UIKit team) about some of the peculiarities of off-screen rendering 上的 Instruments 中测试这些 CG 调用的程序性见解。我可能仍然没有正确利用 CIImage 解决方案,因为初步结果表明,在尝试强制使用 GPU 时,该解决方案变得越来越慢。

由于您使用的是 Swift 2.0:UIStackView 完全符合您手动尝试执行的操作,并且比 UICollectionView 更易于使用。假设您使用的是情节提要,创建具有多个嵌套 UIStackView 的原型 TableViewCell 应该完全符合您的要求。如果您想要的话,您只需要确保您插入的 UIImages 都是相同的宽高比。

您的算法效率非常低,因为它必须重新绘制,使用多个 Core Animation 变换,每张图像 每当您从数组中添加或删除图像时。 UIStackView 支持动态添加和删除对象。

如果出于某种原因您仍然需要将生成的拼贴画快照为 UIImage,you can still do this on the UIStackView

要在内存中构建拼贴画并尽可能高效,我建议研究 Core Image。您可以组合多个 CIFilter 来创建输出图像。

您可以应用 CIAffineTransform filters to each of your images to line them up (cropping them to size with CICrop if necessary), then combine them using 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
}

您可能会发现您的 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)

请注意,此代码忽略了对几个镜像案例的修复。我会把它留给你作为练习,但是 gist of it is here.

如果您优化了 Core Image 处理,那么此时您看到的任何减速都可能是由于将 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()

基于上面 Tommie C 提供的备选方案 2,我创建了一个函数

  • 始终填满整个矩形,拼贴画中没有空格
  • 自动确定行数和列数(nrOfColumns 最多比 nrOfRows 多 1)
  • 为了避免上面提到的空间,所有单独的图片都用 "Aspect Fill" 绘制(因此对于某些图片,这意味着部分将被裁剪)

函数如下:

func collageImage(rect: CGRect, images: [UIImage]) -> UIImage {
    if images.count == 1 {
        return images[0]
    }

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

    let nrofColumns: Int = max(2, Int(sqrt(Double(images.count-1)))+1)
    let nrOfRows: Int = (images.count)/nrofColumns
    let remaindingPics: Int = (images.count) % nrofColumns
    print("columns: \(nrofColumns) rows: \(nrOfRows) first \(remaindingPics) columns will have 1 pic extra")

    let w: CGFloat = rect.width/CGFloat(nrofColumns)
    var hForColumn = [CGFloat]()
    for var c=1;c<=nrofColumns;++c {
        if remaindingPics >= c {
            hForColumn.append(rect.height/CGFloat(nrOfRows+1))
        }
        else {
            hForColumn.append(rect.height/CGFloat(nrOfRows))
        }
    }
    var colNr = 0
    var rowNr = 0
    for var i=1; i<images.count; ++i {
        images[i].drawInRectAspectFill(CGRectMake(CGFloat(colNr)*w,CGFloat(rowNr)*hForColumn[colNr],w,hForColumn[colNr]))
        if i == nrofColumns || ((i % nrofColumns) == 0 && i > nrofColumns) {
            ++rowNr
            colNr = 0
        }
        else {
            ++colNr
        }
    }

    let outputImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return outputImage
}

这使用了 UIImage 扩展 drawInRectAspectFill:

extension UIImage {
    func drawInRectAspectFill(rect: CGRect, opacity: CGFloat = 1.0) {
        let targetSize = rect.size
        let scaledImage: UIImage
        if targetSize == CGSizeZero {
            scaledImage = self
        } else {
            let scalingFactor = targetSize.width / self.size.width > targetSize.height / self.size.height ? targetSize.width / self.size.width : targetSize.height / self.size.height
            let newSize = CGSize(width: self.size.width * scalingFactor, height: self.size.height * scalingFactor)
            UIGraphicsBeginImageContext(targetSize)
            self.drawInRect(CGRect(origin: CGPoint(x: (targetSize.width - newSize.width) / 2, y: (targetSize.height - newSize.height) / 2), size: newSize), blendMode: CGBlendMode.Normal, alpha: opacity)
            scaledImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
        }
        scaledImage.drawInRect(rect)
    }
}

如果有人正在寻找 Objective C 代码,这个 repository 可能会有用。