简体   繁体   English

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

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

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 of images reaches the boundary of the tableview cell's width (this would allow me to display rows of images in the collage). 当图像数量达到tableview单元格宽度的边界时,我想让图像换行(这样我就可以在拼贴画中显示图像行)。 Currently I get a single row. 目前我得到一排。 Please feel free to advise if additional information is required. 如果需要其他信息,请随时告知。 I am most likely not approaching this in the most efficient way since there is a delay as the number of images used in the array begins to increase. 我很可能不会以最有效的方式接近这一点,因为随着阵列中使用的图像数量开始增加而存在延迟。 (any feedback on this would be very much appreciated). (对此的任何反馈将非常感激)。

Nota Bene 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. 在这种情况下,我不相信UICollectionView将是一个有用的替代方案。

Pseudo Code 伪代码

  1. Given an array of images and a target rectangle (representing the target view) 给定一组图像和一个目标矩形(表示目标视图)
  2. Get the number of images in the array compared to max number allowed per row 获取阵列中的图像数量与每行允许的最大数量
  3. Define a smaller rectangle of appropriate size to hold the image (so that each row fills the target rectangle, ie - if one image then that should fill the row; if 9 images then that should fill the row completely; if 10 images with a max of 9 images per row then the 10th begins the second row) 定义一个适当大小的较小矩形来保存图像(以便每行填充目标矩形,即 - 如果一个图像然后填充该行;如果是9个图像那么应该完全填充该行;如果10个图像具有最大值每行9个图像然后第10个开始第二行)
  4. Iterate over the collection 迭代收集
  5. Place each rectangle at the correct location from left to right until either last image or a max number per row is reached; 将每个矩形从左到右放置在正确的位置,直到达到最后一个图像或每行最大数量; continue on next row until all images fit within the target rectangle 继续下一行,直到所有图像都适合目标矩形
  6. When reaching a max number of images per row, place the image and setup the next rectangle to appear on the successive row 当达到每行的最大图像数时,放置图像并设置下一个矩形以显示在连续的行上

Using: Swift 2.0 使用: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
    }

Alternative 1 备选方案1

I've refined my solution to this a bit. 我稍微改进了我的解决方案。 This one does stack the images in columns and rows however, as stated; 然而,如上所述,这个会将图像堆叠成列和行; my interest is in making this as efficient as possible. 我的兴趣在于尽可能提高效率。 What's presented is my attempt at producing the simplest possible thing that works. 所呈现的是我尝试制作最简单的可行方法。

Caveat 警告

The image produced using this is skewed rather than evenly distributed across the entire tableview cell. 使用此图像生成的图像是倾斜的,而不是均匀分布在整个tableview单元格中。 Efficient, even distribution across the tableview cell would be optimal. 在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
    }

Alternative 2 备选方案2

The alternative presented below scales the images so that they always fill the rectangle (in my case the tableview cell). 下面给出的替代方案缩放图像,使它们总是填充矩形(在我的情况下是tableview单元格)。 As more images are added they are scaled to fit the width of the rectangle. 随着添加更多图像,它们被缩放以适合矩形的宽度。 When the images meet the maximum number of images per row, they wrap. 当图像满足每行的最大图像数时,它们会换行。 This is the desired behavior, happens in memory, is relatively fast, and is contained in a simple class function that I extend on the UIImage class. 这是期望的行为,发生在内存中,相对较快,并且包含在我在UIImage类上扩展的简单类函数中。 I am still interested in any algorithm that can deliver the same functionality only faster. 我仍然对任何能够更快地提供相同功能的算法感兴趣。

Nota Bene: I do not believe adding more UI is useful to achieve the effects as noted above. Nota Bene:我不相信添加更多UI对于实现上述效果是有用的。 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
    }

Efficiency Testing 效率测试

Reda Lemeden gives some procedural insight into how to test these CG calls within Instruments on this blog post . Reda Lemeden在这篇博客文章中提供了一些有关如何在Instruments内测试这些CG调用的程序性见解。 He also points out some interesting notes from Andy Matuschak (of the UIKit team) about some of the peculiarities of off-screen rendering . 他还指出了Andy Matuschak(UIKit团队)关于屏幕外渲染的一些特点的一些有趣的笔记。 I am probably still not leveraging the CIImage solution properly because initial results show the solution getting slower when attempting to force GPU utilization. 我可能仍然没有正确利用CIImage解决方案,因为初始结果显示解决方案在尝试强制GPU利用率时变慢。

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. 您可以组合多个CIF过滤器来创建输出图像。

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. 您可以将CIAffineTransform过滤器应用于每个图像以对其进行排列(如果需要,使用CICrop将其裁剪为大小),然后使用CISourceOverCompositing过滤器将它们组合在一起。 Core Image doesn't process anything until you ask for the output; 在您要求输出之前,Core Image不会处理任何内容; and because it's all happening in the GPU, it's fast and efficient. 因为这一切都发生在GPU上,所以它快速而有效。

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. 您可能会发现您的CIImage旋转不正确。 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; 如果你已经优化了你的核心图像处理,那么在这一点上你看到的任何减速可能是由于你的CIImage变成了UIImage; that's because your image has to make the transition from the GPU to the CPU. 那是因为你的图像必须从GPU转换到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. 只需将结果直接渲染到GLKView即可。 You can always transition to a UIImage or CGImage at the point the user wants to save the collage. 您可以在用户想要保存拼贴的位置始终转换为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()

Since you're using Swift 2.0: UIStackView does exactly what you're trying to do manually, and is significantly easier to use than UICollectionView. 由于您使用的是Swift 2.0: UIStackView正是您尝试手动执行的操作,并且比UICollectionView更容易使用。 Assuming you're using Storyboards, creating a prototype TableViewCell with multiple nested UIStackViews should do exactly what you want. 假设您正在使用Storyboard,创建具有多个嵌套UIStackViews的原型TableViewCell应该完全符合您的要求。 You'll just need to make sure that the UIImages you're inserting are all the same aspect ratio if that's what you want. 您只需要确保您插入的UIImages具有相同的宽高比,如果这是您想要的。

Your algorithm is highly inefficient because it has to re-draw, with multiple Core Animation transforms, every image any time you add or remove an image from your array. 您的算法效率非常低,因为每次添加或删除阵列中的图像时, 都必须使用多个Core Animation变换重新绘制每个图像 UIStackView supports dynamically adding and removing objects. UIStackView支持动态添加和删除对象。

If you still, for some reason, need to snapshot the resulting collage as a UIImage, you can still do this on the UIStackView . 如果由于某种原因仍然需要将生成的拼贴作为UIImage进行快照, 您仍然可以在UIStackView上执行此操作

Building on alternative 2 provided by Tommie C above I created a function that 在上面由Tommie C提供的备选方案2的基础上,我创建了一个函数

  • Always fills the total rectangle, without spaces in the collage 始终填充整个矩形,拼贴中没有空格
  • determines the number of rows and columns automatically (maximum 1 more nrOfColumns than nrOfRows) 自动确定行数和列数(最多比nrOfRows多1个nrOfColumns)
  • To prevent the spaces mentioned above all individual pics are drawn with "Aspect Fill" (so for some pics this means that parts will be cropped) 为了防止上面提到的空间,所有单个图片都使用“Aspect Fill”绘制(因此对于某些图片,这意味着将裁剪零件)

Here's the function: 这是功能:

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
}

This uses the UIImage extension drawInRectAspectFill: 这使用了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代码,那么这个存储库可能会很有用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM