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
相关产品推荐
相关产品推荐

