Bài viết này dịch lại 1 bài viết hướng dẫn cách đọc mã thẻ cào điện thoại ở nước ngoài. Bài viết sử dụng các phương pháp xử lý cơ bản với text rõ ràng và độ tương phản cao.
Bài toán cần giải quyết là: từ ảnh thẻ cào làm sao lấy ra chữ số từ phần màu vàng? Và ký tự không rõ ràng như các ký tự trên máy tính, vậy làm sao để nhận dạng?
Ảnh gốc
Nhìn vào hình thấy chữ số muốn tác có background màu vàng và chữ màu đen. Sử dụng không gian màu CMYK là một ý tưởng hay, ngoài ra cũng có thể sử dụng HSV. Trong lý thuyết về hệ màu CMYK, phần màu vàng có giá trị cao trong kênh Y, và màu đen có giá trị cao trong kênh K.
Tuy nhiên OpenCV không hỗ trợ CMYK nên chúng tôi sẽ viết code để chuyển đổi, xem code bên dưới:
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 |
// Covert RGB to CMYK using the formula from // http://rapidtables.com/convert/color/rgb-to-cmyk.htm void rgb2cmyk(cv::Mat& src, std::vector<cv::Mat>& cmyk) { CV_Assert(src.type() == CV_8UC3); cmyk.clear(); for (int i = 0; i < 4; ++i) cmyk.push_back(cv::Mat(src.size(), CV_32F)); for (int i = 0; i < src.rows; ++i) { for (int j = 0; j < src.cols; ++j) { cv::Vec3b p = src.at<cv::Vec3b>(i,j); float r = p[2] / 255.; float g = p[1] / 255.; float b = p[0] / 255.; float k = (1 - std::max(std::max(r,g),b)); cmyk[0].at<float>(i,j) = (1 - r - k) / (1 - k); cmyk[1].at<float>(i,j) = (1 - g - k) / (1 - k); cmyk[2].at<float>(i,j) = (1 - b - k) / (1 - k); cmyk[3].at<float>(i,j) = k; } } } ///////////////////////////////////////////////////////////// cv::Mat im0 = cv::imread("scratchcard.jpg"); if (!im0.data) return -1; std::vector<cv::Mat> cmyk; rgb2cmyk(im0, cmyk); |
Đoạn code trên load ảnh gốc từ BGR rồi chuyển sang hệ màu CMYK. Kết quả là được 4 ma trận chứa trong std::vector. Mỗi kênh màu C, M, Y và K chứa các giá trị pixel như ảnh bên dưới:
4 khung ảnh a,b,c,d theo thứ tự từ trái qua phải, từ trên xuống dưới. (a) C channel. (b) M channel. (c) Y channel. (d) K channel.
Không may trong kênh màu Y, hầu hết đều có giá trị cao nên khó mà tách phần cần thiết. Nhưng hãy nhìn kênh M và K. Trong kênh M, phần cần thiết gần giống màu đen và kênh K, chữ số gần giống màu trắng. Nếu đảo kênh M và nhân với kênh K, chúng ta sẽ được kết quả tốt cho ảnh mask ban đầu.
1 2 |
cv::Mat im1; im1 = cmyk[3].mul(1 - cmyk[1]) > 0.25; |
Đoạn code bên dưới chỉ giữ lại contour lớn nhất
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
cv::Mat im2; im1.convertTo(im2, CV_8U); std::vector<std::vector<cv::Point> > contours; cv::findContours(im2, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); double max_area = 0; int max_idx = 0; for (int i = 0; i < contours.size(); i++) { double area = cv::contourArea(contours[i]); max_idx = area > max_area ? i : max_idx; max_area = area > max_area ? area : max_area; } im2.setTo(cv::Scalar(0)); cv::drawContours(im2, contours, max_idx, cv::Scalar(255), -1); |
Dùng ảnh trên để lấy ký tự từ ảnh gốc
1 2 3 |
cv::Mat im3; cv::cvtColor(im0, im3, CV_BGR2GRAY); im3 = ((255 - im3) & im2) > 200; |
Tiếp theo là lọc bỏ nhiễu
1 2 3 4 5 6 7 |
cv::Mat dst = im3.clone(); cv::findContours(dst.clone(), contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); for (int i = 0; i < contours.size(); i++) { if (cv::contourArea(contours[i]) < 100) cv::drawContours(dst, contours, i, cv::Scalar(0), -1); } |
Bây giờ nó đã hoàn hảo, các ký tự đã được tách ra khỏi background. Tiếp theo là chuyển ảnh qua Tesseract OCR để nhận dạng ký tự sẽ được chữ số chúng ta cần: 52931 75003 3794
1 2 3 4 5 6 7 8 |
tesseract::TessBaseAPI tess; tess.Init(NULL, "eng", tesseract::OEM_DEFAULT); tess.SetVariable("tessedit_char_whitelist", "0123456789"); tess.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK); tess.SetImage((uchar*)dst.data, dst.cols, dst.rows, 1, dst.cols); char* out = tess.GetUTF8Text(); std::cout << out << std::endl; |
Dưới đây là code đầy đủ
[bg_collapse view=”button-orange” color=”#4a4949″ expand_text=”Show More” collapse_text=”Show Less” ]
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
/** * ocr.cpp: * Read the digits from a scratchcard. See the tutorial at * http://opencv-code.com/tutorials/how-to-read-the-digits-from-a-scratchcard * * Compile with: * g++ -I/usr/local/include -L/usr/local/lib ocr.cpp -o ocr \ * -lopencv_core -lopencv_imgproc -lopencv_highgui -ltesseract */ #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> #include <tesseract/baseapi.h> #include <iostream> // Covert RGB to CMYK using the formula from // http://rapidtables.com/convert/color/rgb-to-cmyk.htm void rgb2cmyk(cv::Mat& src, std::vector<cv::Mat>& cmyk) { CV_Assert(src.type() == CV_8UC3); cmyk.clear(); for (int i = 0; i < 4; ++i) cmyk.push_back(cv::Mat(src.size(), CV_32F)); for (int i = 0; i < src.rows; ++i) { for (int j = 0; j < src.cols; ++j) { cv::Vec3b p = src.at<cv::Vec3b>(i,j); float r = p[2] / 255.; float g = p[1] / 255.; float b = p[0] / 255.; float k = (1 - std::max(std::max(r,g),b)); cmyk[0].at<float>(i,j) = (1 - r - k) / (1 - k); cmyk[1].at<float>(i,j) = (1 - g - k) / (1 - k); cmyk[2].at<float>(i,j) = (1 - b - k) / (1 - k); cmyk[3].at<float>(i,j) = k; } } } int main() { cv::Mat im0 = cv::imread("scratchcard.png"); if (!im0.data) return -1; std::vector<cv::Mat> cmyk; rgb2cmyk(im0, cmyk); cv::Mat im1; im1 = cmyk[3].mul(1 - cmyk[1]) > 0.25; cv::Mat im2; im1.convertTo(im2, CV_8U); std::vector<std::vector<cv::Point> > contours; cv::findContours(im2, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); double max_area = 0; int max_idx = 0; for (int i = 0; i < contours.size(); i++) { double area = cv::contourArea(contours[i]); max_idx = area > max_area ? i : max_idx; max_area = area > max_area ? area : max_area; } im2.setTo(cv::Scalar(0)); cv::drawContours(im2, contours, max_idx, cv::Scalar(255), -1); cv::Mat im3; cv::cvtColor(im0, im3, CV_BGR2GRAY); im3 = ((255 - im3) & im2) > 200; cv::Mat dst = im3.clone(); cv::findContours(dst.clone(), contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); for (int i = 0; i < contours.size(); i++) { if (cv::contourArea(contours[i]) < 100) cv::drawContours(dst, contours, i, cv::Scalar(0), -1); } tesseract::TessBaseAPI tess; tess.Init(NULL, "eng", tesseract::OEM_DEFAULT); tess.SetVariable("tessedit_char_whitelist", "0123456789"); tess.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK); tess.SetImage((uchar*)dst.data, dst.cols, dst.rows, 1, dst.cols); char* out = tess.GetUTF8Text(); std::cout << out << std::endl; cv::imshow("src", im0); cv::imshow("dst", dst); cv::waitKey(); return 0; } |
[/bg_collapse]
Nguồn: http://opencv-code.com/tutorials/how-to-read-the-digits-from-a-scratchcard/
Admin có thể chỉ em là tải những phần mềm nào để làm bài này được không ạ, em mới học Open CV nên chưa biết ạ, em cảm ơn Admin!!!
Bạn làm theo bài này nhé https://thigiacmaytinh.com/setup-project-opencv-bang-c/, sau đó copy code vào là chạy. Nếu gặp lỗi trong quá trình chạy thì chụp ảnh mã lỗi rồi post vào group https://www.facebook.com/groups/thigiacmaytinh/
Vâng em cảm ơn để em thử ạ!!