第一部分、前言
在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& rect)
{
cv::Mat dst;
if (3 == img.channels()) { cv::cvtColor(img, dst, cv::COLOR_RGB2GRAY); }
else { img.copyTo(dst); }
std::vector> contours;
std::vector 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 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(i, 0) = list[i].x;
tres.at(i, 1) = list[i].y;
tres.at(i, 2) = list[i].width;
tres.at(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的首地址,其内存放的是 轮廓最小外接矩形。
该接口封装函数,实现了将由python传进来的数据,整合成cpp功能函数方便调用的数据类型,再将功能函数的返回值转化为方便python获取的形式再传出,从而完成数据通信。同时, 还应当返回一个数据长度的量,以方便在内存中进行读取,在上述借口封装的代码中,已经将这个数据长度存储在全局变量g_value下,另写一个函数返回这个全局变量即可。代码如下:
int lenght()
{
std::cout << g_value << std::endl;
return g_value;
}
以上,是dll的代码实现,写在cpp文件中,其头文件内容如下:
#pragma once
#include
#include
#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& 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()
实现对图中两个圆的外接矩形定位框检测。
其中红色框内是由dll内部输出的log信息,分别是:矩形框的坐标(起点坐标和框的长宽尺寸)、返回的首地址值、返回的数据长度值(有几个short数据);
黄色框内是python脚本打印的log信息,分别是:返回的首地址值、返回的数据长度值(有几个short数据)、矩形框的坐标(起点坐标和框的长宽尺寸)。
可以看到结果完全一致,是能够对的上的。
再次提醒:第一部分前言中需要注意关注的几点,否则可能会各种报错。