如何从Swift中的图像arrays高效地创build多行照片拼贴

问题

我正在build立一个从我放置到桌面视图的图像arrays的照片拼贴。 当图像的数量达到tableview单元格宽度的边界时(这将允许我显示拼贴中的图像行),我想要使图像换行。 目前我得到一个单一的行。 如果需要更多信息,请随时通知我们。 我最有可能不会以最有效的方式来处理这个问题,因为在arrays中使用的图像数量开始增加时会有延迟。 (对此的任何反馈将非常感激)。

Nota Bene

我正在创build一个拼贴图片。 它实际上是一个图像。 我想通过在内存中创build一个高效的列和行matrix来安排拼贴。 然后我用图像填充这些反义词。 最后,我快照生成的图像,并在需要时使用它。 该algorithm效率不如书面,只产生一行图像。 我需要一个轻量级的替代下面使用的algorithm。 我不相信在这种情况下,UICollectionView将是一个有用的替代scheme。

伪代码

  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 } 

替代scheme1

我已经完善了我的解决scheme。 但是,如上所述,这种方法不是按照列和行堆叠图像。 我的兴趣在于尽可能提高效率。 所呈现的是我试图制作出最简单可行的作品。

警告

使用这个产生的图像是倾斜的而不是均匀分布在整个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

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

Nota Bene:我不认为添加更多的UI对于达到上述效果是非常有用的。 因此,我正在寻找更有效的编码algorithm。

 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 } 

效率testing

Reda Lemeden给出了一些关于如何在本文博客文章中testing这些CG调用的程序性见解。 他还指出了UIKit团队的Andy Matuschak关于离屏渲染的一些特性。 我可能仍然没有正确使用CIImage解决scheme,因为初始结果显示,在尝试强制使用GPU时,解决scheme变得越来越慢。

为了build立记忆中的拼贴画,并尽可能高效,我build议看看核心图像 。 您可以组合多个CIFilter来创build您的输出图像。

您可以将CIAffineTransformfilter应用于每个图像,以便将它们alignment(如果需要,请使用CICrop将其裁剪成大小),然后使用CISourceOverCompositingfilter合并它们。 核心图像不处理任何东西,直到你要求输出; 而且因为这一切都发生在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) 

请注意,这段代码忽略修复几个镜像的情况。 我会把这个作为一个练习留给你,但它的要点在这里 。

如果你已经优化了你的核心image processing,那么在这一点上你看到的任何放缓都可能是由于把你的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() 

由于您使用的是Swift 2.0,因此UIStackView完全是您要手动执行的操作,而且比UICollectionView更易于使用。 假设你正在使用Storyboard,创build一个具有多个嵌套的UIStackViews的原型TableViewCell应该做你想要的。 你只需要确保你插入的UIImage都是相同的长宽比,如果这是你想要的。

你的algorithm效率非常低,因为每当你从数组中添加或者移除图像的时候, 都需要使用多个核心animation变换重新绘制图像。 UIStackView支持dynamic添加和删除对象。

如果由于某种原因,仍然需要将生成的拼贴画作为UIImage进行快照, 则仍然可以在UIStackView上执行此操作 。

基于Tommie C提供的替代scheme2,我创build了一个函数

  • 始终填充整个矩形,在拼贴中没有空格
  • 自动确定行数和列数(最多比nrOfRows多1个nrOfColumns)
  • 为了防止上面提到的空间所有的个人照片是用“方面填充”(所以对于一些照片,这意味着部分将被裁剪)

这个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 } 

这使用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的代码,这个仓库可能是有用的。

Interesting Posts