Gọi hàm Python trên C++ với OpenCV

Có nhiều khi chúng ta cần gọi một hàm xử lý ảnh hoặc chạy mô hình học máy trên code Python từ C++. Lý do rất đơn giản, có thể là vì bạn muốn dùng luôn code Python, hoặc bạn chưa có thời gian chuyển sang code C++. Bài viết này sẽ hướng dẫn các bạn cách khởi tạo object và gọi đến phương thức Python từ code C++ và truyền ảnh dạng cv::Mat vào để xử lý.

Giả sử mình có đoạn code Python dưới đây. Đoạn code này là một đoạn xử lý ảnh đơn giản, tuy nhiên trong trường hợp của bạn có thể là một class cho mô hình học máy. Việc chuyển sang code C++ có thể tốn kha khá thời gian và kéo đến những rắc rối không cần thiết. Chúng ta sẽ thực hiện lưu đoạn code này dưới tên image_processing.py và thực hiện khởi tạo và gọi luôn hàm xử lý process_img(self, img) từ code C++.

import cv2

class SimpleImageProccessor:
    def __init__(self):
        pass
    def process_img(self, img):
        return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

Chúng ta sẽ sử dụng thư viện Boost để tương tác với Python. Để cài đặt trên Ubuntu - Debian gõ lệnh sau:

sudo apt install libboost-all-dev

Cài đặt OpenCV trên Python:

pip install opencv-python

Để bắt đầu, include các thư viện cần thiết vào file main.cpp. Ở đây chúng ta sẽ cần đến thư viện Boost để tương tác với Python và chuyển đổi dữ liệu qua lại giữa cv::Mat và Numpy.

#include <boost/python.hpp>
#include <boost/python/numpy.hpp>
#include <ctime>
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
namespace py = boost::python;
namespace np = boost::python::numpy;

Tiếp đó viết hàm chuyển từ kiểu dữ liệu cv::Mat sang Numpy array (kiểu dữ liệu để lưu ảnh của OpenCV trên Python):

// Function to convert from cv::Mat to numpy array
np::ndarray ConvertMatToNDArray(const cv::Mat& mat) {
    py::tuple shape = py::make_tuple(mat.rows, mat.cols, mat.channels());
    py::tuple stride =
        py::make_tuple(mat.channels() * mat.cols * sizeof(uchar),
                       mat.channels() * sizeof(uchar), sizeof(uchar));
    np::dtype dt = np::dtype::get_builtin<uchar>();
    np::ndarray ndImg =
        np::from_data(mat.data, dt, shape, stride, py::object());

    return ndImg;
}

Hàm để chuyển ngược từ Numpy array sang cv::Mat để lấy ảnh đầu ra:

// Function to convert from numpy array to cv::Mat
cv::Mat ConvertNDArrayToMat(const np::ndarray& ndarr) {
    int length =
        ndarr.get_nd();  // get_nd() returns num of dimensions. this is used as
                         // a length, but we don't need to use in this case.
                         // because we know that image has 3 dimensions.
    const Py_intptr_t* shape =
        ndarr.get_shape();  // get_shape() returns Py_intptr_t* which we can get
                            // the size of n-th dimension of the ndarray.
    char* dtype_str = py::extract<char*>(py::str(ndarr.get_dtype()));

    // Variables for creating Mat object
    int rows = shape[0];
    int cols = shape[1];
    int channel = length == 3 ? shape[2] : 1;
    int depth;

    // Find corresponding datatype in C++
    if (!strcmp(dtype_str, "uint8")) {
        depth = CV_8U;
    } else if (!strcmp(dtype_str, "int8")) {
        depth = CV_8S;
    } else if (!strcmp(dtype_str, "uint16")) {
        depth = CV_16U;
    } else if (!strcmp(dtype_str, "int16")) {
        depth = CV_16S;
    } else if (!strcmp(dtype_str, "int32")) {
        depth = CV_32S;
    } else if (!strcmp(dtype_str, "float32")) {
        depth = CV_32F;
    } else if (!strcmp(dtype_str, "float64")) {
        depth = CV_64F;
    } else {
        std::cout << "Wrong dtype error" << std::endl;
        return cv::Mat();
    }

    int type = CV_MAKETYPE(
        depth, channel);  // Create specific datatype using channel information

    cv::Mat mat = cv::Mat(rows, cols, type);
    memcpy(mat.data, ndarr.get_data(), sizeof(uchar) * rows * cols * channel);

    return mat;
}

Hàm khởi tạo môi trường Python. Ở đây bạn có thể chỉ đến môi trường bạn đang sử dụng bằng cách bỏ comment phần Set your python location. và sửa đường dẫn đến môi trường Python cần dùng.

void Init() {
    // Set your python location.
    // wchar_t str[] = L"/home/vietanhdev/miniconda3/envs/example_env";
    // Py_SetPythonHome(str);

    setenv("PYTHONPATH", "..", 1);

    Py_Initialize();
    np::initialize();
}

Toàn bộ đoạn code xử lý chính trong hàm main() như sau. Đoạn code này sẽ khởi tạo một instance của class SimpleImageProccessor và gọi phương thức process_img() để xử lý và lấy ảnh đầu ra.

try {
    // Initialize boost python and numpy
    Init();

    // Import module
    py::object main_module = py::import("__main__");

    // Load the dictionary for the namespace
    py::object mn = main_module.attr("__dict__");

    // Import the module into the namespace
    py::exec("import image_processing", mn);

    // Create the locally-held object
    py::object image_processor =
        py::eval("image_processing.SimpleImageProccessor()", mn);
    py::object process_img = image_processor.attr("process_img");

    // Get image. Image from:
    // https://github.com/opencv/opencv/blob/master/samples/data/baboon.jpg
    cv::Mat img = cv::imread("baboon.jpg", cv::IMREAD_COLOR);
    if (img.empty()) {
        std::cout << "can't getting image" << std::endl;
        return -1;
    }

    cv::Mat clone_img = img.clone();  

    float total_time = 0;
    for (size_t i = 0; i < (TEST_EXECUTION_TIME ? 1000 : 1); i++) {
        const clock_t begin_time = clock();

        np::ndarray nd_img = ConvertMatToNDArray(clone_img);
        np::ndarray output_img = py::extract<np::ndarray>(process_img(nd_img));
        cv::Mat mat_img = ConvertNDArrayToMat(output_img);

        float instance_time = float(clock() - begin_time) / CLOCKS_PER_SEC;
        total_time += instance_time;
        cout << "Instance time: " << instance_time << endl;

        // Show image
        if (!TEST_EXECUTION_TIME) {
            cv::namedWindow("Original image", cv::WINDOW_NORMAL);
            cv::namedWindow("Output image", cv::WINDOW_NORMAL);
            cv::imshow("Original image", img);
            cv::imshow("Output image", mat_img);
            cv::waitKey(0);
            cv::destroyAllWindows();
        }
    }

    cout << "Avg. time: " << total_time / 1000 << endl;

} catch (py::error_already_set&) {
    PyErr_Print();
}

Cuối cùng bạn cần viết CMake file cho project này. Tham khảo tại đây.

Để lấy toàn bộ source code của bài viết, các bạn clone Github repo sau: https://github.com/vietanhdev/cpp-call-python-opencv. Biên dịch và chạy thử:

git clone https://github.com/vietanhdev/cpp-call-python-opencv cpp-call-python-opencv
cd cpp-call-python-opencv
cmake .
make
./run

Kết quả thu được sẽ như hình dưới:

Cpp call python

Lưu ý: Trên đây là hướng dẫn biên dịch và sử dụng với Python 2. Để dùng Python 3 bạn cần tự biên dịch và cài đặt Boost với Python 3 và sửa lại CMakeLists.txt cho phù hợp.

Đăng ký để nhận các bài viết mới nhất.