Как создать 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), мне удалось обнаружить одну вещь, которую я делал неправильно:
- При каждом вызове захвата (
self.capturePhotoOutput.capturePhoto
) необходимо установить соединение с объектом PhotoOutput
и обновить его ориентацию, чтобы она соответствовала ориентации устройства в момент съемки. Для этого я создал расширение UIDeviceOrientation
и использовал его в функции snapPhoto()
, которую я создал, чтобы вызвать процедуру захвата и дождаться выполнения метода делегата didFinishProcessingPhoto
. Я добавил моментальный снимок кода, потому что здесь, кажется, разделители примера кода не отображают их правильно.
![enter image description here]()
Обновление 2
Ссылка на полный проект на GitHub: https://github.com/agu3rra/Out-Loud
Ответы
Ответ 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.
}
Просмотр сгенерированных изображений в отладчике показал мне, как они генерируются, поэтому я мог вывести требуемое вращение (UIImageOrientation
), чтобы оно отображалось в вертикальном положении. Другими словами: обновление UIImageOrientation
говорит о том, как изображение должно вращаться, чтобы вы могли видеть его в правильной ориентации. Итак, я пришел к следующей таблице:
![Which UIImageOrientation to apply according to how the device was at the time of capture]()
Мне пришлось обновить расширение 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)")
}
Ответ 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
}
}