web-dev-qa-db-fra.com

Comment générer un UIImage à partir de AVCapturePhoto avec une orientation correcte?

J'appelle la méthode déléguée d'AVFoundation pour gérer une capture de photo, mais j'ai des difficultés à convertir le fichier AVCapturePhoto qu'il génère en un UIImage avec l'orientation correcte. Bien que la routine ci-dessous soit couronnée de succès, je reçois toujours un UIImage orienté à droite (UIImage.imageOrientation = 3). Je n'ai aucun moyen de fournir une orientation lorsque j'utilise UIImage (data: image) et que je tente d'abord d'utiliser photo.cgImageRepresentation ()?. TakeRetainedValue () n'aide pas non plus. Veuillez aider.

L'orientation de l'image est essentielle ici car l'image résultante est transmise à un flux de travail de 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
        }
    }
}

UPDATE 1: En lisant le Guide de programmation de capture de photos (obsolète pour iOS11), j'ai réussi à trouver une chose que je faisais mal:

  1. Lors de chaque appel de capture (self.capturePhotoOutput.capturePhoto), vous devez configurer une connexion avec l'objet PhotoOutput et mettre à jour son orientation pour qu'elle corresponde à l'orientation du périphérique au moment où la photo est prise. Pour ce faire, j'ai créé une extension de UIDeviceOrientation et je l'ai utilisée dans la fonction snapPhoto () que j'ai créée pour appeler la routine de capture et attendre que la méthode de délégation didFinishProcessingPhoto soit exécutée. J'ai ajouté un instantané des codes car les délimiteurs d'échantillon de code ici ne semblent pas les afficher correctement . enter image description here enter image description here

Update 2 Lien vers le projet complet sur GitHub: https://github.com/agu3rra/Out-Loud

10
Andre Guerra

Dernière mise à jour: J'ai effectué quelques expériences avec l'application et je suis arrivé aux conclusions suivantes:

  1. kCGImagePropertyOrientation ne semble pas influencer l’orientation de l’image capturée dans votre application et ne varie avec l’orientation du périphérique que si vous mettez à jour votre connexion photoOutput à chaque fois que vous appelez la méthode capturePhoto. Alors:

    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's output connection to match device's 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. Voir les images générées sur le débogueur m’a montré comment elles étaient générées. Je pouvais donc déduire la rotation requise (UIImageOrientation) pour l’afficher à la verticale. En d'autres termes: la mise à jour de UIImageOrientation indique la rotation de l'image pour que vous puissiez la voir dans la bonne orientation. Je suis donc venu à la table suivante:  Which UIImageOrientation to apply according to how the device was at the time of capture

  3. Je devais mettre à jour mon extension UIDeviceOrientation vers un formulaire peu intuitif:

    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. Voici à quoi ressemble ma dernière méthode de délégué. Il affiche l'image dans l'orientation souhaitée.

    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)")
    }
    
14
Andre Guerra

J'ai eu du succès en faisant ceci: 

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 = UIImageOrientation(rawValue: orientation.intValue)!
        let image = UIImage(cgImage: cgImage, scale: 1, orientation: uiOrientation)

}

C'est basé sur ce que Apple mentionne dans sa documentation:

Chaque fois que vous accédez à cette méthode, AVCapturePhoto génère un nouveau fichier CGImageRef. Lorsqu'il est adossé à un conteneur compressé (tel que HEIC), la CGImageRepresentation est décodée paresseusement au besoin. Lorsque soutenu par un format non compressé tel que BGRA, il est copié dans un fichier .__ séparé. tampon de sauvegarde dont la durée de vie n’est pas liée à celle du fichier AVCapturePhoto. Pour une image de 12 mégapixels, un BGRA CGImage représente ~ 48 Mo par appel. Si vous souhaitez uniquement utiliser CGImage pour rendu à l'écran, utilisez plutôt previewCGImageRepresentation . Notez que la rotation physique de CGImageRef correspond à celle de image principale. L'orientation Exif n'a pas été appliquée. Si vous souhaitez appliquer une rotation lorsque vous travaillez avec UIImage, vous pouvez le faire en interrogeant la métadonnée [kCGImagePropertyOrientation] de la photo, et en transmettant comme paramètre d’orientation vers + [UIImage imageWithCGImage: scale: orientation:]. Les images RAW renvoient toujours un CGImageReprésentation de néant. Si vous souhaitez créer un CGImageRef à partir d'un Image RAW, utilisez CIRAWFilter dans le cadre CoreImage.

3
Mark Bridges

À l'intérieur de la AVCapturePhoto, je suis presque sûr que vous trouverez un objet metadata de également appelé CGImageProperties.
À l'intérieur, vous trouverez le dictionnaire EXIF ​​pour l'orientation. La prochaine étape consiste simplement à prendre l'orientation et à créer une image en fonction de celle-ci.
Je n'ai pas l'habitude d'utiliser AVCapturePhotoOutput mais certains en utilisent l'ancienne.
Notez que le dictionnaire EXIF ​​est mappé différemment dans UIImageOrientation.
Voici un article J'ai écrit il y a longtemps, mais le principe de base est toujours valable.
Cette question vous indiquera quelques implémentations, elle est assez ancienne aussi, je suis à peu près sûr que dans la dernière version, elles ont publié une API plus simple, mais cela vous guidera tout de même dans la résolution du problème.

1
Andrea

Extension mise à jour fournie par Andre qui fonctionne avec 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
        }
    }
}
1
codddeer123

Pour créer notre image avec la bonne orientation, nous devons entrer le UIImage.Orientation correct lorsque nous initialisons l'image. 

Il est préférable d’utiliser la variable CGImagePropertyOrientation renvoyée par le délégué photoOutput pour obtenir l’orientation exacte de la session de la caméra lors de la prise de la photo. Le seul problème ici est que, bien que les valeurs enum entre UIImage.Orientation et CGImagePropertyOrientation soient les mêmes, les valeurs brutes ne le sont pas. Apple suggère un mappage simple pour résoudre ce problème.

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

Voici ma mise en œuvre:

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 pour la cartographie

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
    }
}
0
bmjohns