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

在Apple Vision框架中,如何从VNRectangleObservation获取CIImage?

嘿,我来帮你搞定这个问题!确实,VNRectangleObservation只提供检测到的矩形坐标,不带图像数据,所以我们得从原始的pixelBuffer入手,把对应区域抠出来。下面是一步步的解决方案:

步骤1:给Vision请求添加结果回调

首先,你需要给VNDetectRectanglesRequest设置completionHandler,这样才能拿到检测到的矩形结果——原来的代码只是执行了请求,但没处理返回的观测数据,这一步很关键:

// 在类的初始化逻辑(比如viewDidLoad)里定义你的检测请求
let rectangleRequest = VNDetectRectanglesRequest { [weak self] request, error in
    guard let self = self, let observations = request.results as? [VNRectangleObservation], error == nil else {
        print(error ?? "检测矩形时出现未知错误")
        return
    }
    
    // 遍历每个检测到的矩形,后续处理逻辑写在这里
    for observation in observations {
        self.processDetectedRectangle(observation)
    }
}
// 可以按需设置检测参数,比如最小宽高比、最大检测数量
rectangleRequest.minimumAspectRatio = 0.5
rectangleRequest.maximumObservations = 1
步骤2:在captureOutput中处理当前帧并关联检测结果

在你的captureOutput方法里,先把pixelBuffer转换成CIImage,还要处理视频方向(AVCapture的输出图像方向可能和设备朝向不一致),然后把调整后的图像临时保存,供后续裁剪使用:

// 定义一个线程安全的变量,临时存储当前帧的图像
private var currentFrameImage: CIImage? {
    get { DispatchQueue.main.sync { _currentFrameImage } }
    set { DispatchQueue.main.async { self._currentFrameImage = newValue } }
}
private var _currentFrameImage: CIImage?

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } 
    
    // 1. 将pixelBuffer转换为CIImage
    let ciImage = CIImage(cvImageBuffer: pixelBuffer)
    // 2. 调整图像方向,匹配设备当前朝向
    let adjustedImage = adjustImageOrientation(for: ciImage, connection: connection)
    // 3. 保存当前帧图像,供后续裁剪使用
    currentFrameImage = adjustedImage
    
    // 执行Vision检测请求
    let handler = VNImageRequestHandler(ciImage: adjustedImage, options: [:])
    do { 
        try handler.perform([rectangleRequest])
    } catch { 
        print(error) 
    }
}

// 辅助函数:调整CIImage方向以匹配视频连接的方向
private func adjustImageOrientation(for ciImage: CIImage, connection: AVCaptureConnection) -> CIImage {
    let orientation = connection.videoOrientation
    var transform = CGAffineTransform.identity
    
    switch orientation {
    case .portrait:
        transform = CGAffineTransform(rotationAngle: .pi/2).translatedBy(x: ciImage.extent.height, y: 0)
    case .portraitUpsideDown:
        transform = CGAffineTransform(rotationAngle: -.pi/2).translatedBy(x: 0, y: ciImage.extent.width)
    case .landscapeLeft:
        transform = CGAffineTransform(rotationAngle: .pi).translatedBy(x: ciImage.extent.width, y: ciImage.extent.height)
    case .landscapeRight:
        break // 无需旋转
    @unknown default:
        break
    }
    
    return ciImage.transformed(by: transform)
}
步骤3:从VNRectangleObservation提取对应区域的CIImage

现在实现processDetectedRectangle方法,利用VNRectangleObservation的归一化坐标,从当前帧图像中裁剪出目标区域:

private func processDetectedRectangle(_ observation: VNRectangleObservation) {
    guard let currentImage = currentFrameImage else { return }
    
    // Vision的boundingBox是归一化坐标(0-1范围),原点在图像左下角,和CIImage坐标系一致
    let imageSize = currentImage.extent.size
    let boundingBox = observation.boundingBox
    
    // 转换为实际像素坐标
    let cropRect = CGRect(
        x: boundingBox.origin.x * imageSize.width,
        y: boundingBox.origin.y * imageSize.height,
        width: boundingBox.size.width * imageSize.width,
        height: boundingBox.size.height * imageSize.height
    )
    
    // 裁剪出矩形区域的图像
    let croppedImage = currentImage.cropped(to: cropRect)
    
    // 这里可以对裁剪后的图像进行保存、显示等操作
    saveCroppedImageToAlbum(croppedImage)
}
步骤4:保存裁剪后的图像到相册

如果需要把检测到的矩形区域保存为照片,可以将CIImage转换为UIImage,再调用系统相册API:

private func saveCroppedImageToAlbum(_ croppedImage: CIImage) {
    // 将CIImage转换为CGImage,再转成UIImage
    guard let cgImage = CIContext().createCGImage(croppedImage, from: croppedImage.extent) else {
        print("无法将CIImage转换为CGImage")
        return
    }
    let uiImage = UIImage(cgImage: cgImage)
    
    // 必须在主线程执行相册保存操作
    DispatchQueue.main.async {
        UIImageWriteToSavedPhotosAlbum(uiImage, self, #selector(self.imageSavedCallback(_:didFinishSavingWithError:contextInfo:)), nil)
    }
}

// 保存完成的回调函数
@objc private func imageSavedCallback(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
    if let error = error {
        print("保存失败:\(error.localizedDescription)")
    } else {
        print("矩形区域图像保存成功!")
    }
}
一些关键注意事项
  • 线程安全:captureOutput运行在AVCapture的后台线程,所以访问currentFrameImage时要通过线程同步保护,避免数据竞争。
  • 性能优化:不需要保存整个pixelBuffer,只裁剪需要的矩形区域即可,减少内存占用。
  • 坐标系匹配:一定要处理视频方向,否则裁剪出来的图像可能会和预览画面旋转不一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:24:39