WWDC21 Session 10160 - Capture and process ProRAW images

Apple 设备在图片拍摄和处理上已经有了一段不短的发展历程了。从早期支持处理 JPEG 和 HEIC,到 iOS 10 开始支持拍摄和编辑 Bayer Raw(但是 iOS 系统相机并没有支持 Raw 拍摄,而是第三方相机 App 支持)。而去年的 iPhone12 Pro 系列在 iOS 14.3 起支持了 Apple ProRAW。这篇文章将会介绍开发者应该如何适配 ProRaw 的拍摄、保存、处理、显示等一系列流程。

Apple ProRaw 简介

JPEG 和 HEIC,都属于有损压缩过的图片,而 Raw 相当于照片的原始数据。

压缩格式(JPEG、HEIC):

RAW:

而 Apple 的 ProRaw,结合了两者的优点

兼容性

ProRaw 采用标准的 Adobe DNG 文件格式进行存储。DNG 是一种兼容多家相机厂商 Raw 格式转换的公开通用标准格式,兼容性很好:

  1. 软件支持:大部分修图软件,例如 Adobe Lightroom 等都支持
  2. 开发支持:Apple 的 ImageIO 和 Core Image 框架都支持处理
  3. 系统支持:旧版本的 iOS 和 macOS 都支持
  4. 包含了全像素的 JPEG 预览图

图像质量

  1. 包含线性的 Scene referred
  2. 包含多重曝光和图像融合信息
  3. 低压缩的 12-bit RGB
  4. 14 档的动态范围
  5. 文件大小在 10-40 mb 之间

关于 ProRaw 背后一些技术细节,可以参考 Understanding ProRAW

观感

  1. ProRAW 图像的观感 HEIC 差不多
  2. 通过在 DNG 中嵌入一些特殊标签来保存这些信息

详情可以参考 Adobe DNG 1.4.0 白皮书

其中 ProRaw 还用到 2021.4 才发布的 DNG 1.6.0 的一部分新标签

AVCapture 拍摄

在 AVFoundation 的拍摄 API 中,新增了拍摄 ProRaw 图像的支持。

可以在 Apple 官方的 相机 Demo 的基础上尝试新的 API。

整体上,ProRaw 比普通的 Raw 在拍摄的支持上会完善不少,包括支持更多的镜头,还能带上场景信息等。

1. 设置 AVCaptureSesion 和 AVCaptureDevice

let session = AVCaptureSession()
session.beginConfiguration()
// 1. 设置为 Photo
session.sessionPreset = .photo 

let device: AVCaptureDevice = ... // 根据设备找到对应可用的镜头
// 2. 找到格式是否支持
guard let format = device.formats.first(where: { $0.isHighestPhotoQualitySupported }) else {
	//...
}

do {
	try device.lockForConfiguration()
  // 3. 设置格式
	device.activeFormat = format
	device.unlockForConfiguration()
} catch {   
  //...
}

2. 设置 AVCaptureOutput

/// AVCapturePhotoOutput
let photoOutput = AVCapturePhotoOutput()
// 如果支持 RroRaw 输出,则打开
photoOutput.isAppleProRAWEnabled = photoOutput.isAppleProRAWSupported

AVCapturePhotoOutput 的 maxPhotoQualityPrioritization 质量等级,可在速度和质量之间取舍,详情见 WWDC2021 - Capture high-quality photos using video formats

3. ProRaw 拍摄的特殊配置

// 1. 找到支持的像素格式
guard let pixelFormat = photoOutput.availablePhotoPixelFormatTypes.first(where: { AVCapturePhotoOutput.isAppleProRAWPixelFormat($0) }) 
else { 
  //... 
}

// 2. (可选)找到支持的压缩类型
guard let processedPhotoCodecType = photoOutput.availablePhotoCodecTypes.first 
else {
  //...
}

// 3. 创建拍摄设置,如果需要压缩类型就传入
let photoSettings = AVCapturePhotoSettings(rawPixelFormatType: proRawPixelFormat,	processedFormat: [AVVideoCodecKey: processedPhotoCodecType])

// 4. 设置缩略图的编解码参数,大部分情况下建议使用如下配置
guard let thumbnailPhotoCodecType = photoSettings.availableRawEmbeddedThumbnailPhotoCodecTypes.first 
else {
	//...
}
            
let dimensions = device.activeFormat.highResolutionStillImageDimensions
photoSettings.rawEmbeddedThumbnailPhotoFormat = [
  AVVideoCodecKey: thumbnailPhotoCodecType,
  AVVideoWidthKey: dimensions.width,
  AVVideoHeightKey: dimensions.height,
]

// 5. 设置 photoQualityPrioritization,但这里不能大于 AVCapturePhotoOutput 的 maxPhotoQualityPrioritization(比如 output 设置了 .balanced,这里就只能设置 .balanced 或 .speed)

photoSettings.photoQualityPrioritization = .quality

// 6. (可选)设置用于预览的像素格式
if let previewPhotoPixelFormatType = photoSettings.availablePreviewPhotoPixelFormatTypes.first {
	photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType]
}

在这三步后,就可以和普通照片一样调用 AVCapturePhotoOutput 的方法进行拍摄了。

photoOutput.capturePhoto(with: photoSettings, delegate: delegate)

4. 接受拍摄的 ProRaw

遵循 AVCapturePhotoCaptureDelegate,可以在拍摄后收到对应的回调。

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
	guard error == nil else { return }
  if let preview = photo.previewPixelBuffer {
    // 比如用 photo.previewCGImageRepresentation() 进行预览
	}
  
  // 如果设置了压缩格式,这个方法会调用两次,通过 isRawPhoto 来区分是压缩格式还是 Raw 格式
	if photo.isRawPhoto {
		if let data = photo.fileDataRepresentation() {
			// 处理 DNG 数据,比如保存到相册
		}
            
		if let pixelBuffer = photo.pixelBuffer {
			// 处理 pixel 数据
		}
	}
}

ProRaw 上还带有基于场景信息的语义分割蒙版(Semantic Segmentation Mattes),但是需要通过 Core Image/Image IO 进行处理。

语义分割蒙版的介绍可参考 WWDC2019 - Introducing Photo Segmentation Mattes

如果需要对 DNG 文件做一些自定义,可以自定义处理器并遵循 AVCapturePhotoFileDataRepresentationCustomizer,再调用 fileDataRepresentation(with:) 使用自定义的处理器。

比如下面这个处理器就将压缩设置改为 8bit,90% 质量。

class AppleProRawCustomizer: NSObject, AVCapturePhotoFileDataRepresentationCustomizer {
    func replacementAppleProRAWCompressionSettings(for photo: AVCapturePhoto,
                                                   defaultSettings: [String : Any],
                                                   maximumBitDepth: Int
    ) -> [String : Any] {
        [
            AVVideoAppleProRAWBitDepthKey: min(8, maximumBitDepth),
            AVVideoQualityKey: 0.9,
        ]
    }
}

// 将回调方法中获取 DNG 数据的方法改写
let data = photo.fileDataRepresentation(with: AppleProRawCustomizer()) 

Photokit 保存和获取

保存 ProRaw 到相册和普通的照片没有任何区别,都是使用 PhotoKit 的 PHAssetCreationRequest

iOS 15 的 PHAssetCollectionSubtype 新增了枚举类型 smartAlbumRAW,以便开发者 fetchAssetCollections 时可以指定直接查询 RAW 的 PHAssetCollection

处理 Raw 格式的 PHAssetResource

Raw 的 PHAsset 可能有 alternatePhoto 类型的 PHAssetResource

这是由于某些资源可能是从单反相机拷贝来的,有些单反相机拍摄的 Raw 资源包含了 JPEG 和加上 Raw。

这会导致文件占用空间增加,可移植性降低,并且可能会导致用户体验混乱。

ProRaw 不建议以这种方式存储,而是建议通过上文拍摄时设置,将全尺寸的 JPEG 预览图嵌入进 DNG 文件中。

因此完整的 PHAssetResource 处理如下:

let resources = PHAssetResource.assetResources(for: asset)
for resource in resources {
  	// 过滤类型
    if resource.type == .photo || resource.type == .alternatePhoto {
      	// 过滤通用类型标识符
        if let resourceUTType = UTType(resource.uniformTypeIdentifier),
           resourceUTType.conforms(to: .rawImage) {
            let resourceManager = PHAssetResourceManager.default().requestData(for: resource, options: nil) { data in
                    
            } completionHandler: { error in
                    
            }
        }
    }
}

CoreImage 处理和展示

生成 CIImage

// iOS 15 以前
// 获取预览图片
let isrc = CGImageSourceCreateWithURL(url as CFURL, nil)!
let cgImage = CGImageSourceCreateThumbnailAtIndex(isrc, 0, nil)!
return CIImage(cgImage: cgImage)

// 获取某个语义分割蒙板图片
return CIImage(contentsOf: url, options: [.auxiliarySemanticSegmentationSkinMatte : true])

// 如果只需要用来展示,不修改
return CIImage(contentsOf: url, options: nil)

// 需要编辑
let rawFilter = CIFilter(imageURL: url, options: nil)
return rawFilter?.outputImage

// iOS 15 新增
// 获取预览图片
let filter = CIRAWFilter(imageURL: url)
return filter?.previewImage

// 获取某个予以分割蒙版图片
return filter?.semanticSegmentationSkinMatte

应用常用调整

// iOS 15 以前
func getAdjustedRawImage(url: URL) -> CIImage? {
    let rawFilter = CIFilter(imageURL: url, options: nil)
    
  	// 设置对应的 key/value 值
    rawFilter?.setValue(value, forKey: CIRAWFilterOption.key.rawValue)
    
    return rawFilter?.outputImage
}

// iOS 15 新增
func getAdjustedRawImage(url: URL) -> CIImage? {
    let rawFilter = CIRAWFilter(imageURL: url)
    
 		// 设置对应 key 名字的属性为 value 值,强类型,更 Swift
    rawFilter?.key = value
    
    return rawFilter?.outputImage
}

比如设置不同的 localToneMapAmount

获得线性 Scene-Referred 图

// 将调整全部设置为默认值
rawFilter.baselineExposure = 0
rawFilter.shadowBias = 0
rawFilter.boostAmount = 0
rawFilter.localToneMapAmount = 0
rawFilter.isGamutMappingEnabled = false
// 此时获取的 outputImage 就是未经处理的线性图
let linearRawImage = rawFilter.outputImage

这个线性图可以设置为其他 CIFilter 上的输入,来对 Scene-Referred 图进行计算,也可以使用它进行渲染。

可以看到,左边是默认输出,左边太阳部分和右边天空亮度差异比较小,而线性图的差异就大了不少,更符合真实的明暗关系(真实的差异还要比记录的信息大得多),而这也给后期调整保留了更多的信息,有更大的调整空间。

保存成其他文件格式

// iOS 15 以前
try ciContext.writeHEIFRepresentation(of: rawFilter.outputImage!,
                                      to: url,
                                      format: .RGBA8,
                                      colorSpace: .init(name: CGColorSpace.displayP3)!,
                                      options: [:])

// iOS 15 新增,可以支持保存成 10bit 的 HEIC
try ciContext.writeHEIF10Representation(of: rawFilter.outputImage!,
                                        to: url,
                                        colorSpace: .init(name: CGColorSpace.displayP3)!,
                                        options: [:])

EDR 显示在 Mac 上

默认情况下 CIRawFilter 的输出是 SDR 的,需要通过一些选项设置,就能以 EDR 的方式输出。

推荐使用 MetalKit 的 MTKView 来显示 ProRaw 的 CIImage

使用 CoreImage 渲染最佳实践可以参考 WWDC2020 - Optimize the Core Image pipeline for your video app

// 在 MTKView 初始化的时候设置以下参数
colorPixelFormat = MTLPixelFormat.rgba16Float
if let caml = layer as? CAMetalLayer {
	caml.wantsExtendedDynamicRangeContent = true
}

// 在 CIRawFilter 渲染的时候设置以下参数
rawFilter?.extendedDynamicRangeAmount = 1.0

总结

ProRAW 从实现上看,走得并不是完全类似专业单反的原始信息 Raw 的道路,而是通过 Apple 非常擅长的计算摄影流程,给用户带来媲美 Raw 的高动态范围和丰富的原始色彩信息,同时又能配合软硬件的 Apple 生态产物。iOS 15 上 ProRaw 的全流程开发支持,也意味着对于音视频/图像编辑领域等 SDK 开发者,需要提前对行业头部推出的标准进行熟悉和适配,才能带给用户完整的用户体验。