├── VisionCreditScan
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── AppDelegate.swift
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── SceneDelegate.swift
├── Info.plist
├── TextExtractorVC.swift
└── ViewController.swift
├── vision-credit-card-scanner-output.gif
├── VisionCreditScan.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── anupamchugh.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── anupamchugh.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
└── README.md
/VisionCreditScan/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/vision-credit-card-scanner-output.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anupamchugh/VisionCreditScan/HEAD/vision-credit-card-scanner-output.gif
--------------------------------------------------------------------------------
/VisionCreditScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/VisionCreditScan.xcodeproj/project.xcworkspace/xcuserdata/anupamchugh.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anupamchugh/VisionCreditScan/HEAD/VisionCreditScan.xcodeproj/project.xcworkspace/xcuserdata/anupamchugh.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/VisionCreditScan.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VisionCreditScan.xcodeproj/xcuserdata/anupamchugh.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | VisionCreditScan.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VisionCreditScan
2 |
3 | Scan credit and other buisness cards using rectangle detection, followed by a perspective correction to handle distortion.
4 |
5 | Finally, I've allowed the user to select the region of interest through gestures and extract the relevant
6 | information using Vision's Text Recognizer.
7 |
8 | Doing so, not only makes it a secure way of extracting digits, but also allows us to step away from regular
9 | expressions for parsing texts. Regular Expressions are inefficient in scenarios, since there's not gurantee of the number
10 | of digits that'll be there on the card. For example AMEX cards don't have 16 digits.
11 |
12 | # Demo
13 |
14 | 
15 |
16 | ## Article
17 | [Scanning Credit Cards Using Computer Vision in iOS](https://iosdevie.substack.com/p/vision-ios-credit-card-scanner)
18 |
19 |
--------------------------------------------------------------------------------
/VisionCreditScan/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // VisionCreditScan
4 | //
5 | // Created by Anupam Chugh on 27/01/20.
6 | // Copyright © 2020 iowncode. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/VisionCreditScan/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/VisionCreditScan/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/VisionCreditScan/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // VisionCreditScan
4 | //
5 | // Created by Anupam Chugh on 27/01/20.
6 | // Copyright © 2020 iowncode. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 | guard let _ = (scene as? UIWindowScene) else { return }
21 | }
22 |
23 | func sceneDidDisconnect(_ scene: UIScene) {
24 | // Called as the scene is being released by the system.
25 | // This occurs shortly after the scene enters the background, or when its session is discarded.
26 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
28 | }
29 |
30 | func sceneDidBecomeActive(_ scene: UIScene) {
31 | // Called when the scene has moved from an inactive state to an active state.
32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
33 | }
34 |
35 | func sceneWillResignActive(_ scene: UIScene) {
36 | // Called when the scene will move from an active state to an inactive state.
37 | // This may occur due to temporary interruptions (ex. an incoming phone call).
38 | }
39 |
40 | func sceneWillEnterForeground(_ scene: UIScene) {
41 | // Called as the scene transitions from the background to the foreground.
42 | // Use this method to undo the changes made on entering the background.
43 | }
44 |
45 | func sceneDidEnterBackground(_ scene: UIScene) {
46 | // Called as the scene transitions from the foreground to the background.
47 | // Use this method to save data, release shared resources, and store enough scene-specific state information
48 | // to restore the scene back to its current state.
49 | }
50 |
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/VisionCreditScan/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | NSCameraUsageDescription
45 | The application needs access to the camera in order to scan rectangular regions
46 | UIMainStoryboardFile
47 | Main
48 | NSPhotoLibraryAddUsageDescription
49 | In order to save the extracted image
50 | NSPhotoLibraryUsageDescription
51 | In order to save the extracted image
52 | UIRequiredDeviceCapabilities
53 |
54 | armv7
55 |
56 | UISupportedInterfaceOrientations
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationLandscapeLeft
60 | UIInterfaceOrientationLandscapeRight
61 |
62 | UISupportedInterfaceOrientations~ipad
63 |
64 | UIInterfaceOrientationPortrait
65 | UIInterfaceOrientationPortraitUpsideDown
66 | UIInterfaceOrientationLandscapeLeft
67 | UIInterfaceOrientationLandscapeRight
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/VisionCreditScan/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/VisionCreditScan/TextExtractorVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextExtractorVC.swift
3 | // VisionCreditScan
4 | //
5 | // Created by Anupam Chugh on 27/01/20.
6 | // Copyright © 2020 iowncode. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Vision
11 | //import CoreMotion
12 |
13 | class TextExtractorVC: UIViewController {
14 |
15 | //let manager = CMMotionManager()
16 | let queue = OperationQueue()
17 |
18 | let overlay = UIView()
19 | var lastPoint = CGPoint.zero
20 |
21 | var textRecognitionRequest = VNRecognizeTextRequest(completionHandler: nil)
22 | private let textRecognitionWorkQueue = DispatchQueue(label: "MyVisionScannerQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
23 |
24 | var scannedImage : UIImage?
25 |
26 | private var maskLayer = [CAShapeLayer]()
27 |
28 | lazy var imageView : UIImageView = {
29 |
30 | let b = UIImageView()
31 | b.contentMode = .scaleAspectFit
32 |
33 | view.addSubview(b)
34 |
35 | b.translatesAutoresizingMaskIntoConstraints = false
36 | b.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
37 | b.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
38 | b.topAnchor.constraint(equalTo: view.topAnchor, constant: 30).isActive = true
39 | b.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
40 |
41 | return b
42 |
43 | }()
44 |
45 | lazy var button : UIButton = {
46 |
47 | let b = UIButton(type: .system)
48 | b.setTitle("Extract Digits", for: .normal)
49 | view.addSubview(b)
50 |
51 | b.translatesAutoresizingMaskIntoConstraints = false
52 | b.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
53 | b.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
54 | b.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
55 | b.heightAnchor.constraint(equalToConstant: 50).isActive = true
56 |
57 | return b
58 |
59 | }()
60 |
61 | lazy var digitsLabel : UILabel = {
62 |
63 | let b = UILabel(frame: .zero)
64 |
65 | view.addSubview(b)
66 |
67 | b.translatesAutoresizingMaskIntoConstraints = false
68 | b.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
69 | b.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
70 | b.bottomAnchor.constraint(equalTo: self.button.topAnchor, constant: -20).isActive = true
71 | b.heightAnchor.constraint(equalToConstant: 30).isActive = true
72 |
73 | return b
74 |
75 | }()
76 |
77 | @objc func doExtraction(sender: UIButton!){
78 | processImage(snapshot(in: imageView, rect: overlay.frame))
79 | }
80 |
81 | func snapshot(in imageView: UIImageView, rect: CGRect) -> UIImage {
82 | return UIGraphicsImageRenderer(bounds: rect).image { _ in
83 |
84 | clearOverlay()
85 | imageView.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
86 |
87 | }
88 | }
89 |
90 | override func viewDidLoad() {
91 | super.viewDidLoad()
92 |
93 | setupVision()
94 | self.view.backgroundColor = .black
95 |
96 | imageView.image = scannedImage
97 |
98 | overlay.backgroundColor = UIColor.red.withAlphaComponent(0.5)
99 | overlay.isHidden = true
100 |
101 | imageView.addSubview(overlay)
102 | imageView.bringSubviewToFront(overlay)
103 |
104 | button.addTarget(self, action: #selector(doExtraction(sender:)), for: .touchUpInside)
105 |
106 | }
107 |
108 | private func setupVision() {
109 | textRecognitionRequest = VNRecognizeTextRequest { (request, error) in
110 | guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
111 |
112 | var detectedText = ""
113 | for observation in observations {
114 | guard let topCandidate = observation.topCandidates(1).first else { return }
115 |
116 |
117 | detectedText += topCandidate.string
118 | detectedText += "\n"
119 | }
120 |
121 | DispatchQueue.main.async{
122 | self.digitsLabel.text = detectedText
123 | }
124 | }
125 |
126 | textRecognitionRequest.recognitionLevel = .accurate
127 | }
128 |
129 | private func processImage(_ image: UIImage) {
130 | recognizeTextInImage(image)
131 | }
132 |
133 | private func recognizeTextInImage(_ image: UIImage) {
134 | guard let cgImage = image.cgImage else { return }
135 |
136 | textRecognitionWorkQueue.async {
137 | let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
138 | do {
139 | try requestHandler.perform([self.textRecognitionRequest])
140 | } catch {
141 | print(error)
142 | }
143 | }
144 | }
145 |
146 | func clearOverlay(){
147 | overlay.isHidden = false
148 | overlay.frame = CGRect.zero
149 | }
150 |
151 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
152 |
153 | clearOverlay()
154 | if let touch = touches.first {
155 | lastPoint = touch.location(in: self.view)
156 | }
157 | }
158 |
159 |
160 | override func touchesMoved(_ touches: Set, with event: UIEvent?) {
161 | if let touch = touches.first {
162 | let currentPoint = touch.location(in: view)
163 | drawSelectionArea(fromPoint: lastPoint, toPoint: currentPoint)
164 | }
165 | }
166 |
167 | func drawSelectionArea(fromPoint: CGPoint, toPoint: CGPoint) {
168 |
169 | let rect = CGRect(x: min(fromPoint.x, toPoint.x), y: min(fromPoint.y, toPoint.y), width: abs(fromPoint.x - toPoint.x), height: abs(fromPoint.y - toPoint.y))
170 | overlay.frame = rect
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/VisionCreditScan/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // VisionCreditScan
4 | //
5 | // Created by Anupam Chugh on 27/01/20.
6 | // Copyright © 2020 iowncode. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 | import Vision
12 |
13 | class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
14 |
15 | private let captureSession = AVCaptureSession()
16 | private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
17 | private let videoDataOutput = AVCaptureVideoDataOutput()
18 |
19 | private var isTapped = false
20 |
21 |
22 | lazy var item : UINavigationItem = {
23 | let item = UINavigationItem()
24 |
25 | item.setRightBarButton(UIBarButtonItem(title: "Scan", style: .plain, target: self, action: #selector(doScan(sender:))), animated: false)
26 |
27 | return item
28 | }()
29 |
30 | private var maskLayer = CAShapeLayer()
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | self.setCameraInput()
36 | self.showCameraFeed()
37 | self.setCameraOutput()
38 |
39 | self.navigationController?.navigationBar.setItems([item], animated: false)
40 | }
41 |
42 | override func viewDidAppear(_ animated: Bool) {
43 |
44 | self.videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera_frame_processing_queue"))
45 | self.captureSession.startRunning()
46 | }
47 |
48 | override func viewDidDisappear(_ animated: Bool) {
49 |
50 | self.videoDataOutput.setSampleBufferDelegate(nil, queue: nil)
51 | self.captureSession.stopRunning()
52 | }
53 |
54 | func doPerspectiveCorrection(_ observation: VNRectangleObservation, from buffer: CVImageBuffer) {
55 | var ciImage = CIImage(cvImageBuffer: buffer)
56 |
57 | let topLeft = observation.topLeft.scaled(to: ciImage.extent.size)
58 | let topRight = observation.topRight.scaled(to: ciImage.extent.size)
59 | let bottomLeft = observation.bottomLeft.scaled(to: ciImage.extent.size)
60 | let bottomRight = observation.bottomRight.scaled(to: ciImage.extent.size)
61 |
62 | // pass those to the filter to extract/rectify the image
63 | ciImage = ciImage.applyingFilter("CIPerspectiveCorrection", parameters: [
64 | "inputTopLeft": CIVector(cgPoint: topLeft),
65 | "inputTopRight": CIVector(cgPoint: topRight),
66 | "inputBottomLeft": CIVector(cgPoint: bottomLeft),
67 | "inputBottomRight": CIVector(cgPoint: bottomRight),
68 | ])
69 |
70 | let context = CIContext()
71 | let cgImage = context.createCGImage(ciImage, from: ciImage.extent)
72 | let output = UIImage(cgImage: cgImage!)
73 | //UIImageWriteToSavedPhotosAlbum(output, nil, nil, nil)
74 |
75 | let secondVC = TextExtractorVC()
76 | secondVC.scannedImage = output
77 | self.navigationController?.pushViewController(secondVC, animated: false)
78 |
79 | }
80 |
81 | @objc func doScan(sender: UIButton!){
82 | self.isTapped = true
83 | }
84 |
85 | override func viewDidLayoutSubviews() {
86 | super.viewDidLayoutSubviews()
87 | self.previewLayer.frame = self.view.frame
88 | }
89 |
90 | func captureOutput(
91 | _ output: AVCaptureOutput,
92 | didOutput sampleBuffer: CMSampleBuffer,
93 | from connection: AVCaptureConnection) {
94 |
95 | guard let frame = CMSampleBufferGetImageBuffer(sampleBuffer) else {
96 | debugPrint("unable to get image from sample buffer")
97 | return
98 | }
99 |
100 | self.detectRectangle(in: frame)
101 | }
102 |
103 | private func setCameraInput() {
104 | guard let device = AVCaptureDevice.DiscoverySession(
105 | deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera],
106 | mediaType: .video,
107 | position: .back).devices.first else {
108 | fatalError("No back camera device found.")
109 | }
110 | let cameraInput = try! AVCaptureDeviceInput(device: device)
111 | self.captureSession.addInput(cameraInput)
112 | }
113 |
114 | private func showCameraFeed() {
115 | self.previewLayer.videoGravity = .resizeAspectFill
116 | self.view.layer.addSublayer(self.previewLayer)
117 | self.previewLayer.frame = self.view.frame
118 | }
119 |
120 | private func setCameraOutput() {
121 | self.videoDataOutput.videoSettings = [(kCVPixelBufferPixelFormatTypeKey as NSString) : NSNumber(value: kCVPixelFormatType_32BGRA)] as [String : Any]
122 |
123 | self.videoDataOutput.alwaysDiscardsLateVideoFrames = true
124 | self.videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera_frame_processing_queue"))
125 | self.captureSession.addOutput(self.videoDataOutput)
126 |
127 | guard let connection = self.videoDataOutput.connection(with: AVMediaType.video),
128 | connection.isVideoOrientationSupported else { return }
129 |
130 | connection.videoOrientation = .portrait
131 | }
132 |
133 | private func detectRectangle(in image: CVPixelBuffer) {
134 |
135 | let request = VNDetectRectanglesRequest(completionHandler: { (request: VNRequest, error: Error?) in
136 | DispatchQueue.main.async {
137 |
138 | guard let results = request.results as? [VNRectangleObservation] else { return }
139 | self.removeMask()
140 |
141 | guard let rect = results.first else{return}
142 | self.drawBoundingBox(rect: rect)
143 |
144 | if self.isTapped{
145 | self.isTapped = false
146 | self.doPerspectiveCorrection(rect, from: image)
147 | }
148 | }
149 | })
150 |
151 | request.minimumAspectRatio = VNAspectRatio(1.3)
152 | request.maximumAspectRatio = VNAspectRatio(1.6)
153 | request.minimumSize = Float(0.5)
154 | request.maximumObservations = 1
155 |
156 |
157 | let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: image, options: [:])
158 | try? imageRequestHandler.perform([request])
159 | }
160 |
161 | func drawBoundingBox(rect : VNRectangleObservation) {
162 |
163 | let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -self.previewLayer.frame.height)
164 | let scale = CGAffineTransform.identity.scaledBy(x: self.previewLayer.frame.width, y: self.previewLayer.frame.height)
165 |
166 | let bounds = rect.boundingBox.applying(scale).applying(transform)
167 | createLayer(in: bounds)
168 |
169 | }
170 |
171 | private func createLayer(in rect: CGRect) {
172 |
173 | maskLayer = CAShapeLayer()
174 | maskLayer.frame = rect
175 | maskLayer.cornerRadius = 10
176 | maskLayer.opacity = 0.75
177 | maskLayer.borderColor = UIColor.red.cgColor
178 | maskLayer.borderWidth = 5.0
179 |
180 | previewLayer.insertSublayer(maskLayer, at: 1)
181 |
182 | }
183 |
184 | func removeMask() {
185 | maskLayer.removeFromSuperlayer()
186 |
187 | }
188 | }
189 |
190 | extension CGPoint {
191 | func scaled(to size: CGSize) -> CGPoint {
192 | return CGPoint(x: self.x * size.width,
193 | y: self.y * size.height)
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/VisionCreditScan.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1E5C777123DEF09B00AEB4C5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C777023DEF09B00AEB4C5 /* AppDelegate.swift */; };
11 | 1E5C777323DEF09B00AEB4C5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C777223DEF09B00AEB4C5 /* SceneDelegate.swift */; };
12 | 1E5C777523DEF09B00AEB4C5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5C777423DEF09B00AEB4C5 /* ViewController.swift */; };
13 | 1E5C777823DEF09B00AEB4C5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E5C777623DEF09B00AEB4C5 /* Main.storyboard */; };
14 | 1E5C777A23DEF09F00AEB4C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E5C777923DEF09F00AEB4C5 /* Assets.xcassets */; };
15 | 1E5C777D23DEF09F00AEB4C5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E5C777B23DEF09F00AEB4C5 /* LaunchScreen.storyboard */; };
16 | 1EEBC59E23DF6B58004BE6E3 /* TextExtractorVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEBC59D23DF6B58004BE6E3 /* TextExtractorVC.swift */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | 1E5C776D23DEF09B00AEB4C5 /* VisionCreditScan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionCreditScan.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 1E5C777023DEF09B00AEB4C5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
22 | 1E5C777223DEF09B00AEB4C5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
23 | 1E5C777423DEF09B00AEB4C5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
24 | 1E5C777723DEF09B00AEB4C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
25 | 1E5C777923DEF09F00AEB4C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | 1E5C777C23DEF09F00AEB4C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
27 | 1E5C777E23DEF09F00AEB4C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
28 | 1EEBC59D23DF6B58004BE6E3 /* TextExtractorVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextExtractorVC.swift; sourceTree = ""; };
29 | /* End PBXFileReference section */
30 |
31 | /* Begin PBXFrameworksBuildPhase section */
32 | 1E5C776A23DEF09B00AEB4C5 /* Frameworks */ = {
33 | isa = PBXFrameworksBuildPhase;
34 | buildActionMask = 2147483647;
35 | files = (
36 | );
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXFrameworksBuildPhase section */
40 |
41 | /* Begin PBXGroup section */
42 | 1E5C776423DEF09A00AEB4C5 = {
43 | isa = PBXGroup;
44 | children = (
45 | 1E5C776F23DEF09B00AEB4C5 /* VisionCreditScan */,
46 | 1E5C776E23DEF09B00AEB4C5 /* Products */,
47 | );
48 | sourceTree = "";
49 | };
50 | 1E5C776E23DEF09B00AEB4C5 /* Products */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 1E5C776D23DEF09B00AEB4C5 /* VisionCreditScan.app */,
54 | );
55 | name = Products;
56 | sourceTree = "";
57 | };
58 | 1E5C776F23DEF09B00AEB4C5 /* VisionCreditScan */ = {
59 | isa = PBXGroup;
60 | children = (
61 | 1EEBC59D23DF6B58004BE6E3 /* TextExtractorVC.swift */,
62 | 1E5C777023DEF09B00AEB4C5 /* AppDelegate.swift */,
63 | 1E5C777223DEF09B00AEB4C5 /* SceneDelegate.swift */,
64 | 1E5C777423DEF09B00AEB4C5 /* ViewController.swift */,
65 | 1E5C777623DEF09B00AEB4C5 /* Main.storyboard */,
66 | 1E5C777923DEF09F00AEB4C5 /* Assets.xcassets */,
67 | 1E5C777B23DEF09F00AEB4C5 /* LaunchScreen.storyboard */,
68 | 1E5C777E23DEF09F00AEB4C5 /* Info.plist */,
69 | );
70 | path = VisionCreditScan;
71 | sourceTree = "";
72 | };
73 | /* End PBXGroup section */
74 |
75 | /* Begin PBXNativeTarget section */
76 | 1E5C776C23DEF09B00AEB4C5 /* VisionCreditScan */ = {
77 | isa = PBXNativeTarget;
78 | buildConfigurationList = 1E5C778123DEF09F00AEB4C5 /* Build configuration list for PBXNativeTarget "VisionCreditScan" */;
79 | buildPhases = (
80 | 1E5C776923DEF09B00AEB4C5 /* Sources */,
81 | 1E5C776A23DEF09B00AEB4C5 /* Frameworks */,
82 | 1E5C776B23DEF09B00AEB4C5 /* Resources */,
83 | );
84 | buildRules = (
85 | );
86 | dependencies = (
87 | );
88 | name = VisionCreditScan;
89 | productName = VisionCreditScan;
90 | productReference = 1E5C776D23DEF09B00AEB4C5 /* VisionCreditScan.app */;
91 | productType = "com.apple.product-type.application";
92 | };
93 | /* End PBXNativeTarget section */
94 |
95 | /* Begin PBXProject section */
96 | 1E5C776523DEF09A00AEB4C5 /* Project object */ = {
97 | isa = PBXProject;
98 | attributes = {
99 | LastSwiftUpdateCheck = 1130;
100 | LastUpgradeCheck = 1130;
101 | ORGANIZATIONNAME = iowncode;
102 | TargetAttributes = {
103 | 1E5C776C23DEF09B00AEB4C5 = {
104 | CreatedOnToolsVersion = 11.3;
105 | };
106 | };
107 | };
108 | buildConfigurationList = 1E5C776823DEF09A00AEB4C5 /* Build configuration list for PBXProject "VisionCreditScan" */;
109 | compatibilityVersion = "Xcode 9.3";
110 | developmentRegion = en;
111 | hasScannedForEncodings = 0;
112 | knownRegions = (
113 | en,
114 | Base,
115 | );
116 | mainGroup = 1E5C776423DEF09A00AEB4C5;
117 | productRefGroup = 1E5C776E23DEF09B00AEB4C5 /* Products */;
118 | projectDirPath = "";
119 | projectRoot = "";
120 | targets = (
121 | 1E5C776C23DEF09B00AEB4C5 /* VisionCreditScan */,
122 | );
123 | };
124 | /* End PBXProject section */
125 |
126 | /* Begin PBXResourcesBuildPhase section */
127 | 1E5C776B23DEF09B00AEB4C5 /* Resources */ = {
128 | isa = PBXResourcesBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | 1E5C777D23DEF09F00AEB4C5 /* LaunchScreen.storyboard in Resources */,
132 | 1E5C777A23DEF09F00AEB4C5 /* Assets.xcassets in Resources */,
133 | 1E5C777823DEF09B00AEB4C5 /* Main.storyboard in Resources */,
134 | );
135 | runOnlyForDeploymentPostprocessing = 0;
136 | };
137 | /* End PBXResourcesBuildPhase section */
138 |
139 | /* Begin PBXSourcesBuildPhase section */
140 | 1E5C776923DEF09B00AEB4C5 /* Sources */ = {
141 | isa = PBXSourcesBuildPhase;
142 | buildActionMask = 2147483647;
143 | files = (
144 | 1E5C777523DEF09B00AEB4C5 /* ViewController.swift in Sources */,
145 | 1E5C777123DEF09B00AEB4C5 /* AppDelegate.swift in Sources */,
146 | 1EEBC59E23DF6B58004BE6E3 /* TextExtractorVC.swift in Sources */,
147 | 1E5C777323DEF09B00AEB4C5 /* SceneDelegate.swift in Sources */,
148 | );
149 | runOnlyForDeploymentPostprocessing = 0;
150 | };
151 | /* End PBXSourcesBuildPhase section */
152 |
153 | /* Begin PBXVariantGroup section */
154 | 1E5C777623DEF09B00AEB4C5 /* Main.storyboard */ = {
155 | isa = PBXVariantGroup;
156 | children = (
157 | 1E5C777723DEF09B00AEB4C5 /* Base */,
158 | );
159 | name = Main.storyboard;
160 | sourceTree = "";
161 | };
162 | 1E5C777B23DEF09F00AEB4C5 /* LaunchScreen.storyboard */ = {
163 | isa = PBXVariantGroup;
164 | children = (
165 | 1E5C777C23DEF09F00AEB4C5 /* Base */,
166 | );
167 | name = LaunchScreen.storyboard;
168 | sourceTree = "";
169 | };
170 | /* End PBXVariantGroup section */
171 |
172 | /* Begin XCBuildConfiguration section */
173 | 1E5C777F23DEF09F00AEB4C5 /* Debug */ = {
174 | isa = XCBuildConfiguration;
175 | buildSettings = {
176 | ALWAYS_SEARCH_USER_PATHS = NO;
177 | CLANG_ANALYZER_NONNULL = YES;
178 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
180 | CLANG_CXX_LIBRARY = "libc++";
181 | CLANG_ENABLE_MODULES = YES;
182 | CLANG_ENABLE_OBJC_ARC = YES;
183 | CLANG_ENABLE_OBJC_WEAK = YES;
184 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
185 | CLANG_WARN_BOOL_CONVERSION = YES;
186 | CLANG_WARN_COMMA = YES;
187 | CLANG_WARN_CONSTANT_CONVERSION = YES;
188 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
189 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
190 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
191 | CLANG_WARN_EMPTY_BODY = YES;
192 | CLANG_WARN_ENUM_CONVERSION = YES;
193 | CLANG_WARN_INFINITE_RECURSION = YES;
194 | CLANG_WARN_INT_CONVERSION = YES;
195 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
196 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
197 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
198 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
200 | CLANG_WARN_STRICT_PROTOTYPES = YES;
201 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
203 | CLANG_WARN_UNREACHABLE_CODE = YES;
204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
205 | COPY_PHASE_STRIP = NO;
206 | DEBUG_INFORMATION_FORMAT = dwarf;
207 | ENABLE_STRICT_OBJC_MSGSEND = YES;
208 | ENABLE_TESTABILITY = YES;
209 | GCC_C_LANGUAGE_STANDARD = gnu11;
210 | GCC_DYNAMIC_NO_PIC = NO;
211 | GCC_NO_COMMON_BLOCKS = YES;
212 | GCC_OPTIMIZATION_LEVEL = 0;
213 | GCC_PREPROCESSOR_DEFINITIONS = (
214 | "DEBUG=1",
215 | "$(inherited)",
216 | );
217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
219 | GCC_WARN_UNDECLARED_SELECTOR = YES;
220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
221 | GCC_WARN_UNUSED_FUNCTION = YES;
222 | GCC_WARN_UNUSED_VARIABLE = YES;
223 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
224 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
225 | MTL_FAST_MATH = YES;
226 | ONLY_ACTIVE_ARCH = YES;
227 | SDKROOT = iphoneos;
228 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
229 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
230 | };
231 | name = Debug;
232 | };
233 | 1E5C778023DEF09F00AEB4C5 /* Release */ = {
234 | isa = XCBuildConfiguration;
235 | buildSettings = {
236 | ALWAYS_SEARCH_USER_PATHS = NO;
237 | CLANG_ANALYZER_NONNULL = YES;
238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
240 | CLANG_CXX_LIBRARY = "libc++";
241 | CLANG_ENABLE_MODULES = YES;
242 | CLANG_ENABLE_OBJC_ARC = YES;
243 | CLANG_ENABLE_OBJC_WEAK = YES;
244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
245 | CLANG_WARN_BOOL_CONVERSION = YES;
246 | CLANG_WARN_COMMA = YES;
247 | CLANG_WARN_CONSTANT_CONVERSION = YES;
248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
251 | CLANG_WARN_EMPTY_BODY = YES;
252 | CLANG_WARN_ENUM_CONVERSION = YES;
253 | CLANG_WARN_INFINITE_RECURSION = YES;
254 | CLANG_WARN_INT_CONVERSION = YES;
255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
260 | CLANG_WARN_STRICT_PROTOTYPES = YES;
261 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
263 | CLANG_WARN_UNREACHABLE_CODE = YES;
264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
265 | COPY_PHASE_STRIP = NO;
266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
267 | ENABLE_NS_ASSERTIONS = NO;
268 | ENABLE_STRICT_OBJC_MSGSEND = YES;
269 | GCC_C_LANGUAGE_STANDARD = gnu11;
270 | GCC_NO_COMMON_BLOCKS = YES;
271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
273 | GCC_WARN_UNDECLARED_SELECTOR = YES;
274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
275 | GCC_WARN_UNUSED_FUNCTION = YES;
276 | GCC_WARN_UNUSED_VARIABLE = YES;
277 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
278 | MTL_ENABLE_DEBUG_INFO = NO;
279 | MTL_FAST_MATH = YES;
280 | SDKROOT = iphoneos;
281 | SWIFT_COMPILATION_MODE = wholemodule;
282 | SWIFT_OPTIMIZATION_LEVEL = "-O";
283 | VALIDATE_PRODUCT = YES;
284 | };
285 | name = Release;
286 | };
287 | 1E5C778223DEF09F00AEB4C5 /* Debug */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
291 | CODE_SIGN_STYLE = Automatic;
292 | DEVELOPMENT_TEAM = M6P56H6G97;
293 | INFOPLIST_FILE = VisionCreditScan/Info.plist;
294 | LD_RUNPATH_SEARCH_PATHS = (
295 | "$(inherited)",
296 | "@executable_path/Frameworks",
297 | );
298 | PRODUCT_BUNDLE_IDENTIFIER = com.iowncode.VisionCreditScan;
299 | PRODUCT_NAME = "$(TARGET_NAME)";
300 | SWIFT_VERSION = 5.0;
301 | TARGETED_DEVICE_FAMILY = "1,2";
302 | };
303 | name = Debug;
304 | };
305 | 1E5C778323DEF09F00AEB4C5 /* Release */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
309 | CODE_SIGN_STYLE = Automatic;
310 | DEVELOPMENT_TEAM = M6P56H6G97;
311 | INFOPLIST_FILE = VisionCreditScan/Info.plist;
312 | LD_RUNPATH_SEARCH_PATHS = (
313 | "$(inherited)",
314 | "@executable_path/Frameworks",
315 | );
316 | PRODUCT_BUNDLE_IDENTIFIER = com.iowncode.VisionCreditScan;
317 | PRODUCT_NAME = "$(TARGET_NAME)";
318 | SWIFT_VERSION = 5.0;
319 | TARGETED_DEVICE_FAMILY = "1,2";
320 | };
321 | name = Release;
322 | };
323 | /* End XCBuildConfiguration section */
324 |
325 | /* Begin XCConfigurationList section */
326 | 1E5C776823DEF09A00AEB4C5 /* Build configuration list for PBXProject "VisionCreditScan" */ = {
327 | isa = XCConfigurationList;
328 | buildConfigurations = (
329 | 1E5C777F23DEF09F00AEB4C5 /* Debug */,
330 | 1E5C778023DEF09F00AEB4C5 /* Release */,
331 | );
332 | defaultConfigurationIsVisible = 0;
333 | defaultConfigurationName = Release;
334 | };
335 | 1E5C778123DEF09F00AEB4C5 /* Build configuration list for PBXNativeTarget "VisionCreditScan" */ = {
336 | isa = XCConfigurationList;
337 | buildConfigurations = (
338 | 1E5C778223DEF09F00AEB4C5 /* Debug */,
339 | 1E5C778323DEF09F00AEB4C5 /* Release */,
340 | );
341 | defaultConfigurationIsVisible = 0;
342 | defaultConfigurationName = Release;
343 | };
344 | /* End XCConfigurationList section */
345 | };
346 | rootObject = 1E5C776523DEF09A00AEB4C5 /* Project object */;
347 | }
348 |
--------------------------------------------------------------------------------