16

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.

The_Dude
  • 574
  • 2
  • 6
  • 16
  • It looks like `rectOfInterest` needs to be within the (0,0) – (1,1) range. What is the actual problem that you're having? – jtbandes Aug 30 '15 at 23:13
  • @jtbandes I would like to make the rectOfInterest area to cover a 300 by 300 square area as well as have the same x and y coordinates as the red bordered UIView I added to indicate an area that will recognize QR codes and have that be the only area that does in fact recognize QR code data. Currently if I set the size parameters for rectOfInterest to anything greater than 1 I can no longer scan anything. I do not understand why this is... – The_Dude Aug 30 '15 at 23:24
  • possible duplicate of [Xcode AVCapturesession scan Barcode in specific frame (rectOfInterest is not working)](http://stackoverflow.com/questions/26634036/xcode-avcapturesession-scan-barcode-in-specific-frame-rectofinterest-is-not-wor) – jtbandes Aug 30 '15 at 23:28
  • 1
    @jtbandes I saw that post awhile ago and honestly I don't quite understand how to translate that answer to fix my issue. My apologies, my background is as a web developer and I just started learning ios/swift a few weeks ago so I'm having a hard time wrapping my head around how to use the answer in the post you linked to. – The_Dude Aug 30 '15 at 23:59
  • @jtbandes According to the answer from the post you provided, that user explains that rectOfInterest uses a different coordinate system and to use the method "metadataOutputRectOfInterestForRect" as a conversion tool that accepts a rectangle and returns a rectangle in the correct coordinates for "rectOfInterest". So I fixed one line to look like this `captureMetadataOutput.rectOfInterest = captureMetadataOutput.metadataOutputRectOfInterestForRect(CGRect(x: Int(xPos), y: 150, width: size, height: size))` but that still does not seem to make a difference, I still cannot scan QR codes anywhere. – The_Dude Aug 31 '15 at 00:45
  • @DKrautkramer did you sorted it out? –  Dec 14 '15 at 16:08
  • I can't get it working neither, it make my scanning not working too.. –  Dec 14 '15 at 16:20
  • 2
    @Mayerz I did eventually get this sorted out. I ended up having to post another question around this issue w/bounty to get help, but hopefully this post will now help you: http://stackoverflow.com/questions/32401364/how-do-i-use-the-metadataoutputrectofinterestforrect-method-and-rectofinterest-p – The_Dude Dec 14 '15 at 19:05
  • @DKrautkramer Thanks a lot, much appreciated! –  Dec 15 '15 at 07:52

6 Answers6

6

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.

Sean Calkins
  • 312
  • 2
  • 11
  • Quite wired divisions, 1 / (screenWidth / rect.minX) is equal to rect.minX / screenWidth and so on, also would be great to add guard for screenRect and !rect.isEmpty, and if so return {0, 0, 1, 1} – HotJard Jan 13 '22 at 09:50
3
let metadataOutput = AVCaptureMetadataOutput()
metadataOutput.rectOfInterest = convertRectOfInterest(rect: scanRect)

After reviewing other source(https://www.jianshu.com/p/8bb3d8cb224e), the convertRectOfInterest function has a slight mistake, the return field should be:

return CGRect(x: newY, y: newX, width: newHeight, height: newWidth) 

where x and y, Width and Height input should be interchanged to get it working.

סטנלי גרונן
  • 2,917
  • 23
  • 46
  • 68
Edwin Chau
  • 31
  • 2
1

You need to convert the rect represented in the UIView's coordinates into the coordinate system of the AVCaptureVideoPreviewLayer:

captureMetadataOutput.rectOfInterest = videoPreviewLayer.metadataOutputRectConverted(fromLayerRect: scanRect)

For more info: https://stackoverflow.com/a/55778152/6898849

ramzesenok
  • 5,469
  • 4
  • 30
  • 41
0
let scanView = CGRect(x: centerX, y: centerY, width: width, height: height)
metadataOutput.rectOfInterest = previewLayer.metadataOutputRectConverted(fromLayerRect: scanView)
iminiki
  • 2,549
  • 12
  • 35
  • 45
kor1gp
  • 1
-1

This works for me.

extension AVCaptureVideoPreviewLayer {
    func rectOfInterestConverted(parentRect: CGRect, fromLayerRect: CGRect) -> CGRect {
        let parentWidth = parentRect.width
        let parentHeight = parentRect.height
        let newX = (parentWidth - fromLayerRect.maxX)/parentWidth
        let newY = 1 - (parentHeight - fromLayerRect.minY)/parentHeight
        let width = 1 - (fromLayerRect.minX/parentWidth + newX)
        let height = (fromLayerRect.maxY/parentHeight) - newY

        return CGRect(x: newX, y: newY, width: width, height: height)
    }
}

Usage:

    if let rect = videoPreviewLayer?.rectOfInterestConverted(parentRect: self.view.frame, fromLayerRect: scanAreaView.frame) {
        captureMetadataOutput.rectOfInterest = rect
    }
carus
  • 1
  • 1
-1
metadataOutput.rectOfInterest = previewLayer.metadataOutputRectConverted(fromLayerRect: yourView.frame)

previewLayer it's AVCaptureVideoPreviewLayer

Serhii
  • 7
  • 2