Phát hiện góc nghiêng của ảnh sử dụng biến đổi Hough

Phát hiện góc nghiêng của ảnh có thể được coi như một bước tiền xử lý dữ liệu cho các bài toán khác như nhận dạng chữ viết, tách ký tự … Trong bài này, tôi sẽ giới thiệu một thuật toán đơn giản để phát hiện góc nghiêng văn bản từ một bài của blogger người Pháp Félix Abecassis (có lược bỏ các đoạn không cần thiết). Sau đó tôi sẽ cài đặt thử chương trình và đưa ra kết luận.

Góc nghiêng (Skew angle)

Việc phát hiện góc nghiêng của 1 văn bản là bước tiền xử lý trong phân tích văn bản. Văn bản đó có thể bị nghiêng 1 chút trong quá trình số hóa. Đó là nguyên nhân vì sao chúng ta cần phải thực hiện tính toán góc nghiêng và xoay lại cho thẳng trước khi thực hiện các xử lý khác (như phân đoạn ký tự và sau đó là OCR).

Chúng ta sẽ làm việc với 5 ảnh khác nhau về độ nghiêng nhưng giống nhau về độ dài văn bản.

p24 p16 p3 m20 m8

Quy tắc đặt tên của chúng khá đơn giản, ký tự đầu tiên viết tắt cho dấu của góc (p là góc dương còn m là góc âm), và số sau đó là giá trị của góc. Ví dụ: m8.jpg sẽ được xoay bởi 1 góc là -8 độ.

Chúng ta coi như các nhiễu (noise) trong quá trình số hóa đã được loại bỏ bởi các tiền xử lý (preprocessing) trước đó. Đồng thời chúng ta cũng coi như văn bản là tách biệt rõ ràng: không chứa ảnh, nằm ngang góc 90 độ hoặc dọc,…

Cài đặt với OpenCV

Trước tiên, khai báo 1 hàm là compute_skew, đầu vào là đường dẫn file ảnh và đầu ra là giá trị của góc. Chúng ta tải ảnh và lưu trữ nó trong biến.

void compute_skew(const char* filename)
{
    // Load in grayscale.
    cv::Mat src = cv::imread(filename, 0);
    cv::Size size = src.size();

Trong xử lý ảnh, đối tượng (foreground) thường là trắng và nền (background) là màu đen. Do trong hình đầu vào là ngược lại, vì vậy chúng ta cần đảo ngược lại màu sắc của bức ảnh:

    cv::bitwise_not(src, src);

Và đây là kết quả:

Để tính toán độ nghiêng, chúng ta phải tìm các đường thẳng trong văn bản. Trong dòng văn bản, chúng ta thấy rằng các chữ cái sẽ bên cạnh nhau, các đường do vậy sẽ được tạo bởi các đường thẳng màu trắng trong ảnh. Đây là ví dụ:

lines

Tất nhiên các ký tự có độ cao nhất định, chúng ta tìm các đường thẳng thực được tạo ra từ văn bản bằng cách thay đổi các tham số hoặc tiền xử lý, chúng ta sẽ giảm số đường thẳng tìm được (mục đích giảm các line nhiễu).

Vậy làm thế nào để tìm các đường thẳng đó trong ảnh? Chúng ta sẽ sử dụng 1 thuật toán được gọi là biến đổi Hough (Hough transform). Tôi sẽ không đi sâu chi tiết vào thuật toán này, nhưng ý tưởng chính của biến đổi Hough là sử dụng một bộ tích lũy 2D (2D accumulator) để đếm số lần mà đường thẳng được cho tìm thấy trên ảnh, toàn bộ ảnh được scan và đường thẳng “tốt nhất” được xác định bằng 1 hệ thống voting.

Ở đây, chúng ta sử dụng một biến thể hiệu quả của Standard Hough Transform (SHT) được gọi là Probabilistic Hough Transform (PHT). Trong OpenCV, PHT được cài đặt dưới tên HoughLinesP.

Thêm vào các tham số chuẩn của biến đổi Hough, chúng ta có thêm 2 tham số:

  • minLineLenght – chiều cao tối thiểu của đường thẳng. Các đường thẳng ngắn hơn sẽ bị loại bỏ. Đây là tham số rất tuyệt vời để bỏ bớt các đường thẳng nhỏ, dư thừa.
  • maxLineGap – Giá trị cực đại cho phép các kẽ hở giữa các điểm trên cùng một đường sẽ liên kết với nhau. Điều này là rất hay cho các văn bản có nhiều cột, ví dụ chúng ta có thể lựa chọn không liên kết các đường từ các cột khác nhau.

Quay lại với C++, trong OpenCV, PHT lưu các điểm cuối của đường thẳng trong khi SHT lưu đường thẳng trong các tọa độ của các cực (liên quan đến gốc). Vì vậy, chúng ta cần một vector để lưu toàn bộ các điểm kết thúc:

    std::vector lines;

Bây giờ chúng ta sẽ sử dụng hàm biến đổi Hough như sau:

    cv::HoughLinesP(src, lines, 1, CV_PI/180, 100, size.width / 2.f, 20);

Chúng ta sử dụng kích thước bước của 1 cho ρ and π/180 for θ, ngưỡng (số lượng vote tối thiểu) là 100.

minLineLength là width/2 (sẽ là tham số tốt nếu văn bản được tách biệt rõ ràng)

maxLineGap là 20, đây là giá trị khá ngon cho khoảng cách của kẽ hỡ giữa các đường thẳng nhỏ.

Trong phần còn lại, chúng ta dễ dàng tính toán góc giữa các đường thẳng và đường thẳng nằm ngang bằng cách sử dụng hàm toán học là atan2, sau đó sẽ tính toán góc trung bình (mean angle) của tất cả các đường.

Để cho việc debug được dễ dàng, chúng ta sẽ vẽ tất cả các đường trong một ảnh mới với tên là disp_lines, sau đó sẽ cho nó hiện lên một cửa sổ mới.

    cv::Mat disp_lines(size, CV_8UC1, cv::Scalar(0, 0, 0));
    double angle = 0.;
    unsigned nb_lines = lines.size();
    for (unsigned i = 0; i < nb_lines; ++i)
    {
        cv::line(disp_lines, cv::Point(lines[i][0], lines[i][1]),
                 cv::Point(lines[i][2], lines[i][3]), cv::Scalar(255, 0 ,0));
        angle += atan2((double)lines[i][3] - lines[i][1],
                       (double)lines[i][2] - lines[i][0]);
    }
    angle /= nb_lines; // mean angle, in radians.
 
    std::cout << "File " << filename << ": " << angle * 180 / CV_PI << std::endl;
 
    cv::imshow(filename, disp_lines);
    cv::waitKey(0);
    cv::destroyWindow(filename);
}

Cuối cùng, chúng ta chỉ cần một hàm main để gọi hàm compute_skew:

const char* files[] = { "m8.jpg", "m20.jpg", "p3.jpg", "p16.jpg", "p24.jpg"};
 
int main()
{
    unsigned nb_files = sizeof(files) / sizeof(const char*);
    for (unsigned i = 0; i < nb_files; ++i)
        compute_skew(files[i]);
}

Và đây là các góc thu được của từng ảnh, kết qua thu được với độ chính xác tương đối cao:

* Nguồn bài viết: http://felix.abecassis.me/2011/09/opencv-detect-skew-angle/

Kết luận của cá nhân:

  • Ưu điểm: nhanh, dễ cài đặt, tốc độ thực thi tốt.
  • Nhược điểm: còn quá đơn giản và chỉ phù hợp với ảnh thuần văn bản.

Tải về source code (cài đặt trên OpenCV 2.4.9 và VS 2012) + 5 ảnh test: skew_detection_01

 

Leave a Reply

Your email address will not be published. Required fields are marked *