How do I capture QR Code data in specific area of AVCaptureVideoPreviewLayer using Swift?

DKrautkramer picture DKrautkramer · Aug 31, 2015 · Viewed 8.1k times · Source

I am creating an iPad app and one of it's features is scanning QR codes. I have the QR scanning part working, but the issue I have is that the iPad screen is very large and I will be scanning small QR codes of of a sheet of paper with many QR codes visible at once. I want to designate a smaller area of the display to be the only area that can actually capture a QR code so it is easier for the user to scan the specific QR code they want.

I currently have made a temporary UIView with red borders that is centered on the page as an example of where I will want the user to scan the QR codes. It looks like this:

I have looked all over to find an answer to how I can target a specific region of the AVCaptureVideoPreviewLayer to collect the QR code data, and what I have found is suggestions to use "rectOfInterest" with AVCaptureMetadataOutput. I have attempted to do that, but when I set rectOfInterest to the same coordinates and size as those I use for my UIView that shows up correctly, I can no longer scan/recognize any QR codes. Can someone please tell me why the scannable area does not match the location of the UIView that is seen and how can I get the rectOfInterest to be within the red borders I have added to the screen?

Here is the code for the scan function I am currently using:

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

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


    // 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
    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.addSubview(scanAreaView!)

}

Update The reason I do not think this is a duplicate is because the other post referenced is in Objective-C and my code is in Swift. For those of us that are new to iOS it is not as easy to translate the two. Also, the referenced post's answer does not show the actual update made in the code that resolved his issue. He left a good explanation about having to use the metadataOutputRectOfInterestForRect method to convert the rectangle coordinates, but I still cannot seem to get this method to work, as it is unclear to me how this should work without an example.

Answer

Sean Calkins picture Sean Calkins · May 30, 2018

After fighting with the metedataOutputRectOfInterestForRect method all morning, I got tired of it and decided to write my own conversion.

func convertRectOfInterest(rect: CGRect) -> CGRect {
    let screenRect = self.view.frame
    let screenWidth = screenRect.width
    let screenHeight = screenRect.height
    let newX = 1 / (screenWidth / rect.minX)
    let newY = 1 / (screenHeight / rect.minY)
    let newWidth = 1 / (screenWidth / rect.width)
    let newHeight = 1 / (screenHeight / rect.height)
    return CGRect(x: newX, y: newY, width: newWidth, height: newHeight)
}

Note: I have an image view with a square to show the user where to scan, be sure to use the imageView.frame and not imageView.bounds in order to get the correct location on the screen.

This has been working successfully for me.