WWDC Quick Look 💓 By SwiftGGTeam
Capture and process ProRAW images

Capture and process ProRAW images

观看原视频

Highlight

Apple 把 ProRAW 的采集、图库存储、RAW 资源读取和 Core Image 编辑接入到 AVFoundation、PhotoKit 与 Core Image,开发者可以在自己的相机和修图 App 中处理带有计算摄影结果的 DNG RAW 文件。

核心内容

相机 App 常见的两条路都不省心。HEIC 和 JPEG 适合立即显示,文件小,系统已经做完 Smart HDR、Deep Fusion 或夜间模式融合。摄影用户后期调整时,余量有限。Bayer RAW 保留更多传感器数据,后期空间大,但开发者要处理去马赛克、噪声和渲染速度问题。

ProRAW 解决的是这条生产链。它仍然放在标准 Adobe DNG 文件里,系统框架 ImageIO 和 Core Image 已有基础支持。它的像素来自线性化、场景参考的数据,可由多帧曝光融合生成,默认用 12-bit RGB 无损压缩,文件通常在 10MB 到 40MB 之间。

对开发者来说,变化发生在三个位置。拍摄阶段用 AVFoundation(音视频采集框架)请求 ProRAW 像素格式;入库阶段用 PhotoKit(照片图库框架)把 DNG 作为主要照片资源保存;编辑阶段用 Core Image(图像处理框架)加载 CIRAWFilter,调整曝光、白平衡、锐化、本地色调映射,必要时取出线性场景数据做分析。

从拍摄开始

以前做 RAW 相机,开发者通常要在 Bayer RAW 和处理后 JPEG 之间取舍。ProRAW 让 App 得到一个 DNG 文件,同时保留 Apple 计算摄影的默认观感。它还可以在文件里带全尺寸 JPEG 预览,减少 RAW + JPEG 双资源带来的图库管理成本。

从图库继续

用户的照片库里可能已经有 RAW 文件。iOS 15 新增 .smartAlbumRAW,开发者可以直接取到系统 Photos 里的 RAW 智能相册,再用 PHAssetResourceManager 读取真正的 RAW 数据。

从编辑落地

Core Image 在 iOS 15 和 macOS 12 增加了更适合 Swift 的 CIRAWFilter API。开发者不必只用字符串 key 调整 RAW 滤镜,可以直接设置属性。ProRAW 的默认观感来自 DNG 标签;关掉这些默认输入后,还能拿到线性场景参考输出,用于直方图、分析或自定义渲染。

详细内容

1. 配置支持 ProRAW 的采集管线

07:52)ProRAW 只支持最高质量照片格式。最简单的做法是在 AVCaptureSession 上使用 .photo preset。Apple 建议在 session 启动前启用 photoOutput.isAppleProRAWEnabled,否则会触发耗时的管线重配置。

import AVFoundation

private let session = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()

private func configureSessionAndPhotoOutput() {
    session.beginConfiguration()
    session.sessionPreset = .photo

    photoOutput.isHighResolutionCaptureEnabled = true
    photoOutput.isAppleProRAWEnabled = photoOutput.isAppleProRAWSupported
    photoOutput.maxPhotoQualityPrioritization = .quality

    session.commitConfiguration()
}

关键点:

  • AVCaptureSession() 创建相机采集会话。
  • AVCapturePhotoOutput() 负责静态照片输出。
  • session.beginConfiguration() 开始批量修改会话配置。
  • session.sessionPreset = .photo 选择支持最高质量照片的 preset。
  • photoOutput.isHighResolutionCaptureEnabled = true 开启高分辨率照片采集。
  • photoOutput.isAppleProRAWEnabled = photoOutput.isAppleProRAWSupported 只在设备支持时启用 ProRAW。
  • photoOutput.maxPhotoQualityPrioritization = .quality 把最高照片质量优先级设为质量优先。
  • session.commitConfiguration() 提交配置。

09:26)启用 ProRAW 后,availableRawPhotoPixelFormatTypes 会包含 ProRAW 像素格式。用 AVCapturePhotoOutput.isAppleProRAWPixelFormat(_:) 区分 ProRAW 和 Bayer RAW。

import AVFoundation

func firstProRAWPixelFormat(from photoOutput: AVCapturePhotoOutput) -> OSType? {
    return photoOutput.availableRawPhotoPixelFormatTypes.first { pixelFormat in
        AVCapturePhotoOutput.isAppleProRAWPixelFormat(pixelFormat)
    }
}

关键点:

  • availableRawPhotoPixelFormatTypes 返回当前输出支持的 RAW 像素格式。
  • first { pixelFormat in ... } 从列表里找第一个 ProRAW 格式。
  • isAppleProRAWPixelFormat(pixelFormat) 判断该格式是否属于 Apple ProRAW。
  • 函数返回 OSType?,找不到时返回 nil,调用方应把它当作设备或格式不支持处理。

2. 创建 ProRAW 拍摄设置

10:09)只拍 ProRAW 时,AVCapturePhotoSettings(rawPixelFormatType:) 就够了。如果还要同时得到处理后的照片,可以提供 processedFormat。session 提到,这会让 App 处理多个资源,PhotoKit 里也更难跟踪。

import AVFoundation

func makeProRAWSettings(
    photoOutput: AVCapturePhotoOutput,
    proRawPixelFormat: OSType
) -> AVCapturePhotoSettings? {
    guard let processedPhotoCodecType = photoOutput.availablePhotoCodecTypes.first else {
        return nil
    }

    let photoSettings = AVCapturePhotoSettings(
        rawPixelFormatType: proRawPixelFormat,
        processedFormat: [AVVideoCodecKey: processedPhotoCodecType]
    )

    return photoSettings
}

关键点:

  • availablePhotoCodecTypes.first 取当前输出支持的处理后照片编码格式。
  • guard 在没有可用编码格式时提前返回。
  • AVCapturePhotoSettings(rawPixelFormatType:processedFormat:) 创建一次同时包含 ProRAW 和处理后照片的拍摄请求。
  • AVVideoCodecKey 指定处理后照片使用的编码类型。
  • 返回 AVCapturePhotoSettings?,调用方拿到后再继续补充缩略图、预览和质量优先级。

10:53)ProRAW 支持在 DNG 里嵌入最高可达全尺寸的 JPEG 缩略图。Apple 在 session 中建议用这个模型替代 RAW + JPEG 双资源,图库占用更小,用户体验也更清晰。

import AVFoundation

func addEmbeddedThumbnail(
    to photoSettings: AVCapturePhotoSettings,
    device: AVCaptureDevice
) {
    guard let thumbnailPhotoCodecType = photoSettings.availableRawEmbeddedThumbnailPhotoCodecTypes.first else {
        return
    }

    let dimensions = device.activeFormat.highResolutionStillImageDimensions

    photoSettings.rawEmbeddedThumbnailPhotoFormat = [
        AVVideoCodecKey: thumbnailPhotoCodecType,
        AVVideoWidthKey: dimensions.width,
        AVVideoHeightKey: dimensions.height
    ]
}

关键点:

  • availableRawEmbeddedThumbnailPhotoCodecTypes.first 取 DNG 内嵌缩略图可用编码。
  • device.activeFormat.highResolutionStillImageDimensions 读取当前格式的高分辨率照片尺寸。
  • rawEmbeddedThumbnailPhotoFormat 设置内嵌缩略图格式。
  • AVVideoWidthKeyAVVideoHeightKey 让缩略图使用指定尺寸。

11:08)拍摄前还可以给本次请求设置质量优先级和快速预览。这里的 photoQualityPrioritization 不能超过前面 photoOutput.maxPhotoQualityPrioritization

import AVFoundation

func finishProRAWSettings(
    _ photoSettings: AVCapturePhotoSettings,
    photoOutput: AVCapturePhotoOutput,
    delegate: AVCapturePhotoCaptureDelegate
) {
    photoSettings.photoQualityPrioritization = .quality

    if let previewPixelFormat = photoSettings.availablePreviewPhotoPixelFormatTypes.first {
        photoSettings.previewPhotoFormat = [
            kCVPixelBufferPixelFormatTypeKey as String: previewPixelFormat
        ]
    }

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

关键点:

  • photoQualityPrioritization = .quality 让本次拍摄质量优先。
  • availablePreviewPhotoPixelFormatTypes.first 取一个可用预览像素格式。
  • previewPhotoFormat 请求一个可快速显示的预览图。
  • capturePhoto(with:delegate:) 使用同一个静态照片 API 发起 ProRAW 拍摄。

3. 接收 DNG 文件并定制压缩

11:44didFinishProcessingPhoto 会收到 AVCapturePhoto。如果请求了预览,可以从 previewPixelBuffer 拿显示用图像。用 isRawPhoto 区分 ProRAW 和处理后照片,再用 fileDataRepresentation() 获取 DNG 数据。

import AVFoundation

final class ProRAWCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
    var proRAWFileDataRepresentation: Data?
    var proRAWPixelBuffer: CVPixelBuffer?
    var previewPixelBuffer: CVPixelBuffer?

    func photoOutput(
        _ output: AVCapturePhotoOutput,
        didFinishProcessingPhoto photo: AVCapturePhoto,
        error: Error?
    ) {
        guard error == nil else {
            return
        }

        if let preview = photo.previewPixelBuffer {
            previewPixelBuffer = preview
        }

        if photo.isRawPhoto {
            guard let fileData = photo.fileDataRepresentation() else {
                return
            }
            guard let pixelBuffer = photo.pixelBuffer else {
                return
            }

            proRAWFileDataRepresentation = fileData
            proRAWPixelBuffer = pixelBuffer
        }
    }
}

关键点:

  • AVCapturePhotoCaptureDelegate 接收照片处理完成回调。
  • guard error == nil 过滤采集失败。
  • photo.previewPixelBuffer 提供未压缩预览,可用于快速更新界面。
  • photo.isRawPhoto 判断当前回调是否为 RAW 照片。
  • photo.fileDataRepresentation() 返回可保存的 ProRAW DNG 文件数据。
  • photo.pixelBuffer 返回 RAW 像素数据,适合继续走自定义处理链。

12:52)ProRAW 默认用 12-bit companding curve 无损编码。通过 AVCapturePhotoFileDataRepresentationCustomizer,开发者可以降低无损压缩位深,或把质量值设到 1 以下使用 8-bit 有损压缩。

import AVFoundation

final class AppleProRAWCustomizer: NSObject, AVCapturePhotoFileDataRepresentationCustomizer {
    func replacementAppleProRAWCompressionSettings(
        for photo: AVCapturePhoto,
        defaultSettings: [String: Any],
        maximumBitDepth: Int
    ) -> [String: Any] {
        return [
            AVVideoAppleProRAWBitDepthKey: min(10, maximumBitDepth),
            AVVideoQualityKey: 1.00
        ]
    }
}

关键点:

  • AVCapturePhotoFileDataRepresentationCustomizer 用于替换 ProRAW 文件数据表示的压缩设置。
  • replacementAppleProRAWCompressionSettings 返回新的压缩参数。
  • maximumBitDepth 是当前照片允许的最大位深。
  • min(10, maximumBitDepth) 把无损压缩位深限制在 10-bit 或更低的可用值。
  • AVVideoQualityKey: 1.00 保持无损压缩。

13:51)生成文件数据时,把定制器传入 fileDataRepresentation(with:),就能按新的压缩设置产出 DNG。

import AVFoundation

func customizedProRAWData(from photo: AVCapturePhoto) -> Data? {
    guard photo.isRawPhoto else {
        return nil
    }

    let customizer = AppleProRAWCustomizer()
    return photo.fileDataRepresentation(with: customizer)
}

关键点:

  • guard photo.isRawPhoto 确保只处理 RAW 照片。
  • AppleProRAWCustomizer() 创建压缩设置定制器。
  • fileDataRepresentation(with:) 按定制器生成 ProRAW DNG 数据。
  • 返回 Data?,生成失败时调用方需要处理 nil

4. 用 PhotoKit 保存和读取 RAW 资源

15:19)保存 ProRAW 到图库时,把 DNG 文件作为 .photo 主资源加入 PHAssetCreationRequest。session 明确建议 ProRAW 使用内嵌预览,不推荐旧式 RAW + JPEG 双资源模型。

import PhotoKit

func saveProRAWToPhotoLibrary(proRawFileURL: URL) {
    PHPhotoLibrary.shared().performChanges {
        let creationRequest = PHAssetCreationRequest.forAsset()
        creationRequest.addResource(
            with: .photo,
            fileURL: proRawFileURL,
            options: nil
        )
    } completionHandler: { success, error in
        if success {
            return
        }

        _ = error
    }
}

关键点:

  • PHPhotoLibrary.shared().performChanges 在照片库事务里修改图库。
  • PHAssetCreationRequest.forAsset() 创建一个新照片资源请求。
  • addResource(with:.photo,fileURL:options:) 把 ProRAW DNG 作为主照片资源保存。
  • completionHandler 接收保存结果和错误。

15:45)iOS 15 新增 .smartAlbumRAW。它对应 Photos App 里的 RAW 相册,包含带 RAW 资源的资产。

import PhotoKit

func fetchRAWSmartAlbum() -> PHFetchResult<PHAssetCollection> {
    return PHAssetCollection.fetchAssetCollections(
        with: .smartAlbum,
        subtype: .smartAlbumRAW,
        options: nil
    )
}

关键点:

  • PHAssetCollection.fetchAssetCollections 查询资产集合。
  • .smartAlbum 指定系统智能相册类型。
  • .smartAlbumRAW 指定 RAW 智能相册。
  • 返回 PHFetchResult<PHAssetCollection>,调用方可继续取其中的资产。

17:16)读取某个 PHAsset 的 RAW 数据时,先枚举 PHAssetResource,只看 .photo.alternatePhoto,再用 UTType.rawImage 判断资源类型。

import PhotoKit
import UniformTypeIdentifiers

func requestRAWData(for asset: PHAsset, dataHandler: @escaping (Data) -> Void) {
    let resources = PHAssetResource.assetResources(for: asset)

    for resource in resources {
        if resource.type == .photo || resource.type == .alternatePhoto {
            if let resourceUTType = UTType(resource.uniformTypeIdentifier) {
                if resourceUTType.conforms(to: UTType.rawImage) {
                    let resourceManager = PHAssetResourceManager.default()
                    resourceManager.requestData(for: resource, options: nil) { data in
                        dataHandler(data)
                    } completionHandler: { error in
                        _ = error
                    }
                }
            }
        }
    }
}

关键点:

  • PHAssetResource.assetResources(for:) 取出资产下的所有资源。
  • .photo 是主照片资源,.alternatePhoto 是旧式 RAW + JPEG 模型里的 RAW 资源位置。
  • UTType(resource.uniformTypeIdentifier) 把资源 UTI 转为类型对象。
  • conforms(to: UTType.rawImage) 判断资源是否属于 RAW 图像。
  • PHAssetResourceManager.default() 负责读取资源数据。
  • requestData(for:options:dataReceivedHandler:completionHandler:) 分块返回数据,结束时调用完成回调。

5. 用 Core Image 加载、编辑和导出 ProRAW

18:36)iOS 15 和 macOS 12 的 CIRAWFilter 增加了读取预览图的便捷 API。它比直接走 CGImageSourceCreateThumbnailAtIndex 更贴近 RAW 编辑流程。

import CoreImage

func previewImage(from url: URL) -> CIImage? {
    let rawFilter = CIRAWFilter(imageURL: url)
    return rawFilter.previewImage
}

关键点:

  • CIRAWFilter(imageURL:) 从 ProRAW 文件创建 RAW 滤镜。
  • previewImage 返回 DNG 内的预览图。
  • 返回值是 CIImage?,文件没有可用预览时可能为空。

19:54)如果 App 要提供编辑能力,应该创建 CIRAWFilter 并修改属性。session 展示的常见调整包括曝光、色温、色调、锐化、本地色调映射。

import CoreImage

func adjustedRawImage(url: URL) -> CIImage? {
    let rawFilter = CIRAWFilter(imageURL: url)

    rawFilter.exposure = -1.0
    rawFilter.neutralTemperature = 6500
    rawFilter.neutralTint = 0.0
    rawFilter.sharpnessAmount = 0.5
    rawFilter.localToneMapAmount = 0.5

    return rawFilter.outputImage
}

关键点:

  • CIRAWFilter(imageURL:) 创建可修改的 RAW 滤镜。
  • exposure = -1.0 把图像亮度调为原来的一半。
  • neutralTemperature = 6500 以开尔文设置场景中性点温度。
  • neutralTint = 0.0 设置色调偏移。
  • sharpnessAmount = 0.5 把默认锐化强度设为一半。
  • localToneMapAmount = 0.5 把本地色调映射强度设为一半。
  • outputImage 生成应用这些 RAW 参数后的图像。

21:40)ProRAW 的默认观感由 DNG 标签驱动。要取线性场景参考数据,需要关掉默认观感相关输入,再读取 outputImage

import CoreImage

func linearSceneReferredImage(url: URL) -> CIImage? {
    let rawFilter = CIRAWFilter(imageURL: url)

    rawFilter.baselineExposure = 0.0
    rawFilter.shadowBias = 0.0
    rawFilter.boostAmount = 0.0
    rawFilter.localToneMapAmount = 0.0
    rawFilter.isGamutMappingEnabled = false

    return rawFilter.outputImage
}

关键点:

  • baselineExposure = 0.0 关闭默认基线曝光。
  • shadowBias = 0.0 关闭默认阴影偏置。
  • boostAmount = 0.0 关闭默认 boost。
  • localToneMapAmount = 0.0 关闭本地色调映射。
  • isGamutMappingEnabled = false 关闭色域映射。
  • outputImage 返回线性场景参考图像,可用于分析或继续输入其他 Core Image 滤镜。

23:54)编辑完成后,App 可以把 rawFilter.outputImage 写成 8-bit HEIC。session 推荐输出色彩空间使用 Display P3。

import CoreImage

func writeHEIC(
    rawFilter: CIRAWFilter,
    ciContext: CIContext,
    to theURL: URL
) throws {
    guard let outputImage = rawFilter.outputImage else {
        return
    }

    try ciContext.writeHEIFRepresentation(
        of: outputImage,
        to: theURL,
        format: .RGBA8,
        colorSpace: CGColorSpace(name: CGColorSpace.displayP3)!,
        options: [:]
    )
}

关键点:

  • rawFilter.outputImage 取编辑后的 RAW 输出。
  • guard 在没有输出图像时直接返回。
  • writeHEIFRepresentation 写出 HEIC 文件。
  • format: .RGBA8 使用 8-bit RGBA 输出格式。
  • CGColorSpace.displayP3 使用 Display P3 色彩空间。
  • options: [:] 保持默认写出选项。

25:13)在支持 Extended Dynamic Range(扩展动态范围,EDR)的 Mac 显示器上,Apple 建议用 MTKView 子类显示 ProRAW。视图使用 rgba16FloatCAMetalLayer 打开 EDR 内容,再把 rawFilter.extendedDynamicRangeAmount 设为 1。

import CoreImage
import MetalKit

class MyView: MTKView {
    var context: CIContext
    var commandQueue: MTLCommandQueue

    init(frame frameRect: CGRect, device: MTLDevice, context: CIContext, commandQueue: MTLCommandQueue) {
        self.context = context
        self.commandQueue = commandQueue
        super.init(frame: frameRect, device: device)

        colorPixelFormat = MTLPixelFormat.rgba16Float

        if let caml = layer as? CAMetalLayer {
            caml.wantsExtendedDynamicRangeContent = true
        }
    }

    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func prepareForEDRDisplay(rawFilter: CIRAWFilter) {
        rawFilter.extendedDynamicRangeAmount = 1.0
    }
}

关键点:

  • MTKView 适合把 Core Image 输出交给 Metal 渲染。
  • CIContext 负责 Core Image 渲染。
  • MTLCommandQueue 负责提交 Metal 命令。
  • colorPixelFormat = .rgba16Float 让视图使用半浮点像素格式。
  • wantsExtendedDynamicRangeContent = true 允许 Metal layer 显示 EDR 内容。
  • extendedDynamicRangeAmount = 1.0 请求 RAW 滤镜输出 EDR 版本。

核心启发

  • 做什么:做一个面向摄影师的 ProRAW 相机。 为什么值得做:AVFoundation 可以直接请求 ProRAW DNG,并嵌入全尺寸 JPEG 预览。 怎么开始:用 .photo preset 配置 AVCaptureSession,启用 photoOutput.isAppleProRAWEnabled,选择 isAppleProRAWPixelFormat 返回 true 的像素格式,再调用 capturePhoto(with:delegate:)

  • 做什么:做一个 RAW 图库整理工具,自动找出用户照片库里的 RAW 文件。 为什么值得做:iOS 15 的 .smartAlbumRAW 能直接定位带 RAW 资源的资产。 怎么开始:用 PHAssetCollection.fetchAssetCollections(with:subtype:options:) 获取 RAW 智能相册,再用 PHAssetResource.assetResources(for:)UTType.rawImage 找到 DNG 数据。

  • 做什么:做一个轻量 ProRAW 调色面板。 为什么值得做CIRAWFilter 暴露了曝光、色温、色调、锐化、本地色调映射等常用调整属性。 怎么开始:从文件 URL 创建 CIRAWFilter(imageURL:),把滑杆值映射到 exposureneutralTemperatureneutralTintsharpnessAmountlocalToneMapAmount,实时显示 outputImage

  • 做什么:做一个高动态范围分析工具。 为什么值得做:关闭默认观感输入后,可以拿到线性场景参考图像,用于直方图或亮度统计。 怎么开始:把 baselineExposureshadowBiasboostAmountlocalToneMapAmount 设为 0,关闭 isGamutMappingEnabled,再把 outputImage 传给 CIFilter.areaHistogram()

  • 做什么:做一个 Mac 上的 EDR ProRAW 查看器。 为什么值得做:ProRAW 的动态范围可以通过 MetalKit View 在支持 EDR 的 Mac 显示器上呈现。 怎么开始:创建 MTKView 子类,设置 colorPixelFormat = .rgba16Float,对 CAMetalLayer 打开 wantsExtendedDynamicRangeContent,渲染前设置 rawFilter.extendedDynamicRangeAmount = 1.0

关联 Session

评论

GitHub Issues · utterances