Scan Credit Card trên android cho React-Native

Chào các bạn, scan credit card hiện nay là một chức năng đã rất quen thuộc với chúng ta. Trong các ứng dụng của nhiều ngân hàng, các tổ chức về tài chính đã đưa chức năng này thành một trong những tính năng nổi bật và tạo điểm nhấn cho ứng dụng của họ, nhờ đó tạo sự tiện lợi cho khách hàng. Vậy nếu như bạn đang sử dụng React-Native để build dự án thì chúng ta sẽ xử lý chức năng này như thế nào?. Sau đây mình sẽ giới thiệu cho các bạn một SDK cũng như cách custom nó để xử lý chức năng khó nhằn này. Nào...gét...gô....

Lưu ý: Bài viết này mình chỉ tập trung vào xử lý cho android, iOS thì mình đã dùng framework có sẵn (VisionFramework) và kết quả trả về rất tốt nên thư viện này sẽ được áp dụng cho mỗi android thôi nhé.


I. Một vài thông tin về credit card:


Chắc hẳn các bạn đã quen với tên các loại thẻ như Visa, MasterCard, American Express, JCB bên cạnh đó là các loại thẻ ít được mọi người biết đến như Discovery, Diner Club,...chúng đều được biết đến là các thẻ thanh toán quốc tế được sử dụng nhiều nhất hiện nay. Mỗi loại đều có các đặc điểm khác biệt và các chương trình ưu đãi khác nhau cho mỗi tập khách hàng khác nhau.
Dưới đây là biểu đồ về tỉ lệ phân bố của các loại thẻ thanh toán:

Như các bạn đã thấy, tỉ lệ của mỗi loại chiếm rất khác nhau và để phân biệt các loại thẻ thì nó sẽ có những đặc trưng riêng biệt, một trong số đó là phần number card cũng như phẩn bổ của các dãy số trong đó. Nó là một phần quan trọng để PayCard SDK có thể detect được các số có trên thẻ, mà chúng ta có thể gọi là các block.
Dưới đây là một số đặc điểm của các lại thẻ:

Vậy trước khi custom lại SDK này, thì các bạn cần biết nó là gì và đã làm được những gì, nào cùng tìm hiểu với mình nhé.

II. Tổng quan về PayCard SDK


Trang chủ: https://pay.cards/pages/sdk.html
GitHub: https://github.com/faceterteam/PayCards_Android
App Demo: https://play.google.com/store/apps/details?id=cards.pay.demo

PayCard SDK có những đặc điểm sau:

  • Là mã nguồn mở và hoàn toàn miễn phí.
  • Tốc độ đọc thẻ cao đối với các thẻ đặt vừa khung hình quét của thử viện.
  • Không cần chuẩn hoá thêm về code Native.
  • Dễ dàng chỉnh sửa code android native cũng như code phần detect card.
  • Đã setting sẵn config để đọc được thẻ Visa và Master Card. Nếu muốn đọc được nhiều loại thẻ cần phải custom lại.

Nó là một SDK miễn phí và rất phù hợp cho việc custom để có thể làm cho nó đọc được nhiều loại thẻ hơn.

III. Cách custom PayCard SDK để đọc được nhiều loại thẻ:

1. Cấu trúc thư mục và các file cần chú ý


Các bạn đã thấy, phần thư mục có thư mục cần quan tâm là SDK. Nó chứa toàn bộ phần code của PayCard. Nó được xây dựng như một dự án android bình thường. Phần code bên trong sẽ có các thư mục quan trọng là cpp và phần code java của SDK. Phần code java này sẽ có các phần hỗ trợ để dựng camera và nhận kết quả detect từ SDK trả về. Các bạn có thể đọc tham khảo, vì nó là code java nên cũng không khó đọc và có thể tự custom lại theo ý của các bạn. Phần chúng ta cần quan tâm nhất đó là phần code cpp.
Chúng ta sẽ đi sâu vào trong theo đường dẫn sdk/src/main/cpp/crossplatform/CrossPlatform/Recognizer/ ở đây ta có các file cần chú ý đó là:

  • NumberRecognizer.cpp: File định nghĩa các hàm cho việc detect number card.
  • DateRecognizer.cpp: File định nghĩa các hàm cho việc detect ngày hiệu lực.
  • NameRecognizer.cpp: File định nghĩa các hàm cho việc detect tên chủ thẻ.
  • RecognitionCore.cpp: File gọi hàm Process để detect data của thẻ.

Mình sẽ chỉ tập trung vào việc detect card number còn các phần còn lại sẽ làm tương tự nhé.

2. Cách custom PaycardSDK:

Nào bây giờ chúng ta sẽ đi từng bước để custom PaycardSDK.

Bước 1: Ước lượng tỉ lệ khung cho phần card number.

Ở đây trong code mẫu chúng ta đã thấy được tỉ lệ của Visa card là như sau:

static const cv::Rect numberWindowRect(54, 221, 552, 60);

với ý nghĩa các dãy số sẽ là X, Y, width, height của khung chứa card number.
Sẽ có nhiều cách để các bạn tìm được bộ số thích hợp nhất, ngoài việc thử từng bộ số thì các bạn có thể tận dụng đoạn code java mà SDK đã cho để sửa lại thành hàm cắt ảnh thành khung vừa với dãy card number. Từ đó đoán được nhanh và chính xác hơn. Nên chỗ này cũng cần chút kiến thức và nghiên cứu về camera của android dùng code java.
Mẹo nhỏ: Các bạn có thể dùng vòng lặp để tạo ra nhiều ảnh cắt, hoặc set các giá trị biên với nhiều giá trị khác nhau, như vậy sẽ tìm được bộ số nhanh hơn và chính xác hơn.

Bước 2: Xác định toạ độ của các block trên dãy số card number.


Ở bước này chúng ta sẽ dựa vào đặc điểm của loại thẻ để chia các block. Ở đây ta sẽ có ví dụ cho thẻ visa:

static const cv::Rect areaX0(0, 0, 136, 37);
static const cv::Rect areaX1(138, 0, 136, 37);
static const cv::Rect areaX2(277, 0, 136, 37);
static const cv::Rect areaX3(416, 0, 136, 37);

static const vector<cv::Rect> areasX = {areaX0, areaX1, areaX2, areaX3};

static const cv::Size digitSize(25, 37); // 960/660 = 1.45454545
static int panDigitPaddingX = 2;
static int panDigitPaddingY = 2;
static const int spaceBWDidits = 3;

Ở mỗi loại thẻ chúng ta sẽ có các bộ số khác nhau cũng như các toạ độ khác nhau. Các bạn có thẻ điều chỉnh trong lúc code để có bộ số ưng ý nhất và chính xác nhất. Các toạ độ block này sẽ được đưa về một biến area, như trên ta sẽ có biến areaX cho thẻ Visa. Đối với các thẻ khác nhau sẽ có bộ toạ độ khác nhau tuỳ theo đặc điểm của chúng.
Mẹo: Ở đây cũng sẽ áp dụng cách làm như với card number. Các bạn nhớ cắt tỉ lệ chính xác thì tốc độ đọc sẽ nhanh hơn. Với các bộ số đều nhau như 4-4-4-4 thì nên chia đều các toạ độ

Tiếp theo sẽ cần chú ý các biến digitSize (size của một số), panDigitPaddingX/Y (phần padding trên dưới, trái phải), spaceBWDidits (khoảng cách giữa 2 số), với mỗi loại thẻ sẻ có các tỉ lệ khác nhau, các bạn có thể tự tạo riêng cho mỗi loại thẻ những bộ số khác nhau, hoặc chỉnh sửa cho bộ số có thể detect được nhiều loại thẻ mà bạn mong muốn.

Bước 3: Tạo các hàm detect block riêng

Phần tiếp theo cũng không kém phần quan trọng đó chính là xây dựng các hàm detect block riêng cho mỗi loại thẻ đặc biệt. Như các bạn đã thấy ở bảng đặc điểm các loại thẻ, chúng ta có 3 loại block chính đó là 4,5,6. Ở bản gốc của SDK đã dựng sẵn cho chúng ta block4, vậy chỉ cần tạo thêm block5 và block6 nữa là xong.
Code mẫu sẽ như sau:

vector<Mat> CNumberRecognizer::SplitBlock6(const Mat &mat, int xPos, int yPos, int offset,
                                           int digitSpace, int xPadding, int yPadding,
                                           vector<cv::Rect> &digitsRects) {
    vector<Mat> digits;

    int x = offset + xPos;

    for (int i = 0; i < 6; i++) {

        cv::Rect rect = cv::Rect(x - xPadding, yPos - yPadding, digitSize.width + xPadding * 2,
                                 digitSize.height + yPadding * 2);

        if (CUtils::ValidateROI(mat, rect)) {
            digitsRects.push_back(rect);
            Mat digit = mat(rect);
            digits.push_back(digit);
            x += digitSize.width + digitSpace;
        }
    }

    return digits;
}

Các bạn làm tương tự với block5 nhé.
Vậy công dụng của hàm này sẽ là gì?
Nó sẽ detect các chữ số thông qua việc cắt các block toạ độ thành các ảnh hình chữ nhật bằng hàm cv::Rect và sau đó dùng sử dụng hàm CUtils::ValidateROI(mat, rect) để validate xem khối chữ nhật đó có chứa chữ số nào không. Mình sẽ không đi sâu vào các hàm này, các bạn có thể đọc thêm để biết được nó hoạt động như nào nhé.

Bước 4: Config hàm PreLocalize

Ở hàm này chúng ta cần thực hiện các điều sau:

  • Tạo biến rect cho từng loại thẻ
Rect rect;
 if (scanType == "visa") {
 rect = cv::Rect(numberWindowVisaRect.x, numberWindowVisaRect.y + points[0].y, numberWindowVisaRect.width, digitSize.height);
 } else if (scanType == "diner") {
 rect = cv::Rect(numberWindowDinersRect.x, numberWindowDinersRect.y + points[0].y, numberWindowDinersRect.width, digitSize.height);
 }
  • Tạo vòng lặp kiểm tra khung hình có tồn tại card number cho từng loại thẻ
if (scanType == "visa") {
            for (cv::Rect rect : areasX) {
                Rect _rect = cv::Rect(rect.x, 0, rect.width, fullNumberMat.rows);

                if (!CUtils::ValidateROI(fullNumberMat, _rect)) return false;
                Mat block = fullNumberMat(_rect);

                blocks.push_back(block);
            }
        } else if (scanType == "diner") {
            for (cv::Rect rect : areasY) {
                Rect _rect = cv::Rect(rect.x, 0, rect.width, fullNumberMat.rows);

                if (!CUtils::ValidateROI(fullNumberMat, _rect)) return false;
                Mat block = fullNumberMat(_rect);

                blocks.push_back(block);
            }
        }

Vẫn như mình đã nói, các bạn cần detect cho bao nhiêu loại thẻ thì cần define bấy nhiêu lần.

Bước 5: Config cho hàm ProcessMatrixFinal

Đây là lúc chúng ta sẽ sử dụng các hàm block đã được tạo ở bước 3. Ở hàm này cần xác định được loại thẻ chúng ta cần detect có các loại block nào.
Cùng theo dõi code phía dưới nhé:

 if (scanType == "visa") {
            for (auto it = begin(areasX); it < end(areasX); ++it, ++count) {
                vector<Mat> blockDigits = SplitBlock(numberWindow, points[count].x, points[0].y,
                                                     areasX[count].x, spaceBWDidits, paddingPoint.x,
                                                     paddingPoint.y, digitRects);
                if (blockDigits.size() != 4) return nullptr; //note

                digits.insert(digits.end(), blockDigits.begin(), blockDigits.end());
            }
        } else {
            vector<Mat> blockDigits0 = SplitBlock(numberWindow, points[0].x, points[0].y,
                                                  areasY[0].x, spaceBWDidits, paddingPoint.x,
                                                  paddingPoint.y, digitRects);
            digits.insert(digits.end(), blockDigits0.begin(), blockDigits0.end());

            vector<Mat> blockDigits1 = SplitBlock6(numberWindow, points[1].x, points[0].y,
                                                   areasY[1].x, spaceBWDidits, paddingPoint.x,
                                                   paddingPoint.y, digitRects);
            digits.insert(digits.end(), blockDigits1.begin(), blockDigits1.end());

            vector<Mat> blockDigits2 = SplitBlock(numberWindow, points[2].x, points[0].y,
                                                  areasY[2].x, spaceBWDidits, paddingPoint.x,
                                                  paddingPoint.y, digitRects);
            digits.insert(digits.end(), blockDigits2.begin(), blockDigits2.end());
        }

Mình đã ví dụ cho các bạn về trường hợp các thẻ có bộ block là 4-4-4-4 ở
scanType = visa còn bộ còn lại là thuộc về thẻ Diner Club ứng với 4-6-4. Các bạn xem đặc điểm của các loại thẻ ở bảng phía trên và làm thêm các trường  hợp khác nhé.

Sau khi qua phần xử lý này, chúng ta đã có một dãy số từ phần core AI trả về. Các số trong dãy này sẽ có các mức độ chính xác khác nhau với giá trị từ 0 tới 1.
Việc chúng ta tiếp theo sẽ xử lý các dãy số này để có được dãy số chính xác nhất ở hàm ValidateNumber .

Cần chú ý tới 2 thông số quan trọng trong hàm ValidateNumber đó chính là:

float threshold = 0.75;
const int maxDoubtfulCount = 1;
  • threshold: Độ chính xác nhỏ nhất của các chữ số.
  • maxDoubtfulCount: Số lượng cho phép các chữ số có độ chính xác nhỏ hơn  threshold.

Các bạn hãy chỉnh sửa để có các thông số phù hợp cho việc detect của mình nhé.
Cuối cùng, sau khi có được bộ số với độ chính xác phù hợp nếu bạn cần kiểm tra xem bộ số đó có thuộc card number hay không thì hãy xây dựng cho mình một hàm check validate riêng nhé. Có thể tham khảo thêm về thuật toán Luhn.

Bước 6: Xứ lý hàm Process.

Hàm này sẽ là hàm xử lý chính của chức năng detect card number.
Các bạn theo dõi code nhé:

shared_ptr<INeuralNetworkResultList>
CNumberRecognizer::Process(cv::Mat &matrix, cv::Rect &boundingRect, int scanType) {
    Mat numberWindow;
    cv::Rect extendedRect;
    const int padding = 10;

  if(scanType == "diner") {
        numberWindow = matrix(numberWindowDinersRect);
        extendedRect = cv::Rect(numberWindowDinersRect.x - padding, numberWindowDinersRect.y - padding,
                                numberWindowDinersRect.width + padding * 2,
                                numberWindowDinersRect.height + padding * 2);
    } else {
        numberWindow = matrix(numberWindowRect);
        extendedRect = cv::Rect(numberWindowRect.x - padding, numberWindowRect.y - padding,
                                numberWindowRect.width + padding * 2,
                                numberWindowRect.height + padding * 2);
    }

    Mat extendedNumberWindow = matrix(extendedRect);

    vector<cv::Point> points = {cv::Point(0, 0), cv::Point(0, 0), cv::Point(0, 0), cv::Point(0, 0)};

    if (PreLocalize(numberWindow, matrix, points)) {

        for (cv::Point &point : points) {
            point += cv::Point(padding, padding);
        }
        shared_ptr<INeuralNetworkResultList> result = ProcessMatrixFinal(extendedNumberWindow,
                                                                         points,
                                                                         _recognitionNeuralNetwork,
                                                                         cv::Point(panDigitPaddingX,
                                                                                   panDigitPaddingY),
                                                                         boundingRect);

        boundingRect.x += extendedRect.x;
        boundingRect.y += extendedRect.y;

        return result;
    }

    return nullptr;
}

Hàm Process sẽ được thực hiện như sau:

  • Tạo các biến  numberWindow extendedRect cho từng loại thẻ.
  • Gọi hàm PreLocalize để check tồn tại của dãy số card number.
  • Nếu như PreLocalize trả về true sẽ gọi hàm ProcessMatrixFinal để lấy dãy số đã detect, ngược lại sẽ trả về null.

Bước 7: Gọi hàm Process ở file RecognitionCore.cpp

Và tới đây sẽ là bước cuối cùng để chúng ta hoàn tất việc custom lại PayCardSDK. Ở file RecognitionCore.cpp, chúng ta có xử lý detect card number ở hàm CRecognitionCore::RecognizeNumber. Mình sẽ demo một loại thẻ nhé.

Dưới đây là code của đoạn code detect cho thẻ Visa:

number = {};
                            card = "visa";
                            result = numberRecognizer->Process(frame, boundingRect, 2);
                            if (result) {
                                for (INeuralNetworkResultList::ResultIterator it = result->Begin();
                                     it != result->End(); ++it) {
                                    shared_ptr<INeuralNetworkResult> result = *it;
                                    number.push_back(result->GetMaxIndex());
                                }
                                stringstream showNumber;
                                copy(number.begin(), number.end(),
                                     std::ostream_iterator<int>(showNumber, ""));
                                string n = showNumber.str();
                                string cardType = n.substr(0, 2);
                                int checkRegex_Master = regex_match(n, regex("^5[1-5][0-9]{0,}$"));
                                int checkRegex_Visa = regex_match(n, regex("^4[0-9]{0,}?$"));
                                int checkRegex_JCB = regex_match(n,
                                                                 regex("^(?:2131|1800|35\\d{3})\\d{0,}$"));
                                if ((checkRegex_Master || checkRegex_Visa || checkRegex_JCB) && result != nullptr) {
                                    recognitionResult->SetNumberResult(result);
                                    recognitionResult->SetNumberRect(boundingRect);
                                    recognitionResult->SetCardImage(frame.clone());
                                }
                            }
                        }

Chú ý: Cần có frame ảnh để detect nên các đoạn xử lý này cần để dưới hàm lấy current frame nhé.

frameStorage->GetCurrentFrame(frame);

Các bạn hãy kiểm tra result của từng loại thẻ và tạo thêm cho mình những đoạn code detect khác nữa nhé. Có thể lồng vào nhau bằng lệnh if hoặc chạy tuần tự cũng được.

Vậy là xong, chúng ta đã có các bước custom lại PayCardSDK để có thể đọc được nhiều loại thẻ khác nhau. Các bạn hãy làm thật kỹ những bước mình đã hướng dẫn để có được kết quả tốt nhất nhé.

IV. Demo:

V. Tổng kết:

Đây là một trong những cách để custom PayCardSDK đơn giản, các bạn có thể thử với các cách phức tạp hơn như tách riêng từng hàm process, hay tách thành từng file detect riêng cho các loại thẻ xem sao nhé. Mình đã thử và kết quả rất bất ngờ. Nếu các bạn thấy hứng thú với cách này mình sẽ quay lại với một bài blog với chủ đề này. Hẹn gặp các bạn ở bài viết tiếp theo.

Tài liệu tham khảo:

  1. https://github.com/faceterteam/PayCards_Android
  2. https://www.geeksforgeeks.org/luhn-algorithm/
  3. https://stackoverflow.com/questions/72768/how-do-you-detect-credit-card-type-based-on-number