Как создать UIImage из AVCapturePhoto с правильной ориентацией?

Я вызываю метод делегата AVFoundation для обработки фотоснимка, но мне трудно преобразовать генерируемый AVCapturePhoto в UIImage с правильной ориентацией. Хотя приведенная ниже процедура успешна, я всегда ориентируюсь на правильность UIImage (UIImage.imageOrientation = 3). У меня нет способа обеспечить ориентацию при использовании UIImage(data: image), и попытка первого использования photo.cgImageRepresentation()?.takeRetainedValue() также не помогает. Пожалуйста, помогите.

Ориентация изображения здесь имеет решающее значение, так как полученное изображение передается в рабочий процесс Vision Framework.

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    // capture image finished
    print("Image captured.")
    if let imageData = photo.fileDataRepresentation() {
        if let uiImage = UIImage(data: imageData){
            // do stuff to UIImage
        }
    }
}

ОБНОВЛЕНИЕ 1: Прочитав Apple Руководство по программированию захвата фотографий (устарело для iOS11), мне удалось обнаружить одну вещь, которую я делал неправильно:

  1. При каждом вызове захвата (self.capturePhotoOutput.capturePhoto) необходимо установить соединение с объектом PhotoOutput и обновить его ориентацию, чтобы она соответствовала ориентации устройства в момент съемки. Для этого я создал расширение UIDeviceOrientation и использовал его в функции snapPhoto(), которую я создал, чтобы вызвать процедуру захвата и дождаться выполнения метода делегата didFinishProcessingPhoto. Я добавил моментальный снимок кода, потому что здесь, кажется, разделители примера кода не отображают их правильно. enter image description here enter image description here

Обновление 2 Ссылка на полный проект на GitHub: https://github.com/agu3rra/Out-Loud

Ответ 1

Окончательное обновление: Я провел несколько экспериментов с приложением и пришел к следующим выводам:

  1. kCGImagePropertyOrientation, по-видимому, не влияет на ориентацию захваченного изображения внутри вашего приложения, и он меняется только в зависимости от ориентации устройства, если вы обновляете соединение photoOutput каждый раз, когда собираетесь вызвать метод capturePhoto. Итак:

    func snapPhoto() {
        // prepare and initiate image capture routine
    
        // if I leave the next 4 lines commented, the intented orientation of the image on display will be 6 (right top) - kCGImagePropertyOrientation
        let deviceOrientation = UIDevice.current.orientation // retrieve current orientation from the device
        guard let photoOutputConnection = capturePhotoOutput.connection(with: AVMediaType.video) else {fatalError("Unable to establish input>output connection")}// setup a connection that manages input > output
        guard let videoOrientation = deviceOrientation.getAVCaptureVideoOrientationFromDevice() else {return}
        photoOutputConnection.videoOrientation = videoOrientation // update photo output connection to match device orientation
    
        let photoSettings = AVCapturePhotoSettings()
        photoSettings.isAutoStillImageStabilizationEnabled = true
        photoSettings.isHighResolutionPhotoEnabled = true
        photoSettings.flashMode = .auto
        self.capturePhotoOutput.capturePhoto(with: photoSettings, delegate: self) // trigger image capture. It appears to work only if the capture session is running.
    }
    
  2. Просмотр сгенерированных изображений в отладчике показал мне, как они генерируются, поэтому я мог вывести требуемое вращение (UIImageOrientation), чтобы оно отображалось в вертикальном положении. Другими словами: обновление UIImageOrientation говорит о том, как изображение должно вращаться, чтобы вы могли видеть его в правильной ориентации. Итак, я пришел к следующей таблице: Which UIImageOrientation to apply according to how the device was at the time of capture

  3. Мне пришлось обновить расширение UIDeviceOrientation до довольно неинтуитивной формы:

    extension UIDeviceOrientation {
        func getUIImageOrientationFromDevice() -> UIImageOrientation {
            // return CGImagePropertyOrientation based on Device Orientation
            // This extented function has been determined based on experimentation with how an UIImage gets displayed.
            switch self {
            case UIDeviceOrientation.portrait, .faceUp: return UIImageOrientation.right
            case UIDeviceOrientation.portraitUpsideDown, .faceDown: return UIImageOrientation.left
            case UIDeviceOrientation.landscapeLeft: return UIImageOrientation.up // this is the base orientation
            case UIDeviceOrientation.landscapeRight: return UIImageOrientation.down
            case UIDeviceOrientation.unknown: return UIImageOrientation.up
            }
        }
    }
    
  4. Вот так выглядит мой последний метод делегата. Отображает изображение в ожидаемой ориентации.

    func photoOutput(_ output: AVCapturePhotoOutput,
                                     didFinishProcessingPhoto photo: AVCapturePhoto,
                                     error: Error?)
    {
        // capture image finished
        print("Image captured.")
    
        let photoMetadata = photo.metadata
        // Returns corresponting NSCFNumber. It seems to specify the origin of the image
        //                print("Metadata orientation: ",photoMetadata["Orientation"])
    
        // Returns corresponting NSCFNumber. It seems to specify the origin of the image
        print("Metadata orientation with key: ",photoMetadata[String(kCGImagePropertyOrientation)] as Any)
    
        guard let imageData = photo.fileDataRepresentation() else {
            print("Error while generating image from photo capture data.");
            self.lastPhoto = nil; self.controller.goToProcessing();
            return
    
        }
    
        guard let uiImage = UIImage(data: imageData) else {
            print("Unable to generate UIImage from image data.");
            self.lastPhoto = nil; self.controller.goToProcessing();
            return
    
        }
    
        // generate a corresponding CGImage
        guard let cgImage = uiImage.cgImage else {
            print("Error generating CGImage");self.lastPhoto=nil;return
    
        }
    
        guard let deviceOrientationOnCapture = self.deviceOrientationOnCapture else {
            print("Error retrieving orientation on capture");self.lastPhoto=nil;
            return
    
        }
    
        self.lastPhoto = UIImage(cgImage: cgImage, scale: 1.0, orientation: deviceOrientationOnCapture.getUIImageOrientationFromDevice())
    
        print(self.lastPhoto)
        print("UIImage generated. Orientation:(self.lastPhoto.imageOrientation.rawValue)")
        self.controller.goToProcessing()
    }
    
    
    func photoOutput(_ output: AVCapturePhotoOutput, 
                       willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) 
                       {
        print("Just about to take a photo.")
        // get device orientation on capture
        self.deviceOrientationOnCapture = UIDevice.current.orientation
        print("Device orientation: \(self.deviceOrientationOnCapture.rawValue)")
    }
    

Ответ 2

Я имел успех, делая это:

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {

        let cgImage = photo.cgImageRepresentation()!.takeRetainedValue()
        let orientation = photo.metadata[kCGImagePropertyOrientation as String] as! NSNumber
        let uiOrientation = UIImage.Orientation(rawValue: orientation.intValue)!
        let image = UIImage(cgImage: cgImage, scale: 1, orientation: uiOrientation)

}

Это основано на том, что Apple упоминает в своих документах:

Каждый раз, когда вы получаете доступ к этому методу, AVCapturePhoto генерирует новый CGImageRef. При поддержке сжатого контейнера (такого как HEIC) CGImageRepresentation декодируется лениво по мере необходимости. При поддержке несжатого формата, такого как BGRA, он копируется в отдельный резервный буфер, время жизни которого не связано с временем жизни AVCapturePhoto. Для 12-мегапиксельного изображения BGRA CGImage представляет ~ 48 мегабайт на вызов. Если вы собираетесь использовать CGImage только для визуализации на экране, используйте вместо него previewCGImageRepresentation. Обратите внимание, что физическое вращение CGImageRef совпадает с вращением основного изображения. Exif ориентация не была применена. Если вы хотите применить вращение при работе с UIImage, вы можете сделать это, запросив значение метаданных фотографии [kCGImagePropertyOrientation] и передав его в качестве параметра ориентации в + [UIImage imageWithCGImage: scale: direction:]. Изображения RAW всегда возвращают CGImageRepresentation nil. Если вы хотите создать CGImageRef из изображения RAW, используйте CIRAWFilter в платформе CoreImage.

Ответ 3

Внутри AVCapturePhoto Im довольно уверен, что вы найдете объект metadata так называемого CGImageProperties.
Внутри этого слова вы найдете словарь EXIF ​​для ориентации. Следующим шагом является только ориентация и создание изображения в соответствии с этим.
У меня нет опыта использования AVCapturePhotoOutput, но у меня есть использование старого способа.
Обратите внимание, что словарь EXIF ​​отображается по-разному в UIImageOrientation.
Вот статья статья, которую я написал много лет назад, но основной принцип все еще действителен.
Этот
question укажет вам на некоторые реализации, это тоже довольно старый, я уверен, что в последней версии они выпустили более простой API, но он все равно будет направлять вы должны решить проблему.

Ответ 4

Обновленное расширение, предоставленное Андре, которое работает с Swift 4.2:

import Foundation
import UIKit

extension UIDeviceOrientation {
    var imageOrientation: UIImage.Orientation {
        switch self {
        case .portrait, .faceUp:                return .right
        case .portraitUpsideDown, .faceDown:    return .left
        case .landscapeLeft:                    return .up
        case .landscapeRight:                   return .down
        case .unknown:                          return .up
        }
    }
}

Ответ 5

Чтобы создать наше изображение с правильной ориентацией, нам нужно ввести правильный UIImage.Orientation когда мы инициализируем изображение.

Лучше всего использовать CGImagePropertyOrientation которая возвращается от делегата photoOutput, чтобы получить точную ориентацию, в которой находился сеанс камеры, когда был сделан снимок. Единственная проблема здесь в том, что, хотя значения перечисления между UIImage.Orientation и CGImagePropertyOrientation одинаковы, необработанные значения не являются. Apple предлагает простое сопоставление, чтобы исправить это.

https://developer.apple.com/documentation/imageio/cgimagepropertyorientation

Вот моя реализация:

AVCapturePhotoCaptureDelegate

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let _ = error {
            // Handle Error
        } else if let cgImageRepresentation = photo.cgImageRepresentation(),
            let orientationInt = photo.metadata[String(kCGImagePropertyOrientation)] as? UInt32,
            let imageOrientation = UIImage.Orientation.orientation(fromCGOrientationRaw: orientationInt) {

            // Create image with proper orientation
            let cgImage = cgImageRepresentation.takeUnretainedValue()
            let image = UIImage(cgImage: cgImage,
                                scale: 1,
                                orientation: imageOrientation)
        }
    }

Расширение для картирования

extension UIImage.Orientation {

    init(_ cgOrientation: CGImagePropertyOrientation) {
        // we need to map with enum values becuase raw values do not match
        switch cgOrientation {
        case .up: self = .up
        case .upMirrored: self = .upMirrored
        case .down: self = .down
        case .downMirrored: self = .downMirrored
        case .left: self = .left
        case .leftMirrored: self = .leftMirrored
        case .right: self = .right
        case .rightMirrored: self = .rightMirrored
        }
    }


    /// Returns a UIImage.Orientation based on the matching cgOrientation raw value
    static func orientation(fromCGOrientationRaw cgOrientationRaw: UInt32) -> UIImage.Orientation? {
        var orientation: UIImage.Orientation?
        if let cgOrientation = CGImagePropertyOrientation(rawValue: cgOrientationRaw) {
            orientation = UIImage.Orientation(cgOrientation)
        } else {
            orientation = nil // only hit if improper cgOrientation is passed
        }
        return orientation
    }
}