如何从AVCapturePhoto生成正确方向的UIImage?

27
我正在调用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: 阅读苹果的照片捕获编程指南(iOS11已过时),我确实发现了一个错误:

  1. 在每次拍摄调用(self.capturePhotoOutput.capturePhoto)时,必须设置与PhotoOutput对象的连接,并将其方向更新为与拍摄瞬间设备方向相匹配。为此,我创建了一个UIDeviceOrientation扩展,并在我创建的snapPhoto()函数中使用它来调用捕获例程,并等待执行didFinishProcessingPhoto委托方法。我添加了代码快照,因为这里的代码示例分隔符似乎没有正确显示它们。 输入图像描述 输入图像描述

更新 2: GitHub上完整项目的链接:https://github.com/agu3rra/Out-Loud


有趣的是,如果您将数据写入文件并使用基于文件的UIImage初始化程序,它似乎也会忽略方向。这可能是一个需要报告给苹果的错误 - rickster
在snapPhoto()例程(UPDATE 1)中添加了连接调用后,它不再忽略方向,但是在我创建的UIImage上返回了不正确的方向值。 - Andre Guerra
1
关于您的方向扩展。苹果已经发布了AVCam更新。有一个类似的扩展。extension UIDeviceOrientation { var videoOrientation: AVCaptureVideoOrientation? { switch self { case .portrait: return .portrait case .portraitUpsideDown: return .portraitUpsideDown case .landscapeLeft: return .landscapeRight case .landscapeRight: return .landscapeLeft default: return nil } } } - Leo Dabus
https://developer.apple.com/library/content/samplecode/AVCam/Introduction/Intro.html - Leo Dabus
5个回答

34

最终更新: 我对这个应用进行了一些实验,并得出以下结论:

  1. kCGImagePropertyOrientation 似乎不会影响应用内捕获图像的方向,只有在每次调用 capturePhoto 方法之前更新你的 photoOutput 连接才会随着设备方向变化。因此:

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.
}
查看调试器生成的图像让我了解了它们是如何生成的,因此我可以推断出所需的旋转(UIImageOrientation),以便将其显示为直立状态。换句话说:更新 UIImageOrientation 可以告诉图像应如何旋转,以便您可以以正确的方向看到它。因此,我得出了以下表格:捕获时应用哪个UIImageOrientation

我不得不更新我的 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
        }
    }
}
这是我的最终委托方法的样子。它以期望的方向显示图像。
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)")
}

3
getAVCaptureVideoOrientationFromDevice 是什么? - Anton Belousov
依赖于您设置的 deviceOrientationOnCapture 属性并不是一个好主意,因为委托可能会在该值已经更改之前或之后返回(例如连续拍摄多张照片)。尝试使用从实际委托调用返回的元数据来解决问题,参见 https://dev59.com/MFYN5IYBdhLWcg3w07Bx#54225440。 - bmjohns
1
@AntonBelousov没有在他的回答中提到,但是可以在他的存储库中找到它:https://github.com/agu3rra/Out-Loud - Stunner
2
如果iPhone的自动旋转关闭了,我们就不能依赖于UIDevice.current.orientation因为即使用户在横向模式下拍照,它始终返回.portrait - RawMean

6

我已经成功地做到了这一点:

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绑定。对于1200万像素图像,BGRA CGImage表示每次调用约为48兆字节。如果您只想在屏幕上呈现CGImage,请使用previewCGImageRepresentation代替。请注意,CGImageRef的物理旋转与主图像的旋转相匹配。 Exif方向未应用。如果您希望在使用UIImage时应用旋转,则可以通过查询照片的metadata [kCGImagePropertyOrientation]值并将其作为方向参数传递给+ [UIImage imageWithCGImage:scale:orientation:]来实现。原始图像始终返回nil的CGImageRepresentation。如果您希望从RAW图像创建CGImageRef,请使用CoreImage框架中的CIRAWFilter。

1
实际上,photo.metadata[kCGImagePropertyOrientation]CGImagePropertyOrientation 而不是 UIImage.Orientation。 (c)https://developer.apple.com/documentation/imageio/cgimagepropertyorientation - SoftDesigner
我认为应该是 (rawValue: orientation.intValue - 1) - mojuba

6
为了使图像方向正确,我们需要在初始化图像时输入正确的UIImage.Orientation。最好使用从photoOutput委托返回的CGImagePropertyOrientation来获取拍摄照片时相机会话的确切方向。唯一的问题是,尽管UIImage.OrientationCGImagePropertyOrientation之间的枚举值相同,但原始值不同。苹果建议使用简单的映射来解决这个问题。

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

我很确定当旋转锁定开启时,这个方法是不起作用的。但其他技术也都不行,你必须启用陀螺仪并跟踪设备本身。真是个巧妙的方法。最近我提交了一个DTS事件,想找出是否有其他方法,但苹果嘲笑了我。 - David H
无法工作:orientationInt始终返回相同的值。 - timbre timbre

2

Andre提供的更新扩展适用于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
AVCapturePhoto 中,我很确定你会找到一个也被称为 CGImagePropertiesmetadata 对象。
在其中,你会找到用于方向的 EXIF 字典,下一步就是根据它来创建图像。
我没有使用过 AVCapturePhotoOutput,但我有一些使用旧方法的经验。
请注意,EXIF 字典在 UIImageOrientation 中映射方式不同。
这里是我写的文章,虽然很久以前写的,但主要原则仍然有效。
这个问题将指向一些实现,它也很老了,我非常确定在最新版本中他们发布了更简单的 API,但它仍然会引导你解决问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接