You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AVCapture坐标转换异常:resizeAspectFill下Y轴坐标不符问题求助

解决AVCapture resizeAspectFill模式下曝光兴趣点Y轴坐标转换偏差问题

问题重现

适配iPad Pro M4与iPad Air M3的应用,基于AVCapture实现实时预览、AVCam代码捕获图像。设置自定义曝光兴趣点时,用以下代码转换坐标:

let point = CGPoint(x: 340, y : 445)
let devicePoint = cameraPreview.layer.captureDevicePointConverted(fromLayerPoint: point)

捕获图像时用下面的代码还原坐标:

let point = cameraPreview.layer.layerPointConverted(fromCaptureDevicePoint: videoDevice.exposurePointOfInterest)

预期两次坐标一致,但仅Y轴数值不符。切换videoGravity为普通resize时结果正常,问题仅出在resizeAspectFill模式下。

核心原因

resizeAspectFill会让视频帧按比例缩放后填满预览层,超出部分被裁剪,但系统提供的两个坐标转换方法是基于未裁剪的完整视频帧计算的,没有考虑实际显示的裁剪区域,导致Y轴(或X轴,取决于视频与预览层的宽高比)出现偏差。

手动修正坐标转换方案

通过计算预览层的可见视频区域,手动映射坐标,替代系统方法:

1. 层坐标转设备坐标(设置曝光点时用)

func correctedDevicePoint(from layerPoint: CGPoint, previewLayer: AVCaptureVideoPreviewLayer) -> CGPoint {
    guard let input = previewLayer.session?.inputs.first as? AVCaptureDeviceInput,
          let videoFormat = input.device.activeFormat else {
        return previewLayer.captureDevicePointConverted(fromLayerPoint: layerPoint)
    }
    
    let videoRes = videoFormat.formatDescription.dimensions
    let videoRatio = CGFloat(videoRes.width) / CGFloat(videoRes.height)
    let layerRatio = previewLayer.bounds.width / previewLayer.bounds.height
    
    var visibleRect = CGRect.zero
    if videoRatio > layerRatio {
        // 视频更宽,上下裁剪
        let scaledHeight = previewLayer.bounds.width / videoRatio
        visibleRect = CGRect(x: 0,
                             y: (previewLayer.bounds.height - scaledHeight) / 2,
                             width: previewLayer.bounds.width,
                             height: scaledHeight)
    } else {
        // 视频更高,左右裁剪
        let scaledWidth = previewLayer.bounds.height * videoRatio
        visibleRect = CGRect(x: (previewLayer.bounds.width - scaledWidth) / 2,
                             y: 0,
                             width: scaledWidth,
                             height: previewLayer.bounds.height)
    }
    
    // 计算层点在可见区域内的归一化位置,转换为设备坐标(设备坐标是(0,0)左上到(1,1)右下的归一化值)
    let normX = (layerPoint.x - visibleRect.origin.x) / visibleRect.width
    let normY = (layerPoint.y - visibleRect.origin.y) / visibleRect.height
    return CGPoint(x: normX, y: normY)
}

2. 设备坐标转层坐标(还原曝光点时用)

func correctedLayerPoint(from devicePoint: CGPoint, previewLayer: AVCaptureVideoPreviewLayer) -> CGPoint {
    guard let input = previewLayer.session?.inputs.first as? AVCaptureDeviceInput,
          let videoFormat = input.device.activeFormat else {
        return previewLayer.layerPointConverted(fromCaptureDevicePoint: devicePoint)
    }
    
    let videoRes = videoFormat.formatDescription.dimensions
    let videoRatio = CGFloat(videoRes.width) / CGFloat(videoRes.height)
    let layerRatio = previewLayer.bounds.width / previewLayer.bounds.height
    
    var visibleRect = CGRect.zero
    if videoRatio > layerRatio {
        let scaledHeight = previewLayer.bounds.width / videoRatio
        visibleRect = CGRect(x: 0,
                             y: (previewLayer.bounds.height - scaledHeight) / 2,
                             width: previewLayer.bounds.width,
                             height: scaledHeight)
    } else {
        let scaledWidth = previewLayer.bounds.height * videoRatio
        visibleRect = CGRect(x: (previewLayer.bounds.width - scaledWidth) / 2,
                             y: 0,
                             width: scaledWidth,
                             height: previewLayer.bounds.height)
    }
    
    // 将归一化设备坐标转换为可见区域内的绝对层坐标
    let x = visibleRect.origin.x + devicePoint.x * visibleRect.width
    let y = visibleRect.origin.y + devicePoint.y * visibleRect.height
    return CGPoint(x: x, y: y)
}

使用方式

设置曝光点时替换系统方法:

let layerPoint = CGPoint(x: 340, y: 445)
let correctedDevicePoint = correctedDevicePoint(from: layerPoint, previewLayer: cameraPreview.layer)
videoDevice.setExposurePointOfInterest(correctedDevicePoint)

还原坐标时同样替换:

let correctedLayerPoint = correctedLayerPoint(from: videoDevice.exposurePointOfInterest, previewLayer: cameraPreview.layer)
// 此时correctedLayerPoint应该和最初的layerPoint一致

内容的提问来源于stack exchange,提问作者Prerna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 17:03:11