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

