[英]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 伪代码
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的基础上,我创建了一个函数
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.