Мой код работает отлично, за исключением одного. Спектакль не на высоте. Чего я пытаюсь добиться, так это того, что у меня есть изображение с несколькими кругами (каждый круг имеет границу другого цвета, чем цвет заливки круга). Когда пользователь касается любого круга, я хочу изменить цвет границы выбранного круга. Это работает нормально, но что не соответствует ожиданиям, так это то, что у нас есть таблица в пользовательском интерфейсе, из которой пользователь может выбрать несколько кругов, например, 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 ответ
Есть две проблемы.
Процесс преобразования в и из
UIColor
объекты очень дорогие.Процесс вызова
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) производительностьскорее, чем Array
(с O(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
а поиск по коллекции может существенно повлиять на производительность.
большое спасибо за экономию моего времени!! То, как вы описали каждую проблему и ее решение, было более чем удивительным. Теперь мой код работает как часы после того, как я следовал вашим инструкциям и вашему фрагменту кода.
— Раджа Саад