Как эффективно создавать многострочный фотоколлаж из массива изображений в Swift

Проблема

Я создаю коллаж из фотографий из массива изображений, которые я размещаю на столе. Я хочу сделать обертывание изображений, когда количество изображений достигнет границы ширины ячейки таблицы (это позволит мне отображать строки изображений в коллаже). В настоящее время я получаю один ряд. Пожалуйста, не стесняйтесь сообщать, требуется ли дополнительная информация. Я, скорее всего, не подхожу к этому наиболее эффективным способом, так как есть задержка, так как количество изображений, используемых в массиве, начинает увеличиваться. (любая обратная связь по этому поводу была бы очень оценена).

Nota Bene

Я создаю изображение коллажа. Это на самом деле одно изображение. я хочу упорядочить коллаж, создав эффективную матрицу столбцов и строк в памяти. Затем я заполняю эти прямоугольники изображениями. Наконец, я делаю снимок полученного изображения и использую его, когда это необходимо. Алгоритм не эффективно, как написано, и создает только одну строку изображений. мне нужно легкая альтернатива алгоритму, используемому ниже. я не верьте, что UICollectionView будет полезной альтернативой в этом случае.

Псевдокод

  • Учитывая массив изображений и целевой прямоугольник (представляющий целевой вид)
  • Получить количество изображений в массиве по сравнению с максимальным числом, разрешенным для каждой строки.
  • Определите меньший прямоугольник соответствующего размера, чтобы удерживать изображение (так что каждая строка заполняет целевой прямоугольник, то есть - если одно изображение затем должно заполнить строку; если 9 изображений, то это должно заполнить строку полностью; если 10 изображений с максимальным количеством 9 изображений на строку, а затем 10-й начинается вторая строка)
  • Итерации по коллекции
  • Поместите каждый прямоугольник в нужное место слева направо пока не будет достигнуто последнее изображение или максимальное число в строке; продолжить в следующей строке, пока все изображения не будут помещены в целевой прямоугольник
  • При достижении максимального количества изображений в строке поместите изображение и установите следующий прямоугольник, который появится в следующей строке

Использование: 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 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

Я немного уточнил свое решение. Тем не менее, он устанавливает стек в столбцах и строках, как указано; я заинтересован в том, чтобы сделать это максимально эффективным. Что представляет собой моя попытка создать простейшую возможную вещь, которая работает.

Caveat

Изображение, созданное с использованием этого, искажается, а не равномерно распределяется по всей ячейке tableview. Эффективное, равномерное распределение по ячейке tableview было бы оптимальным.

skewed-distribution

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. Меня все еще интересует любой алгоритм, который может доставлять одни и те же функции только быстрее.

Nota Bene: Я не верю, что добавление большего пользовательского интерфейса полезно для достижения как отмечалось выше. Поэтому более эффективный алгоритм кодирования что я ищу.

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 дает некоторое процедурное представление о том, как тестировать эти вызовы CG внутри инструментов в этом сообщении . Он также отмечает некоторые интересные заметки от Энди Матушака (команды UIKit) о некоторых особенностях off-screen rendering. Вероятно, я до сих пор не использую решение CIImage должным образом, потому что первые результаты показывают, что решение становится медленнее при попытке принудительного использования графического процессора.

Ответ 1

Чтобы создать коллаж в памяти и быть как можно более эффективным, я бы предложил посмотреть Core Image. Вы можете комбинировать несколько CIFilters для создания выходного изображения.

Вы можете применить CIAffineTransform фильтры к каждому из ваших изображений, чтобы выровнять их (обрезать их до размера с помощью CICrop, если необходимо), а затем объединить их с помощью фильтров CISourceOverCompositing. Core Image ничего не обрабатывает, пока вы не спросите о выходе; и потому, что все это происходит в графическом процессоре, это быстро и эффективно.

Вот немного кода. Я старался максимально приблизить его к вашему примеру, чтобы понять. Это не обязательно, как бы я структурировал код, чтобы я использовал основное изображение с нуля.

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)

Обратите внимание, что этот код игнорирует исправление нескольких зеркальных случаев. Я оставлю это как упражнение, но

Ответ 2

Поскольку вы используете Swift 2.0: UIStackView делает именно то, что вы пытаетесь сделать вручную, и значительно проще чем UICollectionView. Предполагая, что вы используете раскадровки, создание прототипа TableViewCell с несколькими вложенными UIStackViews должно делать именно то, что вы хотите. Вам просто нужно убедиться, что UIImages, которые вы вставляете, имеют одинаковое соотношение сторон, если это то, что вы хотите.

Ваш алгоритм очень неэффективен, потому что он должен повторно рисовать, используя несколько преобразований Core Animation, каждое изображение при каждом добавлении или удалении изображения из вашего массива. UIStackView поддерживает динамическое добавление и удаление объектов.

Если вам по какой-то причине необходимо сделать снимок полученного коллажа как UIImage, вы все равно можете сделать это в UIStackView.

Ответ 3

Основываясь на альтернативе 2, представленной Tommie C выше, я создал функцию, которая

  • Всегда заполняет полный прямоугольник, без пробелов в коллаже
  • автоматически определяет количество строк и столбцов (максимум 1 больше nrOfColumns, чем nrOfRows)
  • Чтобы избежать упомянутых выше пространств, все отдельные рисунки рисуются с помощью "Аспект заполнения" (поэтому для некоторых фото это означает, что части будут обрезаны).

Здесь функция:

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

Ответ 4

Если кто-то ищет Objective C-код, этот репозиторий может оказаться полезным.