Вопрос:

Сбой приложения при размытии изображения с помощью CIFilter

ios uiimageview core-image cifilter ciimage

101 просмотра

2 ответа

6571 Репутация автора

Поскольку эта функция используется для размытия изображения, я часто получаю отчеты о сбоях с CoreImage:

// Code exactly as in app
extension UserImage {

    func blurImage(_ radius: CGFloat) -> UIImage? {

        guard let ciImage = CIImage(image: self) else {
            return nil
        }

        let clampedImage = ciImage.clampedToExtent()

        let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: [
            kCIInputImageKey: clampedImage,
            kCIInputRadiusKey: radius])

        var filterImage = blurFilter?.outputImage

        filterImage = filterImage?.cropped(to: ciImage.extent)

        guard let finalImage = filterImage else {
            return nil
        }

        return UIImage(ciImage: finalImage)
    }
}

// Code stripped down, contains more in app
class MyImage {

    var blurredImage: UIImage?

    func setBlurredImage() {
        DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {

            let blurredImage = self.getImage().blurImage(100)

            DispatchQueue.main.async {

                guard let blurredImage = blurredImage else { return }

                self.blurredImage = blurredImage
            }
        }
    }
}

По мнению Crashlytics:

  • сбой происходит только для небольшого процента сеансов
  • сбой происходит на разных версиях iOS от 11.x до 12.x
  • 0% устройств были в фоновом состоянии, когда произошел сбой

Мне не удалось воспроизвести сбой, процесс такой:

  1. MyImageViewОбъект (дочерний UIImageView) получаетNotification
  2. Иногда (в зависимости от другой логики) в UIImageпотоке создается размытая версияDispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async
  3. В главном потоке объекты устанавливают UIImageсself.image = ...

Приложение, кажется, падает после шага 3 согласно журналу сбоев ( UIImageView setImage). С другой стороны, сбой CIImageв журнале сбоев указывает, что проблема где-то на шаге 2, где CIFilterиспользуется для создания размытой версии изображения. Примечание: MyImageViewиногда используется в UICollectionViewCell.

Журнал аварии:

EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000000000000

Crashed: com.apple.main-thread
0  CoreImage                      0x1c18128c0 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 2388
1  CoreImage                      0x1c18128c0 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 2388
2  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
3  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
4  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
5  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
6  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
7  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
8  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
9  CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
10 CoreImage                      0x1c18122e8 CI::Context::recursive_render(CI::TileTask*, CI::Node*, CGRect const&, CI::Node*, bool) + 892
11 CoreImage                      0x1c1812f04 CI::Context::render(CI::ProgramNode*, CGRect const&) + 116
12 CoreImage                      0x1c182ca3c invocation function for block in CI::image_render_to_surface(CI::Context*, CI::Image*, CGRect, CGColorSpace*, __IOSurface*, CGPoint, CI::PixelFormat, CI::RenderDestination const*) + 40
13 CoreImage                      0x1c18300bc CI::recursive_tile(CI::RenderTask*, CI::Context*, CI::RenderDestination const*, char const*, CI::Node*, CGRect const&, CI::PixelFormat, CI::swizzle_info const&, CI::TileTask* (CI::ProgramNode*, CGRect) block_pointer) + 608
14 CoreImage                      0x1c182b740 CI::tile_node_graph(CI::Context*, CI::RenderDestination const*, char const*, CI::Node*, CGRect const&, CI::PixelFormat, CI::swizzle_info const&, CI::TileTask* (CI::ProgramNode*, CGRect) block_pointer) + 396
15 CoreImage                      0x1c182c308 CI::image_render_to_surface(CI::Context*, CI::Image*, CGRect, CGColorSpace*, __IOSurface*, CGPoint, CI::PixelFormat, CI::RenderDestination const*) + 1340
16 CoreImage                      0x1c18781c0 -[CIContext(CIRenderDestination) _startTaskToRender:toDestination:forPrepareRender:error:] + 2488
17 CoreImage                      0x1c18777ec -[CIContext(CIRenderDestination) startTaskToRender:fromRect:toDestination:atPoint:error:] + 140
18 CoreImage                      0x1c17c9e4c -[CIContext render:toIOSurface:bounds:colorSpace:] + 268
19 UIKitCore                      0x1e8f41244 -[UIImageView _updateLayerContentsForCIImageBackedImage:] + 880
20 UIKitCore                      0x1e8f38968 -[UIImageView _setImageViewContents:] + 872
21 UIKitCore                      0x1e8f39fd8 -[UIImageView _updateState] + 664
22 UIKitCore                      0x1e8f79650 +[UIView(Animation) performWithoutAnimation:] + 104
23 UIKitCore                      0x1e8f3ff28 -[UIImageView _updateImageViewForOldImage:newImage:] + 504
24 UIKitCore                      0x1e8f3b0ac -[UIImageView setImage:] + 340
25 App                         0x100482434 MyImageView.updateImageView() (<compiler-generated>)
26 App                         0x10048343c closure #1 in MyImageView.handleNotification(_:) + 281 (MyImageView.swift:281)
27 App                         0x1004f1870 thunk for @escaping @callee_guaranteed () -> () (<compiler-generated>)
28 libdispatch.dylib              0x1bbbf4a38 _dispatch_call_block_and_release + 24
29 libdispatch.dylib              0x1bbbf57d4 _dispatch_client_callout + 16
30 libdispatch.dylib              0x1bbbd59e4 _dispatch_main_queue_callback_4CF$VARIANT$armv81 + 1008
31 CoreFoundation                 0x1bc146c1c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
32 CoreFoundation                 0x1bc141b54 __CFRunLoopRun + 1924
33 CoreFoundation                 0x1bc1410b0 CFRunLoopRunSpecific + 436
34 GraphicsServices               0x1be34179c GSEventRunModal + 104
35 UIKitCore                      0x1e8aef978 UIApplicationMain + 212
36 App                         0x1002a3544 main + 18 (AppDelegate.swift:18)
37 libdyld.dylib                  0x1bbc068e0 start + 4

В чем может быть причина аварии?


Обновить

Может быть связано с утечкой памяти CIImage . При профилировании я вижу много CIImageутечек памяти с той же трассировкой стека, что и в журнале сбоев:

Img

Может быть связано с Core Image и утечкой памяти, swift 3.0 . Я только что обнаружил, что изображения были сохранены в массиве в памяти, onReceiveMemoryWarningнеправильно обрабатывались и не очищали этот массив. Таким образом, приложение может зависать при проблемах с памятью в некоторых случаях. Может быть, это решает проблему, я дам обновление здесь.


Обновление 2

Кажется, я смог воспроизвести аварию. Тестирование на физическом устройстве iPhone Xs Max с изображением JPEG размером 5 МБ.

  • При отображении изображения без полноэкранного изображения использование памяти приложением составляет 160 МБ.
  • При отображении размытого изображения в 1/4 от размера экрана, использование памяти составляет 380 МБ.
  • При отображении размытого изображения в полноэкранном режиме использование памяти увеличивается до> 1,6 ГБ, и приложение в большинстве случаев вылетает с:

Сообщение от отладчика: прекращено из-за проблемы с памятью

Я удивлен, что изображение размером 5 МБ может вызвать использование памяти> 1,6 ГБ для «простого» размытия. Должен ли я вручную что-либо освобождать здесь, CIContextи CIImageт. Д., Или это нормально, и мне нужно вручную изменить размер изображения до ~ кБ, прежде чем размыть изображение?

Обновление 3

Добавление нескольких представлений изображений с отображением размытого изображения приводит к тому, что использование памяти увеличивается каждый раз на несколько сотен МБ при добавлении представления изображений до тех пор, пока представление не будет удалено, даже если за один раз видно только 1 изображение. Может быть CIFilter, не предназначен для использования для отображения изображения, потому что он занимает больше памяти, чем само визуализированное изображение.

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

Вот обновленный метод:

func blurImage(_ radius: CGFloat) -> UIImage? {

    guard let ciImage = CIImage(image: self) else {
        return nil
    }

    let clampedImage = ciImage.clampedToExtent()

    let blurFilter = CIFilter(name: "CIGaussianBlur", withInputParameters: [
        kCIInputImageKey: clampedImage,
        kCIInputRadiusKey: radius])

    var filteredImage = blurFilter?.outputImage

    filteredImage = filteredImage?.cropped(to: ciImage.extent)

    guard let blurredCiImage = filteredImage else {
        return nil
    }

    let rect = CGRect(origin: CGPoint.zero, size: size)

    UIGraphicsBeginImageContext(rect.size)
    UIImage(ciImage: blurredCiImage).draw(in: rect)
    let blurredImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return blurredImage
}

Кроме того, спасибо @matt и @FrankSchlegel, которые предположили в комментариях, что высокое потребление памяти можно уменьшить, уменьшив частоту дискретизации изображения перед размытием, что я и сделаю. Удивительно, что даже изображение размером 300x300 пикселей вызывает скачок в использовании памяти на ~ 500 МБ. Учитывая, что 2 ГБ является пределом, где приложение будет прекращено. Я опубликую обновление, как только приложение перейдет к этим обновлениям.

Обновление 4

Я добавил этот код, чтобы уменьшить изображение до максимального размера 300x300px, прежде чем размыть его:

func resizeImageWithAspectFit(_ boundSize: CGSize) -> UIImage {

    let ratio = self.size.width / self.size.height
    let maxRatio = boundSize.width / boundSize.height

    var scaleFactor: CGFloat

    if ratio > maxRatio {
        scaleFactor = boundSize.width / self.size.width

    } else {
        scaleFactor = boundSize.height / self.size.height
    }

    let newWidth = self.size.width * scaleFactor
    let newHeight = self.size.height * scaleFactor

    let rect = CGRect(x: 0.0, y: 0.0, width: newWidth, height: newHeight)

    UIGraphicsBeginImageContext(rect.size)
    self.draw(in: rect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage!
}

Сбои теперь выглядят по-другому, но я не уверен, происходит ли сбой во время понижающей дискретизации или рисования размытого изображения, как описано в обновлении № 3, поскольку оба используют UIGraphicsImageContext:

EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000000000010
Crashed: com.apple.root.user-initiated-qos
0  libobjc.A.dylib                0x1ce457530 objc_msgSend + 16
1  CoreImage                      0x1d48773dc -[CIContext initWithOptions:] + 96
2  CoreImage                      0x1d4877358 +[CIContext contextWithOptions:] + 52
3  UIKitCore                      0x1fb7ea794 -[UIImage drawInRect:blendMode:alpha:] + 984
4  MyApp                          0x1005bb478 UIImage.blurImage(_:) (<compiler-generated>)
5  MyApp                          0x100449f58 closure #1 in MyImage.getBlurredImage() + 153 (UIImage+Extension.swift:153)
6  MyApp                          0x1005cda48 thunk for @escaping @callee_guaranteed () -> () (<compiler-generated>)
7  libdispatch.dylib              0x1ceca4a38 _dispatch_call_block_and_release + 24
8  libdispatch.dylib              0x1ceca57d4 _dispatch_client_callout + 16
9  libdispatch.dylib              0x1cec88afc _dispatch_root_queue_drain + 636
10 libdispatch.dylib              0x1cec89248 _dispatch_worker_thread2 + 116
11 libsystem_pthread.dylib        0x1cee851b4 _pthread_wqthread + 464
12 libsystem_pthread.dylib        0x1cee87cd4 start_wqthread + 4

Вот потоки, используемые для изменения размера и размытия изображения ( blurImage()этот метод описан в обновлении № 3):

class MyImage {

    var originalImage: UIImage?
    var blurredImage: UIImage?

    // Called on the main thread
    func getBlurredImage() -> UIImage {

        DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {

            // Create resized image
            let smallImage = self.originalImage.resizeImageWithAspectFitToSizeLimit(CGSize(width: 1000, height: 1000))

            // Create blurred image
            let blurredImage = smallImage.blurImage()

                DispatchQueue.main.async {

                    self.blurredImage = blurredImage

                    // Notify observers to display `blurredImage` in UIImageView on the main thread
                    NotificationCenter.default.post(name: BlurredImageIsReady, object: nil, userInfo: ni)
                }
            }
        }
    }
}
Автор: Manuel Источник Размещён: 31.07.2019 04:11

Ответы (2)


1 плюс

1860 Репутация автора

Я провел несколько сравнительных тестов и обнаружил, что возможно размывать и отображать очень большие изображения при рендеринге непосредственно в a MTKView, даже если обработка происходит на исходном входном размере. Вот весь код тестирования:

import CoreImage
import MetalKit
import UIKit

class ViewController: UIViewController {

    var device: MTLDevice!
    var commandQueue: MTLCommandQueue!
    var context: CIContext!
    let filter = CIFilter(name: "CIGaussianBlur")!
    let testImage = UIImage(named: "test10")! // 10 MB, 40 MP image
    @IBOutlet weak var metalView: MTKView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.device = MTLCreateSystemDefaultDevice()
        self.commandQueue = self.device.makeCommandQueue()

        self.context = CIContext(mtlDevice: self.device)

        self.metalView.delegate = self
        self.metalView.device = self.device
        self.metalView.isPaused = true
        self.metalView.enableSetNeedsDisplay = true
        self.metalView.framebufferOnly = false
    }

}

extension ViewController: MTKViewDelegate {

    func draw(in view: MTKView) {
        guard let currentDrawable = view.currentDrawable,
              let commandBuffer = self.commandQueue.makeCommandBuffer() else { return }

        let input = CIImage(image: self.testImage)!

        self.filter.setValue(input.clampedToExtent(), forKey: kCIInputImageKey)
        self.filter.setValue(100.0, forKey: kCIInputRadiusKey)
        let output = self.filter.outputImage!.cropped(to: input.extent)

        let drawableSize = view.drawableSize

        // Scale image to aspect-fit view.
        // NOTE: This is a benchmark scenario. Usually you would scale the image to a reasonable processing size
        //       (i.e. close to your output size) _before_ applying expensive filters.
        let scaleX = drawableSize.width / output.extent.width
        let scaleY = drawableSize.height / output.extent.height
        let scale = min(scaleX, scaleY)
        let scaledOutput = output.transformed(by: CGAffineTransform(scaleX: scale, y: scale))

        let destination = CIRenderDestination(mtlTexture: currentDrawable.texture, commandBuffer: commandBuffer)
        // BONUS: You can Quick Look the `task` in Xcode to see what Core Image is actually going to do on the GPU.
        let task = try! self.context.startTask(toRender: scaledOutput, to: destination)

        commandBuffer.present(currentDrawable)
        commandBuffer.commit()

        // BONUS: No need to wait, but you can Quick Look the `info` to see what was actually done during rendering
        //        and to get performance metrics, like the actual number of pixels processed.
        DispatchQueue.global(qos: .background).async {
            let info = try! task.waitUntilCompleted()
        }
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}

}

Для тестового изображения размером 10 МБ (40 мегапикселей!) Во время рендеринга память очень быстро выросла до 800 МБ, что и следовало ожидать. Я даже попробовал изображение размером 30 МБ (~ 74 мегапикселя !!), и оно прошло без проблем, используя 1,3 ГБ памяти.

Когда я масштабировал изображение до места назначения до применения фильтра, память все время оставалась на уровне ~ 60 МБ. Так что это действительно то, что вы должны делать в любом случае. Но учтите, что в этом случае вам нужно изменить радиус гауссова размытия, чтобы добиться того же результата.

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

Я надеюсь, что это применимо к вашему сценарию.

Автор: Frank Schlegel Размещён: 11.08.2019 07:30

0 плюса

353130 Репутация автора

Кажется, это простая проблема с многопоточностью. CIFilter не является потокобезопасным. Вы не можете сформировать цепочку фильтров в одном потоке, а затем визуализировать полученный CIImage в другом потоке. Вы должны ограничиться небольшими изображениями, делать все в основном потоке и выполнять рендеринг явно с помощью графического процессора. Вот что такое Core Image.

Автор: matt Размещён: 17.08.2019 07:12
Вопросы из категории :
32x32