Низкая производительность усреднения CGImage - PullRequest
1 голос
/ 14 марта 2020

Я пытаюсь создать изображение в среднем из нескольких изображений. Я делаю так, чтобы l oop через пиксельное значение 2 фотографий, сложить их вместе и разделить на два. Простая математика. Однако, хотя это работает, это очень медленно (около 23 секунд для усреднения 2x 10-мегапиксельных фотографий на MacBook Pro 15 "2016 года с максимальной спецификацией, по сравнению с гораздо меньшим временем использования API Apples CIFilter для аналогичных алгоритмов). в настоящее время используется это, основываясь на другом вопросе StackOverflow здесь :

static func averageImages(primary: CGImage, secondary: CGImage) -> CGImage? {
        guard (primary.width == secondary.width && primary.height == secondary.height) else {
            return nil
        }

        let colorSpace       = CGColorSpaceCreateDeviceRGB()
        let width            = primary.width
        let height           = primary.height
        let bytesPerPixel    = 4
        let bitsPerComponent = 8
        let bytesPerRow      = bytesPerPixel * width
        let bitmapInfo       = RGBA32.bitmapInfo

        guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            print("unable to create context")
            return nil
        }

        guard let context2 = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            print("unable to create context 2")
            return nil
        }

        context.draw(primary, in: CGRect(x: 0, y: 0, width: width, height: height))

        context2.draw(secondary, in: CGRect(x: 0, y: 0, width: width, height: height))


        guard let buffer = context.data else {
            print("Unable to get context data")
            return nil
        }

        guard let buffer2 = context2.data else {
            print("Unable to get context 2 data")
            return nil
        }

        let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
        let pixelBuffer2 = buffer2.bindMemory(to: RGBA32.self, capacity: width * height)

        for row in 0 ..< Int(height) {
            if row % 10 == 0 {
                print("Row: \(row)")
            }

            for column in 0 ..< Int(width) {
                let offset = row * width + column

                let picture1 = pixelBuffer[offset]
                let picture2 = pixelBuffer2[offset]

                let minR = min(255,(UInt32(picture1.redComponent)+UInt32(picture2.redComponent))/2)
                let minG = min(255,(UInt32(picture1.greenComponent)+UInt32(picture2.greenComponent))/2)
                let minB = min(255,(UInt32(picture1.blueComponent)+UInt32(picture2.blueComponent))/2)
                let minA = min(255,(UInt32(picture1.alphaComponent)+UInt32(picture2.alphaComponent))/2)


                pixelBuffer[offset] = RGBA32(red: UInt8(minR), green: UInt8(minG), blue: UInt8(minB), alpha: UInt8(minA))
            }
        }

        let outputImage = context.makeImage()


        return outputImage
    }

    struct RGBA32: Equatable {
        //private var color: UInt32
        var color: UInt32

        var redComponent: UInt8 {
            return UInt8((color >> 24) & 255)
        }

        var greenComponent: UInt8 {
            return UInt8((color >> 16) & 255)
        }

        var blueComponent: UInt8 {
            return UInt8((color >> 8) & 255)
        }

        var alphaComponent: UInt8 {
            return UInt8((color >> 0) & 255)
        }

        init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
            let red   = UInt32(red)
            let green = UInt32(green)
            let blue  = UInt32(blue)
            let alpha = UInt32(alpha)
            color = (red << 24) | (green << 16) | (blue << 8) | (alpha << 0)
        }

        init(color: UInt32) {
            self.color = color
        }

        static let red     = RGBA32(red: 255, green: 0,   blue: 0,   alpha: 255)
        static let green   = RGBA32(red: 0,   green: 255, blue: 0,   alpha: 255)
        static let blue    = RGBA32(red: 0,   green: 0,   blue: 255, alpha: 255)
        static let white   = RGBA32(red: 255, green: 255, blue: 255, alpha: 255)
        static let black   = RGBA32(red: 0,   green: 0,   blue: 0,   alpha: 255)
        static let magenta = RGBA32(red: 255, green: 0,   blue: 255, alpha: 255)
        static let yellow  = RGBA32(red: 255, green: 255, blue: 0,   alpha: 255)
        static let cyan    = RGBA32(red: 0,   green: 255, blue: 255, alpha: 255)

        static let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue

        static func ==(lhs: RGBA32, rhs: RGBA32) -> Bool {
            return lhs.color == rhs.color
        }
    }

Я не очень опытен, когда дело доходит до работы со значениями пикселей RAW, и, вероятно, есть место для большой оптимизации Объявление RGBA32 может не потребоваться, но опять же я не уверен, как бы я go упростил код. Однако я попытался просто заменить эту структуру на UInt32, так как делю на 2 разделение между четырьмя каналами нарушается, и я получаю неправильный результат (на положительной ноте это сокращает время вычислений примерно до 6 секунд).

Я пытался сбросить альфа-канал (просто жестко закодировать его до 255), а также отбросить проверки безопасности, чтобы никакие значения не превышали 255. Это уменьшило время вычисления t o 19 секунд Тем не менее, это далеко от 6 секунд, к которым я надеялся приблизиться, и было бы также неплохо усреднить альфа-канал.

Примечание: я знаю о CIFilters; однако сначала затемнение изображения, а затем использование фильтра CIAdditionCompositing не работает, поскольку API, предоставляемый Apple, на самом деле использует более сложный алгоритм, чем прямое добавление. Подробнее об этом см. здесь для моего предыдущего кода на эту тему и аналогичный вопрос здесь с тестированием, доказывающим, что API Apple не является прямым добавлением значений пикселей.

** Редактировать: ** Благодаря всем отзывам я теперь смог сделать огромные улучшения. Самая большая разница заключалась в том, чтобы перейти от отладки к выпуску, что значительно сократило время. Затем я смог написать более быстрый код для изменения значений RGBA, исключив для этого необходимость в отдельной структуре. Это изменило время с 23 секунд до 10 (плюс отладка для выпуска улучшений). Код теперь выглядит следующим образом, его также немного переписывают, чтобы он выглядел более читабельным:

static func averageImages(primary: CGImage, secondary: CGImage) -> CGImage? {
    guard (primary.width == secondary.width && primary.height == secondary.height) else {
        return nil
    }

    let colorSpace       = CGColorSpaceCreateDeviceRGB()
    let width            = primary.width
    let height           = primary.height
    let bytesPerPixel    = 4
    let bitsPerComponent = 8
    let bytesPerRow      = bytesPerPixel * width
    let bitmapInfo       = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue

    guard let primaryContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
        let secondaryContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            print("unable to create context")
            return nil
    }

    primaryContext.draw(primary, in: CGRect(x: 0, y: 0, width: width, height: height))
    secondaryContext.draw(secondary, in: CGRect(x: 0, y: 0, width: width, height: height))

    guard let primaryBuffer = primaryContext.data, let secondaryBuffer = secondaryContext.data else {
        print("Unable to get context data")
        return nil
    }

    let primaryPixelBuffer = primaryBuffer.bindMemory(to: UInt32.self, capacity: width * height)
    let secondaryPixelBuffer = secondaryBuffer.bindMemory(to: UInt32.self, capacity: width * height)

    for row in 0 ..< Int(height) {
        if row % 10 == 0 {
            print("Row: \(row)")
        }

        for column in 0 ..< Int(width) {
            let offset = row * width + column

            let primaryPixel = primaryPixelBuffer[offset]
            let secondaryPixel = secondaryPixelBuffer[offset]

            let red = (((primaryPixel >> 24) & 255)/2 + ((secondaryPixel >> 24) & 255)/2) << 24
            let green = (((primaryPixel >> 16) & 255)/2 + ((secondaryPixel >> 16) & 255)/2) << 16
            let blue = (((primaryPixel >> 8) & 255)/2 + ((secondaryPixel >> 8) & 255)/2) << 8
            let alpha = ((primaryPixel & 255)/2 + (secondaryPixel & 255)/2)

            primaryPixelBuffer[offset] = red | green | blue | alpha
        }
    }

    print("Done looping")
    let outputImage = primaryContext.makeImage()

    return outputImage
}

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

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

1 Ответ

0 голосов
/ 15 марта 2020

Есть несколько вариантов:

  1. Распараллелить процедуру:

    Вы можете улучшить производительность с помощью concurrentPerform, чтобы переместить обработку на несколько ядер. В простейшей форме вы можете просто заменить свой внешний for l oop на concurrentPerform:

    extension CGImage {
        func average(with secondImage: CGImage) -> CGImage? {
            guard
                width == secondImage.width,
                height == secondImage.height
            else {
                return nil
            }
    
            let colorSpace       = CGColorSpaceCreateDeviceRGB()
            let bytesPerPixel    = 4
            let bitsPerComponent = 8
            let bytesPerRow      = bytesPerPixel * width
            let bitmapInfo       = RGBA32.bitmapInfo
    
            guard
                let context1 = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
                let context2 = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
                let buffer1 = context1.data,
                let buffer2 = context2.data
            else {
                return nil
            }
    
            context1.draw(self,        in: CGRect(x: 0, y: 0, width: width, height: height))
            context2.draw(secondImage, in: CGRect(x: 0, y: 0, width: width, height: height))
    
            let imageBuffer1 = buffer1.bindMemory(to: UInt8.self, capacity: width * height * 4)
            let imageBuffer2 = buffer2.bindMemory(to: UInt8.self, capacity: width * height * 4)
    
            DispatchQueue.concurrentPerform(iterations: height) { row in   // i.e. a parallelized version of `for row in 0 ..< height {`
                var offset = row * bytesPerRow
                for _ in 0 ..< bytesPerRow {
                    offset += 1
    
                    let byte1 = imageBuffer1[offset]
                    let byte2 = imageBuffer2[offset]
    
                    imageBuffer1[offset] = byte1 / 2 + byte2 / 2
                }
            }
    
            return context1.makeImage()
        }
    }
    

    Обратите внимание, несколько других наблюдений:

    • Поскольку вы выполняете одинаковые вычисления для каждого байта, вы можете еще больше упростить это, избавившись от приведений, смен, масок и т. Д. c. Я также переместил повторяющиеся вычисления из внутреннего l oop.

    • В результате я использую тип UInt8 и перебираю bytesPerRow.

    • FWIW, я определил это как расширение CGImage, которое вызывается как:

      let combinedImage = image1.average(with: image2)
      
    • Сейчас мы проходим через пикселей за строкой в ​​массиве пикселей. Вы можете поиграть с фактическим изменением этого, чтобы обрабатывать несколько пикселей за итерацию concurrentPerform, хотя я не видел существенных изменений, когда сделал это.

    Я обнаружил, что concurrentPerform во много раз быстрее, чем непараллелизированный for l oop. К сожалению, вложенные for l oop являются лишь малой частью общего времени обработки всей функции (например, если вы включите издержки на создание этих двух пиксельных буферов, общая производительность будет только на 40% быстрее, чем без оптимизированное воспроизведение). На хорошо спроектированном MBP 2018 он обрабатывает 10 000 × 10000 пикселей изображения менее чем за полсекунды.

  2. Другой альтернативой является библиотека Accelerate vImage .

    Эта библиотека предлагает широкий спектр процедур обработки изображений и является хорошей библиотекой, с которой можно ознакомиться, если вы собираетесь обрабатывать большие изображения. Я не знаю, если его алгоритм альфа-композитинга математически идентичен алгоритму «усреднения значений байтов», но может быть достаточным для ваших целей. Он обладает тем преимуществом, что сокращает количество вложенных циклов for за один вызов API. Это также открывает двери для гораздо более широкого разнообразия типов составления изображений и процедур манипуляции:

    extension CGImage {
        func averageVimage(with secondImage: CGImage) -> CGImage? {
            let bitmapInfo: CGBitmapInfo = [.byteOrder32Little, CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)]
            let colorSpace = CGColorSpaceCreateDeviceRGB()
    
            guard
                width == secondImage.width,
                height == secondImage.height,
                let format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: bitmapInfo)
            else {
                return nil
            }
    
            guard var sourceBuffer = try? vImage_Buffer(cgImage: self, format: format) else { return nil }
            defer { sourceBuffer.free() }
    
            guard var sourceBuffer2 = try? vImage_Buffer(cgImage: secondImage, format: format) else { return nil }
            defer { sourceBuffer2.free() }
    
            guard var destinationBuffer = try? vImage_Buffer(width: width, height: height, bitsPerPixel: 32) else { return nil }
            defer { destinationBuffer.free() }
    
            guard vImagePremultipliedConstAlphaBlend_ARGB8888(&sourceBuffer, Pixel_8(127), &sourceBuffer2, &destinationBuffer, vImage_Flags(kvImageNoFlags)) == kvImageNoError else {
                return nil
            }
    
            return try? destinationBuffer.createCGImage(format: format)
        }
    }
    

    В любом случае, я обнаружил, что производительность здесь похожа на алгоритм concurrentPerform.

  3. Для хихиканья и усмешки я также попытался отрисовать изображения с помощью CGBitmapInfo.floatComponents и использовал BLAS catlas_saxpby для однострочного вызова для усреднения двух векторов. Это работало хорошо, но, что неудивительно, было медленнее, чем вышеупомянутые целочисленные подпрограммы.

...