Изменение цвета некоторых пикселей изображения в Swift

Мой код работает отлично, за исключением одного. Спектакль не на высоте. Чего я пытаюсь добиться, так это того, что у меня есть изображение с несколькими кругами (каждый круг имеет границу другого цвета, чем цвет заливки круга). Когда пользователь касается любого круга, я хочу изменить цвет границы выбранного круга. Это работает нормально, но что не соответствует ожиданиям, так это то, что у нас есть таблица в пользовательском интерфейсе, из которой пользователь может выбрать несколько кругов, например, 6-12 кругов максимум. В этом втором сценарии реализация занимает 3-4 секунды. Ниже я делюсь фрагментом кода, который я использую, если что-то не так, пожалуйста, помогите мне.

ПРИМЕЧАНИЕ: У меня есть 2 изображения: Фронт и Назадпереднее изображение отображается пользователю, и при взаимодействии пользователя с передним изображением цвет точки касания берется из заднего изображения, и если эта точка касания представляет собой любой круг на изображении, то replaceColor вызывается метод.

func replaceColor(sourceColor: [UIColor], withDestColor destColor: UIColor, tolerance: CGFloat) -> UIImage
    {
        // This function expects to get source color(color which is supposed to be replaced)
        // and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
        
//        assert(sourceColor.cgColor.numberOfComponents == 4 && destColor.cgColor.numberOfComponents == 4,
//               "Must be RGBA colorspace")
        
        // *** Allocate bitmap in memory with the same width and size as destination image or back image *** //
        
        let backImageBitmap = self.backImage!.cgImage! // Back Image Bitmap
                
        let bitmapByteCountBackImage = backImageBitmap.bytesPerRow * backImageBitmap.height
        
        let rawDataBackImage = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCountBackImage) // A pointer to the memory block where the drawing is to be rendered

        /// *** A graphics context contains drawing parameters and all device-specific information needed to render the paint on a page to a bitmap image *** //
        
        let contextBackImage = CGContext(data: rawDataBackImage,
                                         width: backImageBitmap.width,
                                         height: backImageBitmap.height,
                                         bitsPerComponent: backImageBitmap.bitsPerComponent,
                                         bytesPerRow: backImageBitmap.bytesPerRow,
                                         space: backImageBitmap.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
                                         bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
        

        // Draw bitmap on created context
        
        contextBackImage!.draw(backImageBitmap, in: CGRect(x: 0, y: 0, width: backImageBitmap.width, height: backImageBitmap.height))

        // Allocate bitmap in memory with the same width and size as source image or front image
        
        let frontImageBitmap = self.frontImage!.cgImage! // Front Image Bitmap

        let bitmapByteCountFrontImage = frontImageBitmap.bytesPerRow * frontImageBitmap.height

        let rawDataFrontImage = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCountFrontImage) // A pointer to the memory block where the drawing is to be rendered

        let contextFrontImage = CGContext(data: rawDataFrontImage,
                                          width: frontImageBitmap.width,
                                          height: frontImageBitmap.height,
                                          bitsPerComponent: frontImageBitmap.bitsPerComponent,
                                          bytesPerRow: frontImageBitmap.bytesPerRow,
                                          space: frontImageBitmap.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
                                          bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)

        // Draw bitmap on created context

        contextFrontImage!.draw(frontImageBitmap, in: CGRect(x: 0, y: 0, width: frontImageBitmap.width, height: frontImageBitmap.height))

        // *** Get color components from replacement color *** \\
        
        let destinationColorComponents = destColor.cgColor.components
        let r2 = UInt8(destinationColorComponents![0] * 255)
        let g2 = UInt8(destinationColorComponents![1] * 255)
        let b2 = UInt8(destinationColorComponents![2] * 255)
        let a2 = UInt8(destinationColorComponents![3] * 255)

        // Prepare to iterate over image pixels
        
        var byteIndex = 0

        while byteIndex < bitmapByteCountBackImage
        {
            // Get color of current pixel
            
            let red = CGFloat(rawDataBackImage[byteIndex + 0]) / 255
            let green = CGFloat(rawDataBackImage[byteIndex + 1]) / 255
            let blue = CGFloat(rawDataBackImage[byteIndex + 2]) / 255
            let alpha = CGFloat(rawDataBackImage[byteIndex + 3]) / 255

            let currentColorBackImage = UIColor(red: red, green: green, blue: blue, alpha: alpha);

            // Compare two colors using given tolerance value
            
            if sourceColor.contains(currentColorBackImage)
            {
                // If the're 'similar', then replace pixel color with given target color

                rawDataFrontImage[byteIndex + 0] = r2
                rawDataFrontImage[byteIndex + 1] = g2
                rawDataFrontImage[byteIndex + 2] = b2
                rawDataFrontImage[byteIndex + 3] = a2
            }
            
            byteIndex = byteIndex + 4;
        }

        // Retrieve image from memory context
        
        let imgref = contextFrontImage!.makeImage()
        let result = UIImage(cgImage: imgref!)

        // Clean up a bit
        
        rawDataBackImage.deallocate()
        rawDataFrontImage.deallocate()
        
        return result
    }

1 ответ
1

Есть две проблемы.

  1. Процесс преобразования в и из UIColor объекты очень дорогие.

  2. Процесс вызова contains на Array равно О (n).

Профилировщик времени покажет вам это:

введите описание изображения здесь

FWIW, я использовал точки интереса OSLog:

import os.log

private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: .pointsOfInterest)

И записал диапазон:

@IBAction func didTapProcessButton(_ sender: Any) {
    os_signpost(.begin, log: log, name: #function)
    let final = replaceColor(frontImage: circleImage, backImage: squareImage, sourceColor: [.blue], withDestColor: .green, tolerance: 0.25)
    os_signpost(.end, log: log, name: #function)
    processedImageView.image = final
}

Затем я мог легко масштабировать именно этот интервал в своем коде с помощью инструмента «Точки интереса». Сделав это, я могу переключиться на инструмент «Профилировщик времени», и он показывает, что 49,3% времени было потрачено на contains и 24,9% времени было проведено в UIColor инициализатор.

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

введите описание изображения здесь

Итак, относительно UIColor выпуск, в Изменить цвет определенных пикселей в UIImageя явно использую UInt32 представление цвета (и иметь struct чтобы обеспечить удобное взаимодействие с этим 32-битным целым числом). Я делаю это, чтобы наслаждаться эффективной целочисленной обработкой и избегать UIColor. В этом случае обработка цветов изображения размером 1080×1080 пикселей занимает 0,03 секунды (исключая UIColor туда-сюда для каждого пикселя).

Большая проблема в том, что contains очень неэффективно. Если вы должны использовать contains своего рода логика (как только вы используете UInt32 представления ваших цветов), я бы предложил использовать Setс O(1) производительностьскорее, чем ArrayO(n) производительность).

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


Независимо от вашей проблемы с производительностью, у вас очень серьезная проблема с памятью. Предоставленный фрагмент кода вычисляет bitmapByteCountFrontImage и bitmapByteCountBackImage как ширина × высота, но должно быть ширина × высота × 4. Ваш контекст использует 4 байта на пиксель, поэтому убедитесь, что вы выделяете свой буфер соответствующим образом.

Лично я не занимаюсь ручным выделением буферов и позволяю CGContext сделай это для меня:

let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

let contextFrontImage = CGContext(data: nil,
                                  width: frontImageBitmap.width,
                                  height: frontImageBitmap.height,
                                  bitsPerComponent: 8,                      // eight bits per component, not source image bits per component
                                  bytesPerRow: frontImageBitmap.width * 4,  // explicitly four bytes per pixel, not source image bytes per row
                                  space:  CGColorSpaceCreateDeviceRGB(),    // explicit color space, not source image color space
                                  bitmapInfo: bitmapInfo)

guard let dataFrontImage = contextFrontImage?.data else {
    print("unable to get front image buffer")
    return nil
}
let rawDataFrontImage = dataFrontImage.assumingMemoryBound(to: UInt8.self)

И, когда вы делаете это, это освобождает вас от ручного освобождения позже (что, когда вы начинаете передавать изображения, очень быстро становится очень сложным).

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


Например, вот представление, которое использует 32-битные целые числа для цветов и заменяет contains логика с арифметическим вычислением и обрабатывает изображение 1080×1080 пикселей на симуляторе в релизной сборке за 0,01 секунды:

/// Replace colors in “front” image on the basis of colors within in the “back” image matching a requested color within a certain tolerance.
///
/// - Parameters:
///   - frontImage: This is the image that will be used for the basis of the returned image (i.e. where `searchColor` was not found in `backImage`.
///   - backImage: The image in which we're going to look for `searchColor`.
///   - searchColor: The color we are looking for in the backImage.
///   - replacementColor: The color we are going to replace it with if found within the specified `tolerance`.
///   - tolerance: The tolerance (in `UInt8`) to used when looking for `searchColor`. E.g. a `tolerance` of 5 when
/// - Returns: The resulting image.

func replaceColor(frontImage: UIImage, backImage: UIImage, searchColor: UIColor, replacementColor: UIColor, tolerance: UInt8) -> UIImage? {
    guard
        let backImageBitmap = backImage.cgImage,
        let frontImageBitmap = frontImage.cgImage
    else {
        print("replaceColor: Unable to get cgImage")
        return nil
    }

    let searchColorRGB = RGBA32(color: searchColor)
    let (searchColorMin, searchColorMax) = searchColorRGB.colors(tolerance: tolerance)
    let replacementColorRGB = RGBA32(color: replacementColor)

    /// Graphics context parameters

    let bitsPerComponent = 8
    let colorspace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = RGBA32.bitmapInfo
    let width = backImageBitmap.width
    let height = backImageBitmap.height

    // back image

    let backImageBytesPerRow = width * 4
    let backImagePixelCount = width * height
    let contextBackImage = CGContext(data: nil,
                                     width: width,
                                     height: height,
                                     bitsPerComponent: bitsPerComponent,
                                     bytesPerRow: backImageBytesPerRow,
                                     space: colorspace,
                                     bitmapInfo: bitmapInfo)

    guard let dataBackImage = contextBackImage?.data else {
        print("replaceColor: Unable to get back image buffer")
        return nil
    }
    let bufferBackImage = dataBackImage.bindMemory(to: RGBA32.self, capacity: width * height)
    contextBackImage!.draw(backImageBitmap, in: CGRect(x: 0, y: 0, width: width, height: height))

    // front image

    let contextFrontImage = CGContext(data: nil,
                                      width: width,
                                      height: height,
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: width * 4,
                                      space: colorspace,
                                      bitmapInfo: bitmapInfo)

    guard let dataFrontImage = contextFrontImage?.data else {
        print("replaceColor: Unable to get front image buffer")
        return nil
    }
    let bufferFrontImage = dataFrontImage.bindMemory(to: RGBA32.self, capacity: width * height)
    contextFrontImage!.draw(frontImageBitmap, in: CGRect(x: 0, y: 0, width: width, height: height))

    // Prepare to iterate over image pixels

    for offset in 0..<backImagePixelCount {
        let color = bufferBackImage[offset]

        // Compare two colors using given tolerance value

        if color.between(searchColorMin, searchColorMax) {
            bufferFrontImage[offset] = replacementColorRGB
        }
    }

    // Retrieve image from memory context

    return contextFrontImage?.makeImage().flatMap {
        UIImage(cgImage: $0)
    }
}

Где

struct RGBA32: Equatable {
    private 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: UIColor) {
        var red:   CGFloat = .zero
        var green: CGFloat = .zero
        var blue:  CGFloat = .zero
        var alpha: CGFloat = .zero

        color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

        self.color = (UInt32(red * 255) << 24) | (UInt32(green * 255) << 16) | (UInt32(blue * 255) << 8) | (UInt32(alpha * 255) << 0)
    }

    func colors(tolerance: UInt8) -> (RGBA32, RGBA32) {
        let red   = redComponent
        let green = greenComponent
        let blue  = blueComponent
        let alpha = alphaComponent

        let redMin   = red   < tolerance ? 0 : red   - tolerance
        let greenMin = green < tolerance ? 0 : green - tolerance
        let blueMin  = blue  < tolerance ? 0 : blue  - tolerance
        let alphaMin = alpha < tolerance ? 0 : alpha - tolerance

        let redMax   = red   > (255 - tolerance) ? 255 : red   + tolerance
        let greenMax = green > (255 - tolerance) ? 255 : green + tolerance
        let blueMax  = blue  > (255 - tolerance) ? 255 : blue  + tolerance
        let alphaMax = alpha > (255 - tolerance) ? 255 : alpha + tolerance

        return (RGBA32(red: redMin, green: greenMin, blue: blueMin, alpha: alphaMin),
                RGBA32(red: redMax, green: greenMax, blue: blueMax, alpha: alphaMax))
    }

    func between(_ min: RGBA32, _ max: RGBA32) -> Bool {
        return
            redComponent   >= min.redComponent   && redComponent   <= max.redComponent &&
            greenComponent >= min.greenComponent && greenComponent <= max.greenComponent &&
            blueComponent  >= min.blueComponent  && blueComponent  <= max.blueComponent
    }

    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
}

И вы бы назвали это так:

let resultImage = replaceColor(frontImage: frontImage, backImage: backImage, searchColor: .blue, replacementColor: .green, tolerance: 5)

В результате (с передним, задним и результирующим изображениями, идущими слева направо):

введите описание изображения здесь

Понятно, что можно реализовать tolerance логику, как хотите, но, надеюсь, это иллюстрирует идею о том, что вырезание UIColor а поиск по коллекции может существенно повлиять на производительность.

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

    — Раджа Саад

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *