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

Share on:

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++.

1import cv2
2
3class SimpleImageProccessor:
4    def __init__(self):
5        pass
6    def process_img(self, img):
7        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:

1sudo apt install libboost-all-dev

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

1pip 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.

1#include <boost/python.hpp>
2#include <boost/python/numpy.hpp>
3#include <ctime>
4#include <iostream>
5#include <opencv2/opencv.hpp>
6
7using namespace std;
8namespace py = boost::python;
9namespace 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):

 1// Function to convert from cv::Mat to numpy array
 2np::ndarray ConvertMatToNDArray(const cv::Mat& mat) {
 3    py::tuple shape = py::make_tuple(mat.rows, mat.cols, mat.channels());
 4    py::tuple stride =
 5        py::make_tuple(mat.channels() * mat.cols * sizeof(uchar),
 6                       mat.channels() * sizeof(uchar), sizeof(uchar));
 7    np::dtype dt = np::dtype::get_builtin<uchar>();
 8    np::ndarray ndImg =
 9        np::from_data(mat.data, dt, shape, stride, py::object());
10
11    return ndImg;
12}

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

 1// Function to convert from numpy array to cv::Mat
 2cv::Mat ConvertNDArrayToMat(const np::ndarray& ndarr) {
 3    int length =
 4        ndarr.get_nd();  // get_nd() returns num of dimensions. this is used as
 5                         // a length, but we don't need to use in this case.
 6                         // because we know that image has 3 dimensions.
 7    const Py_intptr_t* shape =
 8        ndarr.get_shape();  // get_shape() returns Py_intptr_t* which we can get
 9                            // the size of n-th dimension of the ndarray.
10    char* dtype_str = py::extract<char*>(py::str(ndarr.get_dtype()));
11
12    // Variables for creating Mat object
13    int rows = shape[0];
14    int cols = shape[1];
15    int channel = length == 3 ? shape[2] : 1;
16    int depth;
17
18    // Find corresponding datatype in C++
19    if (!strcmp(dtype_str, "uint8")) {
20        depth = CV_8U;
21    } else if (!strcmp(dtype_str, "int8")) {
22        depth = CV_8S;
23    } else if (!strcmp(dtype_str, "uint16")) {
24        depth = CV_16U;
25    } else if (!strcmp(dtype_str, "int16")) {
26        depth = CV_16S;
27    } else if (!strcmp(dtype_str, "int32")) {
28        depth = CV_32S;
29    } else if (!strcmp(dtype_str, "float32")) {
30        depth = CV_32F;
31    } else if (!strcmp(dtype_str, "float64")) {
32        depth = CV_64F;
33    } else {
34        std::cout << "Wrong dtype error" << std::endl;
35        return cv::Mat();
36    }
37
38    int type = CV_MAKETYPE(
39        depth, channel);  // Create specific datatype using channel information
40
41    cv::Mat mat = cv::Mat(rows, cols, type);
42    memcpy(mat.data, ndarr.get_data(), sizeof(uchar) * rows * cols * channel);
43
44    return mat;
45}

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.

 1void Init() {
 2    // Set your python location.
 3    // wchar_t str[] = L"/home/vietanhdev/miniconda3/envs/example_env";
 4    // Py_SetPythonHome(str);
 5
 6    setenv("PYTHONPATH", "..", 1);
 7
 8    Py_Initialize();
 9    np::initialize();
10}

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.

 1try {
 2    // Initialize boost python and numpy
 3    Init();
 4
 5    // Import module
 6    py::object main_module = py::import("__main__");
 7
 8    // Load the dictionary for the namespace
 9    py::object mn = main_module.attr("__dict__");
10
11    // Import the module into the namespace
12    py::exec("import image_processing", mn);
13
14    // Create the locally-held object
15    py::object image_processor =
16        py::eval("image_processing.SimpleImageProccessor()", mn);
17    py::object process_img = image_processor.attr("process_img");
18
19    // Get image. Image from:
20    // https://github.com/opencv/opencv/blob/master/samples/data/baboon.jpg
21    cv::Mat img = cv::imread("baboon.jpg", cv::IMREAD_COLOR);
22    if (img.empty()) {
23        std::cout << "can't getting image" << std::endl;
24        return -1;
25    }
26
27    cv::Mat clone_img = img.clone();  
28
29    float total_time = 0;
30    for (size_t i = 0; i < (TEST_EXECUTION_TIME ? 1000 : 1); i++) {
31        const clock_t begin_time = clock();
32
33        np::ndarray nd_img = ConvertMatToNDArray(clone_img);
34        np::ndarray output_img = py::extract<np::ndarray>(process_img(nd_img));
35        cv::Mat mat_img = ConvertNDArrayToMat(output_img);
36
37        float instance_time = float(clock() - begin_time) / CLOCKS_PER_SEC;
38        total_time += instance_time;
39        cout << "Instance time: " << instance_time << endl;
40
41        // Show image
42        if (!TEST_EXECUTION_TIME) {
43            cv::namedWindow("Original image", cv::WINDOW_NORMAL);
44            cv::namedWindow("Output image", cv::WINDOW_NORMAL);
45            cv::imshow("Original image", img);
46            cv::imshow("Output image", mat_img);
47            cv::waitKey(0);
48            cv::destroyAllWindows();
49        }
50    }
51
52    cout << "Avg. time: " << total_time / 1000 << endl;
53
54} catch (py::error_already_set&) {
55    PyErr_Print();
56}

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ử:

1git clone https://github.com/vietanhdev/cpp-call-python-opencv cpp-call-python-opencv
2cd cpp-call-python-opencv
3cmake .
4make
5./run

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

Cpp call python

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.

Related Posts