Google Code Prettify

2017年9月2日 星期六

[C++] Pass an OpenCV Mat Image to a Python Function

  最近因工作需要而研究了一陣子 C++ 與 Python 的互動,主要達成以下幾件事:
  1. 從 C++ 端呼叫 Python 的一個 Function。
  2. 將從 C++ 用 OpenCV 讀進來的 Mat 資料結構轉換成 Numpy Array。
  3. 把 Numpy Array 當成參數傳進 Python Function。




Reference


廢話不多說,直接上 Code。

  1. #ifdef _DEBUG
  2. #undef _DEBUG
  3. #include <Python.h>
  4. #define _DEBUG
  5. #else
  6. #include <Python.h>
  7. #endif
  8. #include <iostream>
  9. #include <numpy/arrayobject.h>
  10. #include <opencv2/imgproc.hpp>
  11. #include <opencv2/imgcodecs.hpp>
  12. #include <opencv2/highgui.hpp>

  • Python.h - 安裝Python時會附帶的 C/C++ Header file,位於 Python3X/include。另外要記得 link 對應的 python3x.lib,位於 Python3X/libs。而由於使用的 .lib 是 Release 版,想在 Debug 版使用則需要如上的寫法。
  • numpy/arrayobject.h - 由於我們要使用 Numpy 的 C/C++ API 來將 cv::Mat 轉換成 numpy array,因此需要 include 該 header,位於 Python3X/Lib/site-packages/numpy/core/include。

  1. int
  2. main(int argc, char *argv[])
  3. {
  4. PyObject *pName = nullptr, *pModule = nullptr, *pFunc = nullptr;
  5. PyObject *pArgs = nullptr, *pReturn = nullptr, *pString = nullptr;
  6. if (argc < 3) {
  7. fprintf(stderr, "Usage: call pythonfile funcname [args]\n");
  8. return EXIT_FAILURE;
  9. }
  10. wchar_t *wcsProgram = Py_DecodeLocale(argv[0], NULL);
  11. Py_SetProgramName(wcsProgram);
  12. Py_Initialize();
  13. pName = PyUnicode_DecodeFSDefault(argv[1]);
  14. if (PyErr_Occurred()) {
  15. std::cerr << "pName decoding failed." << std::endl;
  16. return EXIT_FAILURE;
  17. }
  18. pModule = PyImport_Import(pName);
  19. Py_DECREF(pName);
  20. if (pModule != NULL) {
  21. pFunc = PyObject_GetAttrString(pModule, argv[2]);
  22. /* pFunc is a new reference */

上例中,假設存在一 .py 檔名為 argv[1],內含一 function 名為 argv[2],於是我們將喚醒一個 Python Interpreter,令其幫我們 import 該 .py,並抓取該 function。

  1. if (pFunc && PyCallable_Check(pFunc)) {
  2. cv::Mat img = cv::imread(argv[4], cv::IMREAD_COLOR), img_rgb;
  3. cv::cvtColor(img, img_rgb, cv::COLOR_BGR2RGB);
  4. // Build the array in C++
  5. const int ND = 2;
  6. npy_intp dims[2]{ img_rgb.rows, img_rgb.cols * img_rgb.channels() };
  7. // Convert it to a NumPy array.
  8. import_array();
  9. PyObject *pArray = PyArray_SimpleNewFromData(
  10. ND, dims, NPY_UBYTE, reinterpret_cast<void*>(img_rgb.data));
  11. if (!pArray) {
  12. std::cerr << "PyArray_SimpleNewFromData failed." << std::endl;
  13. return EXIT_FAILURE;
  14. }
  15. PyObject *pValue = PyLong_FromLong(img_rgb.channels());
  16. pString = PyUnicode_FromString(argv[3]);
  17. pReturn = PyObject_CallFunctionObjArgs(pFunc, pString, pArray, pValue, NULL);
  18. if (pReturn != NULL) {
  19. fprintf(stdout, "Result of call: %ld\n", PyLong_AsLong(pReturn));
  20. Py_DECREF(pReturn);
  21. }
  22. else {
  23. Py_DECREF(pFunc);
  24. Py_DECREF(pModule);
  25. PyErr_Print();
  26. fprintf(stderr, "Call failed\n");
  27. return EXIT_FAILURE;
  28. }
  29. }

重點部分來了。要將 Image 傳入 Python function 使用,我們透過 Numpy 提供的 C API "PyArray_SimpleNewFromData()" 將 cv::Mat 轉換成 numpy array。
要注意的是,OpenCV中針對彩圖所使用的資料結構為 2 維的 BGR array,每個 pixel 預設使用 CV_8UC3 型別,其在 memory 中以

[B G R B G R . . . . . .]
[. . . . . . . . . . . .]
[. . . . . . B G R B G R]
的方式儲存,因此我們轉換的目標是 2 維的 numpy array,寬為 cols * channels,高為 rows,型別為 NPY_UBYTE。(前面有先 BGR2RGB)
另外,在使用 Numpy API 之前,絕對一定必然要先呼叫 import_array(),否則程式永遠都會執行失敗!

最後,就可以 PyObject_CallFunctionObjArgs() 去呼叫 Python function 並傳入需要的參數了。

  1. }
  2. else {
  3. if (PyErr_Occurred())
  4. PyErr_Print();
  5. fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
  6. }
  7. Py_XDECREF(pFunc);
  8. Py_DECREF(pModule);
  9. }
  10. else {
  11. PyErr_Print();
  12. fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
  13. return EXIT_FAILURE;
  14. }
  15. Py_Finalize();
  16. return EXIT_SUCCESS;
  17. }

毫無反應,就一些例外處理(並不完善)。但最後的 Py_Finalize() 是必要的,對應開頭的 Py_Initialize()。

2 則留言:

  1. PyObject *pArray = PyArray_SimpleNewFromData(
    ND, dims, NPY_UBYTE, reinterpret_cast(img_rgb.data));
    您好,運行這一行有報錯,可以幫忙解答嗎?

    回覆刪除
    回覆
    1. 您好,請問可有錯誤訊息且有先大概google過了嗎?

      不然我沒有水晶球可能沒辦法解答..

      刪除