EXC_BAD_ACCESS (code = 1) при обновлении UIImage UIImageView - PullRequest
0 голосов
/ 03 мая 2019

Я новичок в iOS и CoreML.У меня очень простой интерфейс с двумя UIImageViews (один должен быть вход, а второй должен быть выход).При нажатии на первое изображение должно обрабатываться нейронной сетью, а вывод должен отображаться во втором.

Однако, когда я пытаюсь загрузить изображение из выходного объекта MLMultiArray и создать из него UIImage, который я могу затем загрузить во второй UIImageView, я получаю EXC_BAD_ACCESS (code = 1).

Я сократил проблему, чтобы вообще не вызывать обработку нейронной сети, а просто пытаться создать новый образ из MLMultiArray.Результат был таким же.

После этого я попытался сгенерировать UIImage из пустого буфера.Изображение создано правильно, но если я пытаюсь обновить UIImageView, чтобы использовать его, я получаю ту же ошибку.

Если я пытаюсь обновить второй UIImageView для другого изображения (например, входного изображения), все работает нормально.

Я предполагаю, что это проблема управления памятью об объекте UIImage, который я создаюно я не могу понять, что я делаю неправильно

class ViewController: UIViewController {

    @IBOutlet weak var out: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func imageTapped(_ sender: UITapGestureRecognizer) {
        let imageView = sender.view as? UIImageView

        if let imageToAnalyse = imageView?.image {
            if let outImg = process(forImage: imageToAnalyse) {
                out.image = outImg
            }
        }
    }

    func process (forImage inImage:UIImage) -> UIImage? {

        let size = CGSize(width: 512, height: 512)
        let mlOut = try? MLMultiArray(shape: [1, size.height, size.width] as [NSNumber], dataType: .float32)
        let newImage = getSinglePlaneImage(inBuffer: mlOut!, width: Int(size.width), height: Int(size.height))

        return newImage
    }

    func getSinglePlaneImage(inBuffer: MLMultiArray, width: Int, height: Int) -> UIImage
    {
        var newImage: UIImage

//        let floatPtr =  inBuffer.dataPointer.bindMemory(to: Float32.self, capacity: inBuffer.count)
//        let floatBuffer = UnsafeBufferPointer(start: floatPtr, count: inBuffer.count)
//        let pixelValues : [UInt8]? = Array(floatBuffer).map({UInt8( ImageProcessor.clamp( (($0) + 1.0) * 128.0, minValue: 0.0, maxValue: 255.0) ) })

        //simulating pixels from MLMultiArray 
        let pixels : [UInt8]? = Array(repeating: 0, count: 512*512)

        var imageRef: CGImage?

        if var pixelValues = pixels {
            let bitsPerComponent = 8
            let bytesPerPixel = 1
            let bitsPerPixel = bytesPerPixel * bitsPerComponent
            let bytesPerRow = bytesPerPixel * width
            let totalBytes = height * bytesPerRow

            imageRef = withUnsafePointer(to: &pixelValues, {
                ptr -> CGImage? in
                var imageRef: CGImage?
                let colorSpaceRef = CGColorSpaceCreateDeviceGray()
                let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(CGBitmapInfo())
                let data = UnsafeRawPointer(ptr.pointee).assumingMemoryBound(to: UInt8.self)
                let releaseData: CGDataProviderReleaseDataCallback = {
                    (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
                }

                if let providerRef = CGDataProvider(dataInfo: nil, data: data, size: totalBytes, releaseData: releaseData) {
                    imageRef = CGImage(width: width,
                                       height: height,
                                       bitsPerComponent: bitsPerComponent,
                                       bitsPerPixel: bitsPerPixel,
                                       bytesPerRow: bytesPerRow,
                                       space: colorSpaceRef,
                                       bitmapInfo: bitmapInfo,
                                       provider: providerRef,
                                       decode: nil,
                                       shouldInterpolate: false,
                                       intent: CGColorRenderingIntent.defaultIntent)
                }


                return imageRef
            })
        }

        newImage = UIImage(cgImage: imageRef!)

        return newImage
    }
}

1 Ответ

1 голос
/ 03 мая 2019

Похоже, ваш код успешно преобразует массив 512x512-float32 в массив 512x512-UInt8, поэтому я пишу этот ответ на основе некомментированной версии вашего кода.(Тем не менее, преобразование недостаточно эффективно и может улучшить ситуацию.)

ОБНОВЛЕНИЕ

Следующее описание не является правильным решением проблемы ОП.Просто хранится для записи.Пожалуйста, перейдите к ОБНОВЛЕННЫЙ КОД внизу этого ответа.

СТАРЫЙ КОД (НЕ правильное решение)

Прежде всего, худшим недостатком в коде являютсяследующие две строки:

imageRef = withUnsafePointer(to: &pixelValues, {

    let data = UnsafeRawPointer(ptr.pointee).assumingMemoryBound(to: UInt8.self)

Первая строка выше передает указатель на [UInt8]?, в Swift [UInt8]? (он же Optional<Array<UInt8>>) является 8-байтовой структурой, а не смежной областью, как C-arrays.

Второй более опасен.ptr.pointee равен [UInt8]?, но доступ к массивам Swift через указатель не гарантируется.А передача массива в UnsafeRawPointer.init(_:) может создать временную область, которая будет освобождена сразу после вызова инициализатора.

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


Я бы написал что-то вроде этого:

func getSinglePlaneImage(inBuffer: MLMultiArray, width: Int, height: Int) -> UIImage {

    //simulating pixels from MLMultiArray
    //...
    let pixelValues: [UInt8] = Array(repeating: 0, count: 1*512*512)

    let bitsPerComponent = 8
    let bytesPerPixel = 1
    let bitsPerPixel = bytesPerPixel * 8
    let bytesPerRow = bytesPerPixel * width
    let totalBytes = height * bytesPerRow

    let imageRef = pixelValues.withUnsafeBytes({bytes -> CGImage? in
        var imageRef: CGImage?
        let colorSpaceRef = CGColorSpaceCreateDeviceGray()
        let bitmapInfo: CGBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)

        let data = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
        let releaseData: CGDataProviderReleaseDataCallback = {_,_,_ in }

        if let providerRef = CGDataProvider(dataInfo: nil, data: data, size: totalBytes, releaseData: releaseData) {
            imageRef = CGImage(width: width,
                               height: height,
                               bitsPerComponent: bitsPerComponent,
                               bitsPerPixel: bitsPerPixel,
                               bytesPerRow: bytesPerRow,
                               space: colorSpaceRef,
                               bitmapInfo: bitmapInfo,
                               provider: providerRef,
                               decode: nil,
                               shouldInterpolate: false,
                               intent: .defaultIntent)
        }


        return imageRef
    })

    let newImage = UIImage(cgImage: imageRef!)

    return newImage
}

Если вы хотите указатель, указывающий на начальный элемент массива, используйте withUnsafeBytes и используйте указатель (на самом деле это UnsafeRawBufferPointer) внутри аргумента замыкания.

Еще один, ваши pixels или pixelValues не должны быть необязательными.


Или же вы можете создать серое изображение с Float32 для каждого пикселя.

func getSinglePlaneImage(inBuffer: MLMultiArray, width: Int, height: Int) -> UIImage {

    //simulating pixels from MLMultiArray
    //...
    let pixelValues: [Float32] = Array(repeating: 0, count: 1*512*512)

    let bitsPerComponent = 32 //<-
    let bytesPerPixel = 4 //<-
    let bitsPerPixel = bytesPerPixel * 8
    let bytesPerRow = bytesPerPixel * width
    let totalBytes = height * bytesPerRow

    let imageRef = pixelValues.withUnsafeBytes({bytes -> CGImage? in
        var imageRef: CGImage?
        let colorSpaceRef = CGColorSpaceCreateDeviceGray()
        let bitmapInfo: CGBitmapInfo = [CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
                          .byteOrder32Little, .floatComponents] //<-
        let data = bytes.baseAddress!.assumingMemoryBound(to: Float32.self)
        let releaseData: CGDataProviderReleaseDataCallback = {_,_,_ in }

        if let providerRef = CGDataProvider(dataInfo: nil, data: data, size: totalBytes, releaseData: releaseData) {
            imageRef = CGImage(width: width,
                               height: height,
                               bitsPerComponent: bitsPerComponent,
                               bitsPerPixel: bitsPerPixel,
                               bytesPerRow: bytesPerRow,
                               space: colorSpaceRef,
                               bitmapInfo: bitmapInfo,
                               provider: providerRef,
                               decode: nil,
                               shouldInterpolate: false,
                               intent: CGColorRenderingIntent.defaultIntent)
        }


        return imageRef
    })

    let newImage = UIImage(cgImage: imageRef!)

    return newImage
}

Оба работают как положено в моем проекте тестирования, но если вы обнаружите что-то не так, пожалуйста,сообщите мне.


ОБНОВЛЕННЫЙ КОД (Надеюсь, это правильное решение)

Я упустил тот факт, что CGDataProvider сохраняет указатель при создании с init(dataInfo:data:size:releaseData:) даже после CGImage создано.Таким образом, на него можно ссылаться после завершения закрытия до withUnsafeBytes, когда оно недопустимо.

В таких случаях лучше использовать CGDataProvider.init(data:).

func getSinglePlaneImage(inBuffer: MLMultiArray, width: Int, height: Int) -> UIImage {
    var newImage: UIImage

    //let floatPtr =  inBuffer.dataPointer.bindMemory(to: Float32.self, capacity: inBuffer.count)
    //let floatBuffer = UnsafeBufferPointer(start: floatPtr, count: inBuffer.count)
    //let pixelValues: Data = Data((floatBuffer.lazy.map{
    //    UInt8(ImageProcessor.clamp((($0) + 1.0) * 128.0, minValue: 0.0, maxValue: 255.0))
    //})

    //simulating pixels from MLMultiArray
    //...
    let pixelValues = Data(count: 1*512*512) // <- ###

    var imageRef: CGImage?

    let bitsPerComponent = 8
    let bytesPerPixel = 1
    let bitsPerPixel = bytesPerPixel * bitsPerComponent
    let bytesPerRow = bytesPerPixel * width

    let colorSpaceRef = CGColorSpaceCreateDeviceGray()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)

    if let providerRef = CGDataProvider(data: pixelValues as CFData) { // <-###
        imageRef = CGImage(width: width,
                           height: height,
                           bitsPerComponent: bitsPerComponent,
                           bitsPerPixel: bitsPerPixel,
                           bytesPerRow: bytesPerRow,
                           space: colorSpaceRef,
                           bitmapInfo: bitmapInfo,
                           provider: providerRef,
                           decode: nil,
                           shouldInterpolate: false,
                           intent: CGColorRenderingIntent.defaultIntent)
    }

    newImage = UIImage(cgImage: imageRef!)

    return newImage
}

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

...