Blog

Simple OCR Android App Tutorial (Dùng Tesseract OCR)

Như đã đề cập ở bài trước, chúng ta đã biết Tesseract hỗ trợ cho việc nhận diện chữ viết trên hình ảnh khá là hiệu quả, đặc biệt là các ngôn ngữ thông dụng như tiếng Anh. Bên cạnh đó, công cụ này còn hỗ trợ rất nhiều ngôn ngữ, bao gồm cả tiếng Việt.

Với bài này, mình sẽ trình bày một bài hướng dẫn step-by-step với mục đích là xây dựng một ứng dụng Android đơn giản, có thể nhận biết tiếng Việt trong hình ảnh với sự hỗ trợ của Tesseract OCR.

Để có thể sử dụng bài hướng dẫn này một cách dễ dàng, người đọc sẽ cần có một số phương tiện cơ bản sau:

  • Android Studio: IDE hỗ trợ lập trình Android
  • Tập tin chứa dữ liệu ngôn ngữ
  • Hình ảnh chữ

Bài hướng dẫn này sẽ gồm các bước chính như sau:

  1. Tạo dự án Android
  2. Khai báo dependency
  3. Chuẩn bị hình ảnh
  4. Thiết kế Activity
  5. Khởi tạo các phương thức cơ bản
  6. Copy training data vào thiết bị
  7. Xử lý hình ảnh
  8. Chạy ứng dụng

1. Tạo dự án Android

Khởi động Android Studio, khởi tạo một dự án với khởi đầu là một Empty Activity.

createnew

2. Khai báo dependency

Chúng ta sẽ khai báo dependency cho ứng dụng trong tập tin gradle.build của thư mục app với nội dung như sau:

compile 'com.rmtheis:tess-two:5.4.1'

Như vậy, tập tin gradle.build của chúng ta sẽ có nội dung phần dependency như sau:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.rmtheis:tess-two:5.4.1'
    testCompile 'junit:junit:4.12'
}

3. Chuẩn bị hình ảnh

Với một ứng dụng OCR đơn giản, ta nên chuẩn bị một hình ảnh đơn giản với chữ đen, rõ ràng trên nền trắng.

vie_tnq

4. Thiết kế Activity

Trong bài hướng dẫn này, chúng ta sẽ xây dựng ứng dụng Android nhận diện văn bản đơn giản gồm 01 ImageView, 01 button và 01 TextView

Screenshot_2017-07-30-19-37-28

Như vậy, giao diện trên có thể sẽ được định nghĩa bởi file layout như sau

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/activity_main"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:paddingBottom="@dimen/activity_vertical_margin"
 android:paddingLeft="@dimen/activity_horizontal_margin"
 android:paddingRight="@dimen/activity_horizontal_margin"
 android:paddingTop="@dimen/activity_vertical_margin"
 tools:context="sample.tesseract.tesseractocrdemo.MainActivity">

<ImageView
 android:id="@+id/img_input"
 android:layout_width="match_parent"
 android:layout_height="wrap_content" />

<Button
 android:id="@+id/btn_rec"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:onClick="doRecognize"
 android:text="@string/btn_action_name"
 android:layout_below="@id/img_input"/>

<TextView
 android:id="@+id/txt_result"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Your result is here..."
 android:layout_below="@id/btn_rec"/>
</RelativeLayout>

5. Khởi tạo các phương thức cơ bản

Trước hết, bên cạnh các phương thức cơ bản để hiển thị một activity trong ứng dụng Android, bạn sẽ cần phải khởi tạo một biến thuộc class TessBaseAPI để sử dụng các chức năng liên quan đến Tesseract OCR. Ở đây, mình sẽ thực hiện như sau:

public class MainActivity extends AppCompatActivity {
 private TessBaseAPI m_tess;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   initImageView();
   try {
     prepareLanguageDir();
     m_tess = new TessBaseAPI();
     m_tess.init(getFilesDir(), "vie");
   } catch (Exception e) {
     // Logging here
   }
 }

 private void initImageView() {
   ImageView imgView = (ImageView) findViewById(R.id.img_input);
   Bitmap input = BitmapFactory.decodeResource(getResources(), R.drawable.vie_tnq);
   imgView.setImageBitmap(input);
 }
}

Ta có thể thấy được các API của Tesseract sẽ được truy cập thông qua biến m_tess (class TessBaseAPI) và để khởi tạo biến này, ta sẽ cần chỉ định đường dẫn của thư mục chứa tập tin dữ liệu ngôn ngữ và loại ngôn ngữ được sử dụng. Ở đây, chúng ta sẽ dùng thư mục bên trong thư mục data của ứng dụng và ngôn ngữ là tiếng Việt.

Nên lưu ý rằng, thư mục được chỉ định phải chứa thư mục con có tên tessdata. Thư mục tessdata đó sẽ chứa các tập tin dữ liệu ngôn ngữ.

6. Copy training data vào thiết bị

Trước tiên, chúng ta cần phải chuẩn bị tập tin dữ liệu ngôn ngữ. Bạn có thể download dữ liệu cần thiết được chia sẻ trên Github tessdata. Ở đây, mình sẽ download tập tin vie.trainneddata và lưu trong thư mục assets/tessdata của ứng dụng.

assetsFolder

Như đã nói, chúng ta sẽ lưu tập tin trong thư mục assets nên ứng dụng sẽ không thể truy cập trên thiết bị. Do đó, chúng ta sẽ cần có một bước chuyển file vào thiết bị thông qua phương thức prepareLanguageDir() như sau:

public class MainActivity extends AppCompatActivity {
 // Other methods...

 // copy file from assets to another folder due to accessible
 private void copyFile() throws IOException {
   // work with assets folder
   AssetManager assMng = getAssets();
   InputStream is = assMng.open("tessdata/vie.traineddata");
   OutputStream os = new FileOutputStream(getFilesDir() + 
                                          "/tessdata/vie.traineddata");
   byte[] buffer = new byte[1024];
   int read;
   while ((read = is.read(buffer)) != -1) {
     os.write(buffer, 0, read);
   }

   is.close();
   os.flush();
   os.close();
 }

 private void prepareLanguageDir() throws IOException {
   File dir = new File(getFilesDir() + "/tessdata");
   if (!dir.exists()) {
     dir.mkdirs();
   }

   File trainedData = new File(getFilesDir() + "/tessdata/vie.traineddata");
   if (!trainedData.exists()) {
     copyFile();
   }
 }

7. Xử lý hình ảnh

Tới bước này, chúng ta đã hoàn tất những điều kiện cơ bản để có thể nhận diện ký tự trên một hình ảnh rõ ràng. Tiếp theo, chúng ta sẽ sử dụng API của Tesseract để nhận diện các kí tự. Với bài hướng dẫn này, việc nhận diện sẽ được khởi động bởi sự kiện nhấn button “Do Recognize” như sau:

public class MainActivity extends AppCompatActivity {
 // Other methods ...
 public void doRecognize() {
   if (m_tess == null) {
     return;
   }

   try {
     m_tess.setImage(BitmapFactory.decodeResource(getResources(), R.drawable.vie_tnq));
     String result = m_tess.getUTF8Text();
     TextView resultView = (TextView) findViewById(R.id.txt_result);
     resultView.setText(result);
   } catch (Exception e) {
     // Do what you like here...
   }
  }
 }

8. Chạy ứng dụng

Và bây giờ, bạn chạy thử ứng dụng và xem xem có đạt được kết quả mong muôn

Tesseract_OCR engine

Tesseract là một OCR (Optical Character Recognition) engine hàng đầu hiện nay. Công cụ này được phân phối với bản quyền mã nguồn mở Apache 2.0. Nó hỗ trợ nhận diện kí tự trên các tập tin hình ảnh và xuất ra dưới dạng kí tự thuần, html, pdf, tsv, invisible-text-only pdf. Người dùng có thể sử dụng trực tiếp hoặc lập trình viên có thể sử dụng các chức năng thông qua API.

Tesseract được phát triển bởi Hewlett-Packard Laboratories Bristol tại Hewleett-Packard Co, Greeley Colorado từ 1985 đến 1994. Sau đó, nó được cập nhật một số thay đổi nhỏ và tạm ngưng phát triển từ sau 1998. Đến năm 2005, Tesseract được phân bố dưới dạng mã nguồn mở bởi HP và được phát triển bởi Google từ năm 2006.

Hiện tại, Tesseract đã phát triển đến version 3.0x và có thể hoạt động trên 3 hệ điều hành phổ biến là Window, Mac và Linux. Công cụ này hỗ trợ nhận diện kí tự của hơn 100 ngôn ngữ khác nhau, bao gồm cả tiếng Việt. Không những thế, chúng ta có thể huấn luyện chương trình dùng Tesseract để có thể nhận diện một ngôn ngữ nào đó. Bên cạnh đó, mã nguồn mở này không hỗ trợ GUI, nên bạn sẽ cần tới ứng dụng của bên thứ ba nếu muốn sử dụng chức năng này.

Đối với các lập trình viên, họ có thể sử dụng các API của Tesseract để xây dựng ứng dụng của mình. Thư viện đó gọi là labtesseract và được cung cấp cho ngôn ngữ C/C++. Trong trường hợp bạn sử dụng ngôn ngữ khác thì cần phải sử dụng các gói hỗ trợ tương ứng:

Một lưu ý nhỏ nữa là vì yêu cầu của bản quyền Apache 2.0, bạn sẽ phải ghi rõ thông tin về bản quyền này khi sử dụng Tesseract trong ứng dụng của mình.

Link tham khảo:

Nâng cao hiệu suất của Android Webapp

Webapp có thể là một giải pháp vô cùng hữu ích cho ứng dụng. Tuy nhiên, hiệu suất của nó không phải lúc nào cũng như mong muốn vì còn phụ thuộc vào nhiều yếu tố bên ngoài ứng dụng của bạn.

Để có thể nâng cao hiệu suất của ứng dụng cũng như có thể tối đa hoá lợi ích của việc này, chúng ta có thể thử các cách dưới đây:

  • Huỷ bỏ Javascript và đóng gói module:

Javascript được tung ra dưới dạng source-code form mà sẽ tốn thời gian cho cả các kí tự không hay code, hay là những ký tự không ảnh hưởng đến việc hoạt động. Quá trình này sẽ dẫn đến việc hoạt động kém hiệu quả, đặc biệt là với các ứng dụng lớn, quá trình khởi động sẽ đủ lâu để khiến người dùng muốn ngưng sử dụng.

Bên cạnh đó, việc đóng các module lại sẽ giúp hạn chế số lượng request ứng dụng gửi ra. Điều này giúp hạn chế thời gian chờ. Nhờ đó mà bạn sẽ thấy ứng dụng nhanh hơn nhiều.

  • Tải các tài nguyên theo yêu cầu

Các bạn có thể hiểu tài nguyên ở đây chính là hình ảnh và video. Chúng ta không nên tải chúng ngay khi khởi động ứng dụng, mà chỉ nên tải khi người dùng cần xem, chẳng hạn như khi người dùng nhấn nút play hoặc là đã kéo xuống khung hình.

Để hiểu rõ hơn, các bạn có thể tìm kiếm với từ khoá On-demand Loading hay Lazy Loading

  • Sử dụng mảng các id khi dùng các thư viện DOM

Với React, Ember, AngularJS hay bất cứ thư viện DOM nào, việc sử dụng array-ids (hay track-by) đều sẽ mang lại lợi ích to lớn cho ứng dụng. Việc này sẽ ngăn không để các thư viện này xoá tất cả các node hiện hữu và tạo node mới, mà chỉ cập nhật các node có sẵn dựa vào array-ids (nếu cần).

  • Cache

Caching là một biện pháp hữu hiệu để tăng cường hiệu suất của ứng dụng thông qua việc cải thiện thời gian phản hồi và giảm nhu cầu sử dụng CPU. Vấn đề quan trọng là bạn cần phải xác định được đâu là phần quan trọng để thực hiện caching.

  • Enable HTTP/2

HTTP/2 được giới thiệu là mang lại rất nhiều lợi ích trong việc cho phép nhiều kết nối đồng thời trên một server. Điểm nhấn của nhó so với HTTP/1 chính là độ trễ và hiệu suất. Bạn có thể lưu ý đến vấn đề này khi muốn cải thiện một ứng dụng bất kì trên nền web.

  • Mô tả ứng dụng:

Mô tả ứng dụng có thể sẽ đem lại nhiều lợi ích hơn là bạn tưởng. Việc này sẽ giúp chúng ta nắm được tổng quát về ứng dụng, giúp cho quá trình tối ưu đi đúng định hướng nhằm đạt được lợi ích lớn nhất. Như vậy, để xác định được các nhu cầu của ứng dụng như lượt tải, độ trễ… cũng không đơn giản. Nhưng các bạn có thể sử dụng các công cụ hỗ trợ như Chrome’s Dev Tools.

  • Hạn chế sử dụng các khối Javascript and CSS

Cả Javascript và CSS đều có thể cản trở quá trình hiển thị trang web. Như vậy, chúng ta sẽ có thể tối ưu hoá ứng thông qua việc hạn chế chúng.

Đối với CSS, chúng ta nên chỉ định CSS sẽ được tải với các chế độ nào, chứ không mặc định tải tất cả.

Đối với Javascript, chúng ta có thể dựa nào đoạn script bên trong HTML (inline script). Đoạn code này nên được viết ngắn nhất có thể và đặt ở vị trí mà không cản trở quá trình parsing phần còn lại của trang web. Ngược lại, chúng sẽ trở thành thủ phạm của việc làm giảm hiệu suất.

Bên cạnh các phương án trên, các bạn có thể thử các cách sau:

  • Use a load balancing solution
  • Consider Isomorphic Javascript for faster startup time
  • Speed up database query with indexing
  • Use faster transpiling solution
  • Use service workers and streams
  • Image encoding optimization

Bài viết này được viết dựa vào auth0.

Android Virtual Device

Thiết bị ảo (Android virtual device_AVD) là một công cụ hỗ trợ được cung cấp bởi Android SDK, cho phép người dùng định nghĩa một thiết bị Android giả lập. Thiết bị này có thể là điện thoại, máy tính bảng, đồng hồ hay ti vi.

Để có thể sử dụng loại thiết bị này, chúng ta sẽ cần sử dụng công cụ quản lý AVD Manager. Công cụ này cho phép chúng ta khởi tạo, quản lý, chỉnh sửa và khởi động các thiết bị ảo.

Sau khi đã có AVD, ta có thể bắt đầu chạy thử các ứng dụng trên nó. Tuy nhiên, các thiết bị ảo chạy khá chậm và hiệu suất không cao. Ta có thể áp dụng một số cách sau để cải thiện tình hình:

  1. Sử dụng Intel® Hardware Accelerated Execution Manager: trong trường hợp này, thuộc tính CPU (trong màn hình cài đặt) của thiết bị ảo nên là Intel Atom (x86) và khởi động ứng dụng \android-sdk\extras\intel\Hardware_Accelerated_Execution_Manager\IntelHaxm.exe
  2. Sử dụng một thiết bị ảo khác để thay thế, chẳng hạn như dùng Virtual box

 

 

 

 

 

 

 

 

 

 

Android App Issue: Parsing Error

Ứng dụng di động là một trong những loại ứng dụng được sử dụng rất phổ biến, đặc biệt là trong giai đoạn bùng nổ của các thiết bị di động như hiện nay.

Một trong những hệ điều hành phổ biến của các loại thiết bị này là hệ điều hành Android. Hệ điều hành này chiếm trên 50% thị phần. Điều này dẫn đến nhu cầu sử dụng khá cao của các ứng dụng Android như hiện nay.

Một trong những lỗi khá phổ biến và cũng khá dễ để xử lý là lỗi Parsing Error (Parsing Error – There is a problem parsing the packet).

Parsing Error thường xuất hiện vì một số lý do sau:

  • Phiên bản Android (SdkVersion) của ứng dụng không phù hợp với thiết bị
  • Tập tin APK (.apk) bị lỗi hoặc nội dung bị thiếu khi download
  • Tên của tập tin APK bị thay đổi
  • Thiết bị không đáp ứng được các quyền mà ứng dụng yêu cầu

Như vậy, để tránh hoặc sửa lỗi này, chúng ta cần phải đảm bảo tập tin APK đang dùng tên gốc và thiết bị đáp ứng các yêu cầu của ứng dụng, đặc biệt là yêu cầu trong tập tin AndroidManifest.

Thuật toán chia để trị và quy hoạch động

Có lẽ đa số trong chúng ta sẽ gặp ít nhiều khó khăn khi phải phân biệt thuật toán chia để trị và quy hoạch động (mặc dù vẫn biết cách sử dụng chúng). Điều này cũng không phải là một vấn đề lớn vì cách sử dụng cũng như mục đích sử dụng của chúng khá giống nhau.

Cả hai thuật toán đều hoạt động dựa trên nguyên tắc chia nhỏ bài toán lớn thành các bài toán nhỏ hơn để giải quyết. Từ đó đạt được đáp án mong muốn.

Tuy nhiên, nếu các bạn để ý thì sẽ thấy được hai điểm khác nhau khá rõ giữa chúng. Đầu tiên, chia để trị chỉ xử lý bài toán theo chiều từ trên xuống (top-down) trong khi quy hoạch động hỗ trợ cả hai chiều (top-down và bottom-up). Điểm thứ hai đó là việc chú trọng hiệu suất. Đối với thuật toán chia để trị, ta sẽ gọi đệ quy các bài toán con mà không quan tâm nhiều đến việc hạn chế xử lý các công việc tương tự nhau. Ngược lại, thuật toán quy hoạch động sẽ sử dụng một số kỹ thuật để tận dụng lại những gì có sẵn như memoization.

Để rõ hơn về điểm giống và khác nhau giữa hai thuật toán, bạn có thể xem lại chúng ở hai link sau:

Thuật toán chia để trị

Thuật toán chia để trị là phương pháp giải một bài toán bằng cách chia nó thành các bài toán nhỏ hơn. Từ kết quả của các bài toán con đó, ta tổng hợp lại để thu được đáp án của bài toán ban đầu.

Thông thường, bài toán con được gọi đệ quy từ bài toán cha. Như vậy, việc phân chia này sẽ diễn ra tự động dựa vào điều kiện lập trình viên đề ra. Nói cách khác, quá trình chia nhỏ bài toán sẽ kết thúc dựa vào điều kiện nhất định, thường là khi bài toán đã đủ nhỏ để được giải trực tiếp.

Thuật toán chia để trị có độ ứng dụng khá cao nên ta có thể dễ dàng  tìm thấy trong thực tế. Bạn có thể tìm thấy một số bài toán có thể áp dụng thuật toán chia để trị bên dưới:

  • Merge Sort
  • Quick Sort
  • Bubble Sort
  • Nhân hai ma trận
  • Binary search
  • Tìm cặp điểm gần nhau nhất

Binary search hay còn gọi là tìm kiếm nhị phân là một thuật toán được áp dụng để tìm một giá trị trong một mảng đã được sắp xếp.

Giả sử ta có mảng array gồm các số nguyên được sắp xếp theo thứ tự tăng dần. Để tìm giá trị n cho trước, ta có thể làm như sau:

public int findNumberInSortedArray(int n, int[] array) {
  int midIndex = array.lenght / 2;
  int mid = array[midIndex];
  
  if (n == mid) return midIndex;

  // Không tìm thấy n
  if (mid == 0) return -1;

  // Tìm kiếm với nửa mảng nhỏ hơn
  if (n < mid) {
    int[] subArray = Arrays.copy(array, 0, midIndex-1);
    return findNumberInSortedArray(n, subArray);
  }
  
  // Tìm kiếm với nửa mảng lớn hơn
  if (n < mid) {
    int[] subArray = Arrays.copy(array, midIndex+1, array.length);
    return findNumberInSortedArray(n, subArray);
  }
}

Ở thuật toán trên, chúng ta đã áp dụng thuật toán chia để trị để chia việc tìm kiếm trên một mảng lớn thành tìm kiếm trên một mảng nhỏ hơn, có kích thước bằng 1/2 mảng ban đầu. Việc phân chia sẽ kết thúc khi độ lớn của mảng là 1 (giá trị ở giữa có index là 0). Như vậy, độ phức tạp của thuật toán trong trường hợp này sẽ là O(log n).

Để có tìm hiểu thêm thông tin về chủ đề này,  bạn có thể tham khảo thêm tại:

Quy hoạch động

Quy hoạch động (dynamic programing) là phương pháp thường được áp dụng để giải các bài toán toàn cục bằng cách chia chúng thành các bài toán con đơn giản hơn.

Đối với dạng bài toán, độ phức tạp thường sẽ là O(n). Tuy nhiên, chúng ta có thể sử dụng một số biện pháp để giảm độ phức tạp, cũng như giảm thời gian chạy.

Đầu tiên là phải kể đến việc tối ưu hoá cấu trúc. Biện pháp này có thể được áp dụng rộng rãi trong rất nhiều lĩnh vực. Đương nhiên, đây cũng không phải là một việc dễ dàng mà nó đòi hỏi một mức độ hiểu biết cũng như kinh nghiệm nhất định.

Cách thứ hai đó là lưu trữ lại đáp án của các bài toán con (memoization), nhằm hạn chế việc tính toán. Chúng ta sẽ có hai hướng tiếp cận chính, đó là từ trên xuống và từ dưới lên. Từ trên xuống (top-down) là hướng tiếp cận mà chúng ta sẽ bắt đầu từ bài toán phức tạp. Từ dưới lên (bottom-up) là hướng tiếp cận mà chúng ta sẽ giải các bài toán con cần thiết trước khi tiếp xúc với bài toán phức tạp.

Như đã đề cập ở trên, bằng phương pháp và các cách tiếp cận đã nêu ra, ta có thể giải các bài toán toàn cục. “Dãy số Fibonacci” và “Tìm đường đi ngắn nhất từ một đỉnh” là hai bài toán tiêu biểu.

Dãy số Fibonacci là dãy số được tính dựa theo các quy tắc sau:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) với n>=2

Dựa vào yêu cầu trên, cùng với quy hoạch động, ta sẽ có một phương thức đơn giản để tính kết quả như sau:

public int calFibonacci(int n) {
  if (n < 2) {
    return n;
  }

  return calFibonacci(n-1) + calFibonacci(n-2);
}

Để tìm thêm thông tin về chủ đề này, chúng ta có thể xem thêm một số link sau: