Áp dụng Tesseract vào iOS

Áp dụng Tesseract vào iOS

Giới thiệu

Tesseract là 1 OCR engine hỗ trợ trên nhiều nền tảng khác nhau, miễn phí, bản quyền Apache và được hỗ trợ phát triển bởi Google từ năm 2006 cho tới nay. Đây là 1 OCR engine phổ biến và hiệu quả nhất (cho tới hiện nay), sử dụng AI(trí tuệ nhân tạo) để xác định và nhận diện chữ.  Các hệ điều hành được hỗ trợ: MacOS, Window, Linux, nhưng có thể biên dịch cho iOS và Android để chạy trên 2 nền tảng này.

Đây là souce của Tessearct.

https://github.com/tesseract-ocr/tesseract

Số lượng  ngôn ngữ hỗ trợ lên đến 100, với mỗi file .traineddata là mô hình ngôn ngữ đã được huấn luyện.

https://github.com/tesseract-ocr/tessdata 

Sau đây tôi sẽ trình bày cách áp dụng Tesseract trong iOS.

Môi trường phát triển

  • Macos Catalina 10.15.2
  • Xcode 11.3, Swift 5
  • Tesseract 4.1.1
  • Leptonica 1.79.0
  • OpenCV 4.2.0

Tải xuống và nhúng các thư viện cần thiết

Trước tiên hãy tạo mới 1 project với chế độ Single View App

Tải xuống Tesseract 4.1.1 phiên bản dành cho iOS

(Đây là phiên bản đã được biên dịch CHỈ dành cho iOS từ nguồn https://github.com/tesseract-ocr/tesseract)

https://github.com/kang298/Tesseract-builds-for-iOS/tree/tesseract-4.1.1

Sau khi tải xuống và giải nén, chúng ta sẽ có 2 thư mục “include” và “lib”, hãy kéo thả chúng vào project của bạn.

Tải xuống OpenCV

Tải từ link sau  https://opencv.org/releases/ và kéo thả chúng vào project của bạn.

Bấm command + R để chạy lại project để đảm bảo không có lỗi phát sinh.

Tải xuống các file ngôn ngữ đã được huấn luyện

Link tải  xuống:

https://github.com/tesseract-ocr/tessdata

Trong bài viết này chúng ta sẽ kiểm tra thử với 3 ngôn ngữ: tiếng Anh, Nhật, Việt Nam. Do đó hãy tải xuống các mô hình sau đây và lưu chúng vào 1 thư mục đặt tên là  tessdata.

  • eng.traineddata
  • jpn.traineddata
  • vie.traineddata

Sau đó kéo thả thư mục trên vào xcode project. CHÚ Ý: chọn chế độ “Create folder references” thay vì “Create Group” khi thêm thư mục vào dự án.

Tiến hành lập trình

Bởi vì Tesseract được phát triển bằng ngôn ngử C++ cho nên chúng ta chỉ có thể lập trình bằng C++. Tạo 1 file C++ có tên là tesseract_wrapper.cpp như trong hình.

Lưu ý: nhớ chọn “Also create a header file” để Xcode tạo 1 file tesseract_wrapper.hpp sẵn.

tesseract_wrapper.hpp

//
//  tesseract_wrapper.hpp
//  TestTesseract
//
//  Created by Briswell on 1/13/20.
//  Copyright © 2020 Briswell. All rights reserved.
//

#ifndef tesseract_wrapper_hpp
#define tesseract_wrapper_hpp

#include "opencv2/imgproc.hpp"
#include "stdio.h"

using namespace cv;
String ocrUsingTesseractCPP(String image_path,String data_path,String language);

#endif /* tesseract_wrapper_hpp */

tesseract_wrapper.cpp

//
//  tesseract_wrapper.cpp
//  TestTesseract
//
//  Created by Briswell on 1/13/20.
//  Copyright © 2020 Briswell. All rights reserved.
//

#include "allheaders.h"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "baseapi.h"
#include "tesseract_wrapper.hpp"

using namespace cv;
using namespace tesseract;

/*
 matToPix():
    convert from OpenCV Image Container to Leptonica's Pix Struct
 Params:
    mat: OpenCV Mat image Container
 Output
    Leptonica's Pix Struct
 */
Pix* matToPix(Mat *mat){
    int image_depth = 8;
    //create a Leptonica's Pix Struct with width, height of OpenCV Image Container
    Pix *pixd = pixCreate(mat->size().width, mat->size().height, image_depth);
    for(int y=0; yrows; y++) {
        for(int x=0; xcols; x++) {
            pixSetPixel(pixd, x, y, (l_uint32) mat->at(y,x));
        }
    }
    return pixd;
}

/*
 ocrUsingTesseractCPP():
    Using Tesseract engine to read text from image
 Params:
    image_path: path to image
    data_path: path to folder containing .traineddata files
    language: expeted language to detect (eng,jpn,..)
 Output:
    String detected from image
 */
String ocrUsingTesseractCPP(String image_path,String data_path,String language){
    //load a Mat Image Container from image's path and gray scale mode
    Mat image = imread(image_path,IMREAD_GRAYSCALE);
    TessBaseAPI* tessEngine = new TessBaseAPI();
    //Tesseract 4 adds a new neural net (LSTM) based OCR engine which is focused on line recognition, but also still supports the legacy Tesseract OCR engine of Tesseract 3 which works by recognizing character patterns, in this tutorial we just focus on LSTM only
    OcrEngineMode mode = tesseract::OEM_LSTM_ONLY;
    
    //init Tesseract engine
    tessEngine->Init(data_path.c_str(), language.c_str(), mode);
    
    //Set mode for page layout analysis, refer for all modes supporting
    //https://tesseract.patagames.com/help/html/T_Patagames_Ocr_Enums_PageSegMode.htm
    PageSegMode pageSegMode = tesseract::PSM_SINGLE_BLOCK;
    tessEngine->SetPageSegMode(pageSegMode);
    
    //increase accuracy for japanese
    if(language.compare("jpn") == 0){
        tessEngine->SetVariable("chop_enable", "true");
        tessEngine->SetVariable("use_new_state_cost", "false");
        tessEngine->SetVariable("segment_segcost_rating", "false");
        tessEngine->SetVariable("enable_new_segsearch", "0");
        tessEngine->SetVariable("language_model_ngram_on", "0");
        tessEngine->SetVariable("textord_force_make_prop_words", "false");
        tessEngine->SetVariable("edges_max_children_per_outline", "40");
    }
    
    
    //convert from OpenCV Image Container to Leptonica's Pix Struct
    Pix *pixImage = matToPix(&image);
    //set Leptonica's Pix Struct to Tesseract engine
    tessEngine->SetImage(pixImage);
    
    //get recognized text in UTF8 encoding
    char *text = tessEngine->GetUTF8Text();
    
    //release Tesseract's cache
    tessEngine->End();
    pixDestroy(&pixImage);
    
    return text;
}

 

Bởi vì Swift không thể gọi trực tiếp các hàm của C++ nên phải có file trung gian objective-C.

  • TesseractWrapper.h
  • TesseractWrapper.mm (đuôi file phải là .mm để cho viêc biên dịch C++)

TesseractWrapper.h

//
//  TesseractWrapper.h
//  TestTesseract
//
//  Created by Briswell on 1/13/20.
//  Copyright © 2020 Briswell. All rights reserved.
//

#import "Foundation/Foundation.h"
#import "UIKit/UIKit.h"
@interface TesseractWrapper : NSObject
+(NSString*)ocrUsingTesseractObjectiveC:(UIImage*)image language:(NSString*)language;
@end

TesseractWrapper.mm

//
//  TesseractWrapper.m
//  TestTesseract
//
//  Created by Briswell on 1/13/20.
//  Copyright © 2020 Briswell. All rights reserved.
//

#import "TesseractWrapper.h"

#include "tesseract_wrapper.hpp"
@implementation TesseractWrapper

/*
ocrUsingTesseractObjectiveC()
    call ocrUsingTesseractCPP() to recognize  text from image
 params:
    image: image to recognize text
    language: eng/jpn/vie
 output:
    recognized string
 */
+(NSString*)ocrUsingTesseractObjectiveC:(UIImage*)image language:(NSString*)language{
    //get path of folder containing .traineddata files
    NSString* data_path = [NSString stringWithFormat:@"%@/tessdata/",[[NSBundle mainBundle] bundlePath]];
    //save image to app's cache directory
    NSString* cache_dir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString* image_path = [NSString stringWithFormat:@"%@/image.jpeg",cache_dir];
    NSData* data = UIImageJPEGRepresentation(image, 0.5);
    NSURL* url = [NSURL fileURLWithPath:image_path];
    [data writeToURL:url atomically:true];
    
    //get text from image using ocrUsingTesseractCPP() from file tesseract_wrapper.hpp
    String str = ocrUsingTesseractCPP([image_path UTF8String], [data_path UTF8String], [language UTF8String]);
    NSString* result_string = [NSString stringWithCString:str.c_str()
    encoding:NSUTF8StringEncoding];
    //remove cached image
    [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
    return result_string;
}
@end

Tạo 1 màn hình đơn giản chứa 1 textview và 1 button cho file ViewController.swift.

 

ViewController.swift

//
//  ViewController.swift
//  TestTesseract
//
//  Created by Briswell on 1/13/20.
//  Copyright © 2020 Briswell. All rights reserved.
//

import UIKit
import CropViewController

class ViewController: UIViewController {

    @IBOutlet weak var txt: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func ocr(_ sender: Any) {
        //if camera not supported
        if !UIImagePickerController.isSourceTypeAvailable(.camera){
            return
        }
        //present camera to take image
        let pickerController = UIImagePickerController()
        pickerController.delegate = self as UIImagePickerControllerDelegate & UINavigationControllerDelegate
        pickerController.sourceType = .camera
        self.present(pickerController, animated: true, completion: nil)
    }
    
}

extension ViewController: UIImagePickerControllerDelegate,UINavigationControllerDelegate{
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true) {
            guard let image = info[.originalImage] as? UIImage else { return  }
            //present a crop image frame to focus on text content
            let cropViewController = CropViewController.init(image: image)
            cropViewController.delegate = self
            self.present(cropViewController, animated: true, completion: nil)
        }
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}

extension ViewController:CropViewControllerDelegate{
    func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
            cropViewController.dismiss(animated: true) {
                //call objective-c wrapper with expected language
                let str = TesseractWrapper.ocr(usingTesseract: image, language: "jpn")
                self.txt.text = str
            }
        }
        
        func cropViewController(_ cropViewController: CropViewController, didFinishCancelled cancelled: Bool) {            
            cropViewController.dismiss(animated: true, completion: nil)            
        }
}

Sau đây là kết quả kiểm thử với ngôn ngữ tiếng Nhật, chúng ta có thể làm tương tự với 2 ngôn ngữ Anh và Việt.

 

Lời kết

Nhận diện chữ trong hình ảnh hoàn toàn có thể nhưng có nhiều vấn đề xung quanh. Vấn đề chủ yếu lớn nhất là chất lượng của hình (độ tương phản, độ sáng, kích cỡ,..). Và mỗi hình lại có những vấn đề khác nhau do đó chúng ta có thể tạo 1 công cụ lọc màu để người dùng tự xử lý hình ảnh trước khi đưa vào xử lý nhận diện. Dưới đây là link tham khảo cho việc cải thiện chất lượng hình ảnh.

https://github.com/tesseract-ocr/tesseract/wiki/ImproveQuality