windows環境下python調用cpp動態庫(win10+opencv)

第一部分、前言

在windows環境下,可以利用python直接調用cpp的動態鏈接庫,從而達到混合編程的目的。

一、cpp的動態鏈接庫

windows下編譯cpp的動態鏈接庫的技術比較多了,這裏指出兩處需要特別注意的地方:

(1)要利用extern "C"關鍵字,實現C編譯;

(2)pythoe與 cpp的接口最好重寫封裝,即在功能函數外面添加一層包裝,在包裝內實現數據交互。

二、python調用cpp的動態鏈接庫

主要注意三個地方:

(1)dll如果依賴於其他動態庫(如opencv),則必須保證其他被依賴的dll文件在環境變量的路徑下(或者與被調用的dll扔在同一路徑下),否則python調用的時候會找不到依賴項。

(2)使用ctypes模塊實現python內調用cpp動態庫時的數據交互,主要是用指針交互;對於x64平臺,輸出數據指針必須定義爲64位無符整型(ctypes.c_uint64),否則會報錯【注:此處指的是python內的接口數據定義,cpp內不必遵守,這看起來是一個矛盾的問題,具體原因未知,有些技術人員認爲這是一個bug,留待以後進一步確認】。

(3)由cpp傳回的數據指針,是由ASCII碼錶示的二進制,即不論cpp的返回數據類型是什麼,python接收到的連續內存都會被且分爲一個一個的8bit小單元,需要進一步數據拼接,才能恢復原始數據【注:比如cpp返回數據爲short類型的12941,其二進制標表示爲0011 0010 1000 1101,十六進制表示爲0x328D,但是python返回的不是一個short的16位數據,而是被拆分爲兩個8bit的數位,先返回低8位0x8D,後返回高8位0x50,因此需要進一步處理】。

第二部分、代碼示例

一、cpp功能函數封裝dll

功能函數代碼如下:

void detect(const cv::Mat img, std::vector<cv::Rect>& rect)
{
	cv::Mat dst;
	if (3 == img.channels()) { cv::cvtColor(img, dst, cv::COLOR_RGB2GRAY); }
	else { img.copyTo(dst); }
	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::Rect tmp;
	if (CV_8U != dst.type()) { dst.convertTo(dst, CV_8U); }
	cv::findContours(dst, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
	for (int index = 0; index >= 0; index = hierarchy[index][0])
	{
		tmp = cv::boundingRect(contours[index]);
		rect.push_back(tmp);
	}
}

上述cpp代碼實現檢測目標物體輪廓,並返回最小外接矩形。

封裝接口如下

short* py2cpp(int height, int width, uchar* src_data)
{
    //> 由python接口傳入的數據轉換爲適合功能函數獲取的數據
    cv::Mat src(height, width, CV_8UC3, (void*)src_data);
    //> 調用功能函數
    std::vector<cv::Rect> list;
    detect(src, list);
    //> 將結果轉換爲適合python接口返回的數據形式
    cv::Mat tres((int)list.size(), 4, CV_16SC1);
    g_value = (int)list.size();
    for (int i = 0; i < tres.rows; i++)
    {
        tres.at<short>(i, 0) = list[i].x;
        tres.at<short>(i, 1) = list[i].y;
        tres.at<short>(i, 2) = list[i].width;
        tres.at<short>(i, 3) = list[i].height;
        //> 輸出cpp內計算的計算結果
        std::cout << "[" << list[i].x << ", " << list[i].y << ", " << list[i].width << ", " << list[i].height << "]" << std::endl;
    }
    short *bb = (short *)(tres.data);
    //> 輸出內存首地址
    std::cout << bb << std::endl;
    return bb;
}

輸入值:圖像的高度height,圖像寬度width, 以及圖像指針src_data。

返回值:short類型指針,指向std::vector<cv::Rect>的首地址,其內存放的是 輪廓最小外接矩形。

該接口封裝函數,實現了將由python傳進來的數據,整合成cpp功能函數方便調用的數據類型,再將功能函數的返回值轉化爲方便python獲取的形式再傳出,從而完成數據通信。同時, 還應當返回一個數據長度的量,以方便在內存中進行讀取,在上述藉口封裝的代碼中,已經將這個數據長度存儲在全局變量g_value下,另寫一個函數返回這個全局變量即可。代碼如下:

int lenght()
{
	std::cout << g_value << std::endl;
	return g_value;
}

以上,是dll的代碼實現,寫在cpp文件中,其頭文件內容如下:

#pragma once

#include <iostream>
#include <opencv2/opencv.hpp>

#define DLLEXPORT __declspec(dllexport)

extern "C"
{
	int g_value;
	DLLEXPORT short* __stdcall py2cpp(int height, int width, uchar* src_data);

	DLLEXPORT int lenght();

	void detect(const cv::Mat img, std::vector<cv::Rect>& rect);
}

注意在第一部分提到的,用extern "C"關鍵字轉譯爲C編譯。生成的DLL用於python調用即可。

二、python調用DLL

直接上python代碼如下

import numpy as np
import ctypes
import cv2

#載入dll文件的路徑
test = ctypes.cdll.LoadLibrary("..\\x64\\Release\\dll2py.dll")
#讀入圖片
frame = cv2.imread('..\\circle.png')
#將opencv的mat數據類型轉換爲char*的數據格式
frame_data = np.asarray(frame, dtype=np.uint8)
frame_data = frame_data.ctypes.data_as(ctypes.c_char_p)
#定義輸出數據格式(此處似乎有一個bug,即x64框架下僅支持uint64格式,其他格式會報錯)
test.py2cpp.restype = ctypes.c_uint64
test.lenght.restype = ctypes.c_uint64
#調用dll的接口函數
buf = test.py2cpp(frame.shape[0], frame.shape[1], frame_data)
mlenght = test.lenght()
#打印返回值的指針首地址
print('地址值:')
print('%#x'%buf)
#打印返回框的個數
print('框個數:')
print(mlenght)
#數據解析:返回的指針是short類型(16位),但返回地址是按照8位存儲的(此處實際上是存儲的二進制數據,但是是以ASCII碼形式存儲的,因此僅能一次性存儲8位)。我們的16位返回數據,是被拆解爲高8位和低8位分別存儲的,且先讀出來的是低位,厚度出來的是高位,按照這一原則進行數據拼接。
bit_typr = 4*2
for num in range(0, mlenght):#框的個數循環
    #x數據解析,先讀低8位,後讀高8位
    L8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr, 1), byteorder='big', signed=False)
    H8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 1, 1), byteorder='big', signed=False)
    x = H8 << 8
    x = x + L8
    #y數據解析,先讀低8位,後讀高8位
    L8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 2, 1), byteorder='big', signed=False)
    H8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 3, 1), byteorder='big', signed=False)
    y = H8 << 8
    y = y + L8
    #w數據解析,先讀低8位,後讀高8位
    L8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 4, 1), byteorder='big', signed=False)
    H8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 5, 1), byteorder='big', signed=False)
    w = H8 << 8
    w = w + L8
    #h數據解析,先讀低8位,後讀高8位
    L8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 6, 1), byteorder='big', signed=False)
    H8 = int.from_bytes(ctypes.string_at(buf + num * bit_typr + 7, 1), byteorder='big', signed=False)
    h = H8 << 8
    h = h + L8
    print(x, y, w, h)
    cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 1, 8)

cv2.namedWindow('src', cv2.WINDOW_AUTOSIZE)
cv2.imshow('src', frame)
cv2.waitKey(0)
cv2.destroyAllWindows()

運行結果如下圖所示:

實現對圖中兩個圓的外接矩形定位框檢測。

log信息分享如下:

其中紅色框內是由dll內部輸出的log信息,分別是:矩形框的座標(起點座標和框的長寬尺寸)、返回的首地址值、返回的數據長度值(有幾個short數據);

黃色框內是python腳本打印的log信息,分別是:返回的首地址值、返回的數據長度值(有幾個short數據)、矩形框的座標(起點座標和框的長寬尺寸)。

可以看到結果完全一致,是能夠對的上的。

再次提醒:第一部分前言中需要注意關注的幾點,否則可能會各種報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章