如何使用 metadataOutputRectOfInterestForRect 方法和 rectOfInterest 属性 扫描特定区域? (二维码)

How do I use the metadataOutputRectOfInterestForRect method and rectOfInterest property to scan a specific area? (QR Code)

我正在用 Swift 构建一个 QR 码扫描仪,在这方面一切正常。我遇到的问题是我试图让整个可见区域 AVCaptureVideoPreviewLayer 中只有一小部分区域能够扫描 QR 码。我发现,为了指定屏幕的哪个区域能够 read/capture QR 码,我必须使用 AVCaptureMetadataOutput 的 属性 称为 rectOfInterest。问题是当我将它分配给 CGRect 时,我无法扫描任何东西。在网上做了更多研究后,我发现一些建议我需要使用一种名为 metadataOutputRectOfInterestForRect 的方法将 CGRect 转换为 属性 rectOfInterest 实际可以使用的正确格式。但是,我现在 运行 遇到的一个大问题是,当我使用此方法 metadataoutputRectOfInterestForRect 时,我收到一条错误消息 CGAffineTransformInvert: singular matrix。谁能告诉我为什么会出现此错误?我相信我正在根据 Apple 开发人员文档正确使用此方法,并且我相信我需要根据我在网上找到的所有信息来使用此方法来实现我的目标。我将包含到目前为止找到的文档的链接以及我用来扫描二维码的函数的代码示例

代码示例

func startScan() {
        // Get an instance of the AVCaptureDevice class to initialize a device object and provide the video
        // as the media type parameter.
        let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)

        // Get an instance of the AVCaptureDeviceInput class using the previous device object.
        var error:NSError?
        let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error)

        if (error != nil) {
            // If any error occurs, simply log the description of it and don't continue any more.
            println("\(error?.localizedDescription)")
            return
        }

        // Initialize the captureSession object.
        captureSession = AVCaptureSession()
        // Set the input device on the capture session.
        captureSession?.addInput(input as! AVCaptureInput)

        // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session.
        let captureMetadataOutput = AVCaptureMetadataOutput()
        captureSession?.addOutput(captureMetadataOutput)

        // calculate a centered square rectangle with red border
        let size = 300
        let screenWidth = self.view.frame.size.width
        let xPos = (CGFloat(screenWidth) / CGFloat(2)) - (CGFloat(size) / CGFloat(2))
        let scanRect = CGRect(x: Int(xPos), y: 150, width: size, height: size)

        // create UIView that will server as a red square to indicate where to place QRCode for scanning
        scanAreaView = UIView()
        scanAreaView?.layer.borderColor = UIColor.redColor().CGColor
        scanAreaView?.layer.borderWidth = 4
        scanAreaView?.frame = scanRect
        view.addSubview(scanAreaView!)

        // Set delegate and use the default dispatch queue to execute the call back
        captureMetadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
        captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]



        // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer.
        videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
        videoPreviewLayer?.frame = view.layer.bounds
        captureMetadataOutput.rectOfInterest = videoPreviewLayer!.metadataOutputRectOfInterestForRect(scanRect)
        view.layer.addSublayer(videoPreviewLayer)

        // Start video capture.
        captureSession?.startRunning()

        // Initialize QR Code Frame to highlight the QR code
        qrCodeFrameView = UIView()
        qrCodeFrameView?.layer.borderColor = UIColor.greenColor().CGColor
        qrCodeFrameView?.layer.borderWidth = 2
        view.addSubview(qrCodeFrameView!)
        view.bringSubviewToFront(qrCodeFrameView!)

        // Add a button that will be used to close out of the scan view
        videoBtn.setTitle("Close", forState: .Normal)
        videoBtn.setTitleColor(UIColor.blackColor(), forState: .Normal)
        videoBtn.backgroundColor = UIColor.grayColor()
        videoBtn.layer.cornerRadius = 5.0;
        videoBtn.frame = CGRectMake(10, 30, 70, 45)
        videoBtn.addTarget(self, action: "pressClose:", forControlEvents: .TouchUpInside)
        view.addSubview(videoBtn)


        view.bringSubviewToFront(scanAreaView!)

    }

请注意导致错误的兴趣行是这样的: captureMetadataOutput.rectOfInterest = videoPreviewLayer!.metadataOutputRectOfInterestForRect(scanRect)

我尝试过的其他方法是直接将 CGRect 作为参数传递,这导致了同样的错误。我还传入了 scanAreaView!.bounds 作为参数,因为这正是我正在寻找的 size/area 并且也会导致同样的错误。我已经在其他人的在线代码示例中看到过这样做,他们似乎没有我遇到的错误。以下是一些示例:

AVCaptureSession barcode scan

Xcode AVCapturesession scan Barcode in specific frame (rectOfInterest is not working)

Apple 文档

metadataOutputRectOfInterestForRect

rectOfInterest

我正在使用的scanAreaView的图像作为指定区域我试图使视频预览层的唯一可扫描区域:

我真的无法澄清 metadataOutputRectOfInterestForRect 的问题,但是,您也可以直接设置 属性。您需要预先指定视频的宽度和高度分辨率。我很快使用了 640*480 设置。如文档中所述,这些值必须是

"extending from (0,0) in the top left to (1,1) in the bottom right, relative to the device’s natural orientation".

https://developer.apple.com/documentation/avfoundation/avcaptureoutput/1616304-metadataoutputrectofinterestforr

下面是我试过的代码

var x = scanRect.origin.x/480
var y = scanRect.origin.y/640
var width = scanRect.width/480
var height = scanRect.height/640
var scanRectTransformed = CGRectMake(x, y, width, height)
captureMetadataOutput.rectOfInterest = scanRectTransformed

我刚刚在 iOS 设备上测试过它,它似乎可以工作。

编辑

至少我已经解决了 metadataOutputRectOfInterestForRect 问题。我相信您必须在正确设置相机并且 运行 之后执行此操作,因为相机的分辨率尚不可用。

首先在viewDidLoad()中添加通知观察者方法

NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("avCaptureInputPortFormatDescriptionDidChangeNotification:"), name:AVCaptureInputPortFormatDescriptionDidChangeNotification, object: nil)

然后添加如下方法

func avCaptureInputPortFormatDescriptionDidChangeNotification(notification: NSNotification) {

    captureMetadataOutput.rectOfInterest = videoPreviewLayer.metadataOutputRectOfInterestForRect(scanRect)

}

然后您可以在此处重置 rectOfInterest 属性。然后,在您的代码中,您可以在 didOutputMetadataObjects 函数

中显示 AVMetadataObject
var rect = videoPreviewLayer.rectForMetadataOutputRectOfInterest(YourAVMetadataObject.bounds)

dispatch_async(dispatch_get_main_queue(),{
     self.qrCodeFrameView.frame = rect
})

我试过了,矩形总是在指定区域内。

在 iOS 9.3.2 中,我能够使 metadataoutputRectOfInterestForRectAVCaptureSessionstartRunning 方法之后立即调用它:

captureSession.startRunning()
let visibleRect = previewLayer.metadataOutputRectOfInterestForRect(previewLayer.bounds)
captureMetadataOutput.rectOfInterest = visibleRect

我写了以下内容:

videoPreviewLayer?.frame = view.layer.bounds
videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill

这对我有用,但我仍然不知道为什么。

/// 在

之后
captureSession.startRunning()

/// 添加这个

if let videoPreviewLayer = self.videoPreviewLayer {
self.captureMetadataOutput.rectOfInterest =
videoPreviewLayer.metadataOutputRectOfInterest(for:
self.getRectOfInterest())


fileprivate func getRectOfInterest() -> CGRect {
        let centerX = (self.frame.width / 2) - 100
        let centerY = (self.frame.height / 2) - 100
        let quadr: CGFloat = 200

        let myRect = CGRect(x: centerX, y: centerY, width: quadr, height: quadr)

        return myRect
    }

Swift 4:

captureSession?.startRunning()
let scanRect = CGRect(x: 0, y: 0, width: 100, height: 100)
let rectOfInterest = layer.metadataOutputRectConverted(fromLayerRect: scanRect)
metaDataOutput.rectOfInterest = rectOfInterest

我设法创造了一种具有兴趣区域的效果。我尝试了所有建议的解决方案,但该区域要么是 CGPoint.zero 要么大小不合适(将帧转换为 0-1 坐标后)。对于那些无法让 regionOfInterest 工作并且没有优化检测的人来说,这实际上是一种 hack。

在:

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) 

我有以下代码:

let visualCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj)
if self.viewfinderView.frame.contains(visualCodeObject.bounds) { 
    //visual code is inside the viewfinder, you can now handle detection
}

从完整相机视图中的小矩形(特定区域)读取 QRCode/BarCode。

<br> **Mandatory to keep the below line after (start running)** <br>
[captureMetadataOutput setRectOfInterest:[_videoPreviewLayer metadataOutputRectOfInterestForRect:scanRect] ];

[_captureSession startRunning];
[captureMetadataOutput setRectOfInterest:[_videoPreviewLayer metadataOutputRectOfInterestForRect:scanRect] ];

注:

  1. captureMetadataOutput --> AVCaptureMetadataOutput
  2. _videoPreviewLayer --> AVCaptureVideoPreviewLayer
  3. scanRect --> 在您希望读取二维码的位置设置矩形。

我知道已经有解决方案了,但已经很晚了,但我通过捕获完整的视图图像然后用特定的矩形裁剪它来实现我的。

 func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {

if let imageData = photo.fileDataRepresentation() {
    print(imageData)
    capturedImage = UIImage(data: imageData)

    var crop = cropToPreviewLayer(originalImage: capturedImage!)

    let sb = UIStoryboard(name: "Main", bundle: nil)
    let s = sb.instantiateViewController(withIdentifier: "KeyFobScanned") as! KeyFobScanned
    s.image = crop
    self.navigationController?.pushViewController(s, animated: true)

}
}

private func cropToPreviewLayer(originalImage: UIImage) -> UIImage? {
guard let cgImage = originalImage.cgImage else { return nil }

let scanRect = CGRect(x: stackView.frame.origin.x, y: stackView.frame.origin.y, width: innerView.frame.size.width, height: innerView.frame.size.height)

let outputRect = videoPreviewLayer.metadataOutputRectConverted(fromLayerRect: scanRect)

let width = CGFloat(cgImage.width)
let height = CGFloat(cgImage.height)

let cropRect = CGRect(x: outputRect.origin.x * width, y: outputRect.origin.y * height, width: outputRect.size.width * width, height: outputRect.size.height * height)

if let croppedCGImage = cgImage.cropping(to: cropRect) {
    return UIImage(cgImage: croppedCGImage, scale: 1.0, orientation: originalImage.imageOrientation)
}

return nil
}

可能无关,但对我来说问题是屏幕方向。在我的纵向应用程序上,我想要一个条形码扫描仪,它只检测屏幕中间水平线上的代码。我认为这会起作用:

CGRect(x: 0, y: 0.4, width: 1, height: 0.2)

相反,我不得不将 x 与 y 和宽度与高度切换

CGRect(x: 0.4, y: 0, width: 0.2, height: 1)