Как нормализовать значения пикселей UIImage в Swift? - PullRequest
1 голос
/ 30 марта 2019

Мы пытаемся нормализовать UIImage, чтобы его можно было правильно передать в модель CoreML.

Способ, которым мы получаем значения RGB для каждого пикселя, - это сначала инициализировать массив [CGFloat], называемый rawData значений для каждого пикселя, так что есть позиция для цветов Красный, Зеленый, Синий иальфа-значение.В bitmapInfo мы получаем необработанные значения пикселей из самого исходного изображения UIimage и проводим их.Используется для заполнения параметра bitmapInfo в context, переменной CGContext.Позже мы будем использовать переменную context для draw a CGImage, которая позже преобразует нормализованный CGImage обратно в UIImage.

Использование вложенного цикла for с итерацией координат x и y, минимальных и максимальных значений цвета пикселей среди всех цветов (найденных в массиве необработанных данных CGFloat) по всем пикселямнайдены.Связанная переменная устанавливается для завершения цикла for, в противном случае она будет иметь ошибку вне диапазона.

range указывает диапазон возможных значений RGB (т. Е. Разницу между максимальным значением цвета и минимальным).

Использование уравнения для нормализации каждого значения пикселя:

A = Image
curPixel = current pixel (R,G, B or Alpha) 
NormalizedPixel = (curPixel-minPixel(A))/range

и аналогичный метод, разработанный для цикла сверху, для анализа массива rawData и изменения цветов каждого пикселя в соответствии с этимнормализация.

Большинство наших кодов:

  1. UIImage для UIColor массив цветов пикселей
  2. Изменить цвет определенногопикселей в UIImage
  3. https://gist.github.com/pimpapare/e8187d82a3976b851fc12fe4f8965789

Мы используем CGFloat вместо UInt8, потому что нормализованные значения пикселей должны быть действительными числами от 0 до 1,не 0 или 1.

func normalize() -> UIImage?{

    let colorSpace = CGColorSpaceCreateDeviceRGB()

    guard let cgImage = cgImage else {
        return nil
    }

    let width = Int(size.width)
    let height = Int(size.height)

    var rawData = [CGFloat](repeating: 0, count: width * height * 4)
    let bytesPerPixel = 4
    let bytesPerRow = bytesPerPixel * width
    let bytesPerComponent = 8

    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue & CGBitmapInfo.alphaInfoMask.rawValue

    let context = CGContext(data: &rawData,
                            width: width,
                            height: height,
                            bitsPerComponent: bytesPerComponent,
                            bytesPerRow: bytesPerRow,
                            space: colorSpace,
                            bitmapInfo: bitmapInfo)

    let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
    context?.draw(cgImage, in: drawingRect)

    let bound = rawData.count

    //find minimum and maximum
    var minPixel: CGFloat = 1.0
    var maxPixel: CGFloat = 0.0

    for x in 0..<width {
        for y in 0..<height {

            let byteIndex = (bytesPerRow * x) + y * bytesPerPixel

            if(byteIndex > bound - 4){
                break
            }
            minPixel = min(CGFloat(rawData[byteIndex]), minPixel)
            minPixel = min(CGFloat(rawData[byteIndex + 1]), minPixel)
            minPixel = min(CGFloat(rawData[byteIndex + 2]), minPixel)

            minPixel = min(CGFloat(rawData[byteIndex + 3]), minPixel)


            maxPixel = max(CGFloat(rawData[byteIndex]), maxPixel)
            maxPixel = max(CGFloat(rawData[byteIndex + 1]), maxPixel)
            maxPixel = max(CGFloat(rawData[byteIndex + 2]), maxPixel)

            maxPixel = max(CGFloat(rawData[byteIndex + 3]), maxPixel)
        }
    }

    let range = maxPixel - minPixel
    print("minPixel: \(minPixel)")
    print("maxPixel : \(maxPixel)")
    print("range: \(range)")

    for x in 0..<width {
        for y in 0..<height {
            let byteIndex = (bytesPerRow * x) + y * bytesPerPixel

            if(byteIndex > bound - 4){
                break
            }
            rawData[byteIndex] = (CGFloat(rawData[byteIndex]) - minPixel) / range
            rawData[byteIndex+1] = (CGFloat(rawData[byteIndex+1]) - minPixel) / range
            rawData[byteIndex+2] = (CGFloat(rawData[byteIndex+2]) - minPixel) / range

            rawData[byteIndex+3] = (CGFloat(rawData[byteIndex+3]) - minPixel) / range

        }
    }

    let cgImage0 = context!.makeImage()
    return UIImage.init(cgImage: cgImage0!)
}

До нормализации мы ожидаем, что диапазон значений пикселей равен 0 - 255, а после нормализации диапазон значений пикселей равен 0 - 1.

Формула нормализацииспособен нормализовать значения пикселей до значений от 0 до 1. Но когда мы пытаемся распечатать (просто добавляем операторы печати, когда мы перебираем значения пикселей) значения пикселей перед нормализацией, чтобы убедиться, что мы получаем необработанные значения пикселейПрямо, мы обнаружили, что диапазон этих значений выключен.Например, значение пикселя имеет значение 3.506e + 305 (больше 255.) Мы думаем, что вначале мы получаем неправильное значение необработанного пикселя.

Мы не знакомы с обработкой изображений в Swift и не уверены в правильности всего процесса нормализации.любая помощь будет оценена!

1 Ответ

0 голосов
/ 30 марта 2019

Пара наблюдений:

  1. Ваш rawData является массивом с плавающей запятой, CGFloat, но ваш контекст не заполняет его данными с плавающей запятой, а скорееUInt8 данные.Если вам нужен буфер с плавающей запятой, создайте контекст с плавающей запятой с помощью CGBitmapInfo.floatComponents и соответственно настройте параметры контекста.Например:

    func normalize() -> UIImage? {
        let colorSpace = CGColorSpaceCreateDeviceRGB()
    
        guard let cgImage = cgImage else {
            return nil
        }
    
        let width = cgImage.width
        let height = cgImage.height
    
        var rawData = [Float](repeating: 0, count: width * height * 4)
        let bytesPerPixel = 16
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 32
    
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.floatComponents.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
    
        guard let context = CGContext(data: &rawData,
                                      width: width,
                                      height: height,
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: bytesPerRow,
                                      space: colorSpace,
                                      bitmapInfo: bitmapInfo) else { return nil }
    
        let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
        context.draw(cgImage, in: drawingRect)
    
        var maxValue: Float = 0
        var minValue: Float = 1
    
        for pixel in 0 ..< width * height {
            let baseOffset = pixel * 4
            for offset in baseOffset ..< baseOffset + 3 {
                let value = rawData[offset]
                if value > maxValue { maxValue = value }
                if value < minValue { minValue = value }
            }
        }
        let range = maxValue - minValue
        guard range > 0 else { return nil }
    
        for pixel in 0 ..< width * height {
            let baseOffset = pixel * 4
            for offset in baseOffset ..< baseOffset + 3 {
                rawData[offset] = (rawData[offset] - minValue) / range
            }
        }
    
        return context.makeImage().map { UIImage(cgImage: $0, scale: scale, orientation: imageOrientation) }
    }
    
  2. Но возникает вопрос, почему вы беспокоитесь о данных с плавающей запятой.Если вы возвращали эти данные с плавающей запятой обратно в модель ML, то я могу представить, что это может быть полезно, но вы просто создаете новое изображение.Поэтому у вас также есть возможность просто извлечь данные UInt8, выполнить вычисления с плавающей запятой, а затем обновить буфер UInt8 и создать из него изображение.Таким образом:

    func normalize() -> UIImage? {
        let colorSpace = CGColorSpaceCreateDeviceRGB()
    
        guard let cgImage = cgImage else {
            return nil
        }
    
        let width = cgImage.width
        let height = cgImage.height
    
        var rawData = [UInt8](repeating: 0, count: width * height * 4)
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 8
    
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
    
        guard let context = CGContext(data: &rawData,
                                      width: width,
                                      height: height,
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: bytesPerRow,
                                      space: colorSpace,
                                      bitmapInfo: bitmapInfo) else { return nil }
    
        let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
        context.draw(cgImage, in: drawingRect)
    
        var maxValue: UInt8 = 0
        var minValue: UInt8 = 255
    
        for pixel in 0 ..< width * height {
            let baseOffset = pixel * 4
            for offset in baseOffset ..< baseOffset + 3 {
                let value = rawData[offset]
                if value > maxValue { maxValue = value }
                if value < minValue { minValue = value }
            }
        }
        let range = Float(maxValue - minValue)
        guard range > 0 else { return nil }
    
        for pixel in 0 ..< width * height {
            let baseOffset = pixel * 4
            for offset in baseOffset ..< baseOffset + 3 {
                rawData[offset] = UInt8(Float(rawData[offset] - minValue) / range * 255)
            }
        }
    
        return context.makeImage().map { UIImage(cgImage: $0, scale: scale, orientation: imageOrientation) }
    }
    

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

    Я проверил это, и на iPhone XS Max это было немного быстрее, чем воспроизведение с плавающей запятой, но занимает четверть памяти (например, изображение размером 2000 × 2000 пикселей занимает 16 МБ с UInt8, но 64 МБ с Float).

  3. Наконец, я должен отметить, что vImage имеет высокооптимизированныйфункция, vImageContrastStretch_ARGB8888, которая делает нечто очень похожее на то, что мы сделали выше.Просто import Accelerate, и тогда вы можете сделать что-то вроде:

    func normalize3() -> UIImage? {
        let colorSpace = CGColorSpaceCreateDeviceRGB()
    
        guard let cgImage = cgImage else { return nil }
    
        var format = vImage_CGImageFormat(bitsPerComponent: UInt32(cgImage.bitsPerComponent),
                                          bitsPerPixel: UInt32(cgImage.bitsPerPixel),
                                          colorSpace: Unmanaged.passRetained(colorSpace),
                                          bitmapInfo: cgImage.bitmapInfo,
                                          version: 0,
                                          decode: nil,
                                          renderingIntent: cgImage.renderingIntent)
    
        var source = vImage_Buffer()
        var result = vImageBuffer_InitWithCGImage(
            &source,
            &format,
            nil,
            cgImage,
            vImage_Flags(kvImageNoFlags))
    
        guard result == kvImageNoError else { return nil }
    
        defer { free(source.data) }
    
        var destination = vImage_Buffer()
        result = vImageBuffer_Init(
            &destination,
            vImagePixelCount(cgImage.height),
            vImagePixelCount(cgImage.width),
            32,
            vImage_Flags(kvImageNoFlags))
    
        guard result == kvImageNoError else { return nil }
    
        result = vImageContrastStretch_ARGB8888(&source, &destination, vImage_Flags(kvImageNoFlags))
        guard result == kvImageNoError else { return nil }
    
        defer { free(destination.data) }
    
        return vImageCreateCGImageFromBuffer(&destination, &format, nil, nil, vImage_Flags(kvImageNoFlags), nil).map {
            UIImage(cgImage: $0.takeRetainedValue(), scale: scale, orientation: imageOrientation)
        }
    }
    

    Хотя в нем используется немного другой алгоритм, его стоит рассмотреть, потому что в моем тесте производительности на моем iPhone XS Max он был более чем в 5 раз быстреекак представление с плавающей запятой.


Несколько несвязанных наблюдений:

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

  2. Вместо использования ширины UIImage ивысота, я использую значения из CGImage.Это важное различие в том случае, если ваши изображения могут не иметь масштаба 1.

  3. Возможно, вы захотите рассмотреть возможность досрочного выхода, если, например, диапазон уже был 0-255 (т.е.нормализация не требуется).

...