├── 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 | ![alt-text](https://github.com/anupamchugh/VisionCreditScan/blob/master/vision-credit-card-scanner-output.gif) 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 | --------------------------------------------------------------------------------