Text Detection in iOS11

Chúng ta sẽ dùng Vision Framework của iOS11 để xây dựng một ứng dụng sẽ có thể phát hiện text bất kể font, đối tượng và màu sắc, nó có thể nhận ra là text được in và viết tay. Lưu ý là project này sẽ cần XCode9device chạy iOS11 để test.

Tạo camera với AVCapture

Ta khởi tạo 1 object AVCaptureSession để thực hiện real-time hay offline capture.

var session = AVCaptureSession()

Tiếp theo, ta thực hiện kết nối session đến device

func startVideo() {

        session.sessionPreset = AVCaptureSession.Preset.photo
        let captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
        
        let deviceInput = try! AVCaptureDeviceInput(device: captureDevice!)
        let deviceOutput = AVCaptureVideoDataOutput()
        deviceOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
        deviceOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: DispatchQoS.QoSClass.default))
        session.addInput(deviceInput)
        session.addOutput(deviceOutput)
        
        let imageLayer = AVCaptureVideoPreviewLayer(session: session)
        imageLayer.frame = imageView.bounds
        imageView.layer.addSublayer(imageLayer)
        
        session.startRunning()
    }
  1. Ta setting cho AVCaptureSession và set AVMediaType là video vì ta sẽ thực hiện quay camera do đó nó sẽ liên tục chạy
  2. Tiếp theo, ta định nghĩa device output và input. Input sẽ là những gì camera nhìn thấy và output là video. Ta set type cho video format là kCVPixelFormatType_32BGRA. Tham khảo thêm type cho video https://developer.apple.com/documentation/corevideo/cvpixelformatdescription/1563591-pixel_format_types
  3. Cuối cùng, ta add sublayer chứa video vào imageView và start session. Ta gọi hàm này trong viewDidLoad. Ngoài ra ta còn cần set lại frame của layer.
override func viewDidLoad() {
        super.viewDidLoad()
        
        startLiveVideo()
    }
override func viewDidLayoutSubviews() {
        imageView.layer.sublayers?[0].frame = imageView.bounds
    }

Lưu ý là phải set Privacy - Camera Usage Description trong plist. Vậy là ta đã xong bước tạo camera với AVCapture.

Text Detection

Trước tiên, ta cần hiểu cách hoạt động của Vision framework. Về cơ bản, có 3 bước để implement vision vào app:

  • Requests: là khi ta yêu cầu framework detect
  • Handlers: là khi ta muốn framework thực hiện cái gì đó sau khi request được gọi
  • Observations: là những gì ta muốn làm với dữ liệu được cung cấp Ta bắt đầu với 1 request.
var requests = [VNRequest]()
func startTextDetection() {
        let textRequest = VNDetectTextRectanglesRequest(completionHandler: self.detectTextHandler)
        textRequest.reportCharacterBoxes = true
        self.requests = [textRequest]
    }
    func detectTextHandler(request: VNRequest, error: Error?) {
        guard let observations = request.results else {
            print("no result")
            return
        }
        
        let result = observations.map({$0 as? VNTextObservation})
        
    }

Ta tạo 1 text request là VNDetectTextRectanglesRequest. Về cơ bản nó chỉ là một loại VNRequest mà chỉ viền xung quanh các text. Khi framework hoàn thành request, ta sẽ gọi hàm detectTextHandler. Ta cũng sẽ muốn biết chính xác cái mà framework recognized cho nên ta set biến reportCharacterBoxes = true. Ngoài ra, ta bắt đầu định nghĩa observations chứa tất cả kết quả của VNDetectTextRectanglesRequest. Ta add Vision vào camera output.

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        
        var requestOptions:[VNImageOption : Any] = [:]
        
        if let camData = CMGetAttachment(sampleBuffer, kCMSampleBufferAttachmentKey_CameraIntrinsicMatrix, nil) {
            requestOptions = [.cameraIntrinsics:camData]
        }
        
        let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: CGImagePropertyOrientation(rawValue: 6)!, options: requestOptions)
        
        do {
            try imageRequestHandler.perform(self.requests)
        } catch {
            print(error)
        }
    }
}

Về cơ bản hàm trên sẽ kiểm tra nếu CMSampleBuffer tồn tại và đưa ra AVCaptureOutput. Sau đó tạo 1 biến requestOptions là 1 dictionary loại VNImageOption. VNImageOption là loại cấu trúc có thể chứa các thuộc tính và dữ liệu từ camera. Cuối cùng ta tạo VNImageRequestHandler và thực hiện text request

Viền xung quanh text detected

Ta sẽ vẽ 2 loại: 1 là cho mỗi kí tự detect được, 2 là cho cả từ. Ta sẽ bắt đầu với từ

func highlightWord(box: VNTextObservation) {
        guard let boxes = box.characterBoxes else {
            return
        }
        
        var maxX: CGFloat = 9999.0
        var minX: CGFloat = 0.0
        var maxY: CGFloat = 9999.0
        var minY: CGFloat = 0.0
        
        for char in boxes {
            if char.bottomLeft.x < maxX {
                maxX = char.bottomLeft.x
            }
            if char.bottomRight.x > minX {
                minX = char.bottomRight.x
            }
            if char.bottomRight.y < maxY {
                maxY = char.bottomRight.y
            }
            if char.topRight.y > minY {
                minY = char.topRight.y
            }
        }
        
        let xCord = maxX * imageView.frame.size.width
        let yCord = (1 - minY) * imageView.frame.size.height
        let width = (minX - maxX) * imageView.frame.size.width
        let height = (minY - maxY) * imageView.frame.size.height
        
        let outline = CALayer()
        outline.frame = CGRect(x: xCord, y: yCord, width: width, height: height)
        outline.borderWidth = 2.0
        outline.borderColor = UIColor.red.cgColor
        
        imageView.layer.addSublayer(outline)
    }

Sau đó là cho từng kí tự

func highlightLetters(box: VNRectangleObservation) {
        let xCord = box.topLeft.x * imageView.frame.size.width
        let yCord = (1 - box.topLeft.y) * imageView.frame.size.height
        let width = (box.topRight.x - box.bottomLeft.x) * imageView.frame.size.width
        let height = (box.topLeft.y - box.bottomLeft.y) * imageView.frame.size.height
        
        let outline = CALayer()
        outline.frame = CGRect(x: xCord, y: yCord, width: width, height: height)
        outline.borderWidth = 1.0
        outline.borderColor = UIColor.blue.cgColor
        
        imageView.layer.addSublayer(outline)
    }

Sau đó cập nhật hàm detectTextHandler

func detectTextHandler(request: VNRequest, error: Error?) {
        guard let observations = request.results else {
            print("no result")
            return
        }
        
        let result = observations.map({$0 as? VNTextObservation})
        
        DispatchQueue.main.async() {
            self.imageView.layer.sublayers?.removeSubrange(1...)
            for region in result {
                guard let rg = region else {
                    continue
                }
                
                self.highlightWord(box: rg)
                
                if let boxes = region?.characterBoxes {
                    for characterBox in boxes {
                        self.highlightLetters(box: characterBox)
                    }
                }
            }
        }
    }

Build app ta được