参观飞机工厂不能让你学得流体力学,也不能让你学会开飞机。然而如果你会开飞机又懂流体力学,参观飞机工厂可以带给你最大的乐趣和价值。-- 侯捷

献给 @AliceInt-ZLJ by Knight @2019/11/23

Knight:191123 使用 Pybind11 和 OpenCV 创建 Python 库​zhuanlan.zhihu.com


2020.03.01 更新:

新增 github 项目地址 ausk/keras-unet-deploy 。

本文内容可在 keras-unet-deploy/cpp/unet/pycv 目录下查看。


C++ 是一种编译型(compiled)语言,设计重点是性能、效率和使用灵活性,偏向于系统编程、嵌入式、资源受限的软件和系统。Mordern C++ 支持宏、泛型重载、面向过程、面向对象、函数式等不同风格。模板类型萃取、容器、迭代器、算法是C++ STL 标准库的重要组成部分,善用之可改善编码效率。

Python是一种解释型(interpreted)语言,同样也支持不同的编程范式。Python 内置了常用数据结构(str, tuple, list, dict),支持鸭子类型(动态类型)和自动垃圾回收,加上简洁的语法、丰富的内置库(os,sys,urllib,...)和三方库(numpy, tf, torch ...),使其成为瑞士军刀,啥都能做。

有时候,我们需要编译型语言(C++)性能,以及解释型语言(Python)的灵活, 而 pybind11 则可以用作 C++ 和 Python 之间沟通的桥梁。Pybind11 是一个轻量级只包含头文件的库,用于 Python 和 C++ 之间接口转换,可以为现有的 C++ 代码创建 Python 接口绑定。Pybind11 通过 C++ 编译时的自省来推断类型信息,来最大程度地减少传统拓展 Python 模块时繁杂的样板代码, 已经实现了 STL 数据结构、智能指针、类、函数重载、实例方法等到Python的转换,其中函数可以接收和返回自定义数据类型的值、指针或引用。


最近在研究 openpose 人体骨骼关键点检测的非官方 keras 代码实现,转 pb 模型文件后,使用 c++ 版 tensorflow 进行推理及后处理。由于使用 C++ 重写了 keras python 版对应的后处理部分(当然 openpose 源码里有实现,不过由于其嵌入到了工程里,导致难以直接复用), 典型的后处理部分从 200 ms 减少到 20 ms。突发奇想,可不可以把重写后的代码暴露给 Python 调用。在 C++ 端,使用 cv::Mat 表示多维矩阵;对应地在 Python 端使用 numpy.ndarray 表示多维矩阵。经过各种尝试后,发现了使用 pybind11 基本可以实现 C++ 和 Python 之间的无缝对接,从而实现代码复用。

需要暴露接口的典型的 OpenCV 数据结构有 cv::Point, cv::Rect, cv::Mat, 其中 cv::Point 是 cv::Point_<int> 特化,cv::Rect 是 cv::Rect_<int> 特化,而常用的 cv::Mat 中的元素类型有 CV_8U (uchar), CV_32S (int), CV_32F (float)。

核心,是对上述需要转换的数据结构 T,通过添加模板重载结构体 pybind11::detail::type_caster<T>,实现对类型 T 的转换方法 load 和 cast 的注册。

1. 点与元组转换 cv::Point <=> tuple(x,y)

实现 cv::Point 和 tuple(x,y) 之间的转换, 需要特化模板结构体 pybind11::detail::type_caster<cv::Point>,然后实现 load 和 cast 方法。

简洁版:

namespace pybind11 { namespace detail{template<>
struct type_caster<cv::Point>{    PYBIND11_TYPE_CASTER(cv::Point, _("tuple_xy"));    bool load(handle obj, bool){        py::tuple pt = reinterpret_borrow<py::tuple>(obj);        value = cv::Point(pt[0].cast<int>(), pt[1].cast<int>());       return true;    }    static handle cast(const cv::Point& pt, return_value_policy, handle){        return py::make_tuple(pt.x, pt.y).release();   }
};
}}//! end namespace pybind11::detail

加上注释和参数检验:

namespace pybind11 { namespace detail{//! 实现 cv::Point 和 tuple(x,y) 之间的转换。
template<>
struct type_caster<cv::Point>{//! 定义 cv::Point 类型名为 tuple_xy, 并声明类型为 cv::Point 的局部变量 value。PYBIND11_TYPE_CASTER(cv::Point, _("tuple_xy"));//! 步骤1:从 Python 转换到 C++。    //! 将 Python tuple 对象转换为 C++ cv::Point 类型, 转换失败则返回 false。    //! 其中参数2表示是否隐式类型转换。   bool load(handle obj, bool){        // 确保传参是 tuple 类型        if(!py::isinstance<py::tuple>(obj)){            std::logic_error("Point(x,y) should be a tuple!");            return false;       }       // 从 handle 提取 tuple 对象,确保其长度是2。        py::tuple pt = reinterpret_borrow<py::tuple>(obj);        if(pt.size()!=2){            std::logic_error("Point(x,y) tuple should be size of 2");            return false;        }       //! 将长度为2的 tuple 转换为 cv::Point。        value = cv::Point(pt[0].cast<int>(), pt[1].cast<int>());       return true;    }//! 步骤2: 从 C++ 转换到 Python。    //! 将 C++ cv::Mat 对象转换到 tuple,参数2和参数3常忽略。    static handle cast(const cv::Point& pt, return_value_policy, handle){       return py::make_tuple(pt.x, pt.y).release();   }
};
}} //!  end namespace pybind11::detail

2. 矩形与元组转换 cv::Rect <=> tuple(x,y,w,h)

与 cv::Point 转换类似,实现 cv::Rect 和 tuple(x,y,w,h) 之间的转换, 也需要上述步骤。

namespace pybind11 { namespace detail{template<>
struct type_caster<cv::Rect>{    PYBIND11_TYPE_CASTER(cv::Rect, _("tuple_xywh"));  bool load(handle obj, bool){        if(!py::isinstance<py::tuple>(obj)){            std::logic_error("Rect should be a tuple!");            return false;        }        py::tuple rect = reinterpret_borrow<py::tuple>(obj);       if(rect.size()!=4){            std::logic_error("Rect (x,y,w,h) tuple should be size of 4");            return false;        }      value = cv::Rect(rect[0].cast<int>(), rect[1].cast<int>(), rect[2].cast<int>(), rect[3].cast<int>());        return true;   }    static handle cast(const cv::Rect& rect, return_value_policy, handle){        return py::make_tuple(rect.x, rect.y, rect.width, rect.height).release();    }
};
}} //!  end namespace pybind11::detail

3. cv::Mat 与 numpy.ndarray 之间转换

Python 支持一种通用的插件间数据交换缓冲区协议(buffer protocol)。让类型暴露一个缓冲区视图(buffer view), 这样可以对原始内部数据进行直接访问,常用于矩阵类型。

Pybind11 提供了 py::buffer_info 类型,来映射 Python 缓冲区协议(buffer protocol)。

struct buffer_info {    void *ptr;                      /* Pointer to buffer */    ssize_t itemsize;               /* Size of one scalar */    std::string format;             /* Python struct-style format descriptor */    ssize_t ndim;                   /* Number of dimensions */    std::vector<ssize_t> shape;     /* Buffer dimensions */    std::vector<ssize_t> strides;    /* Strides (in bytes) for each index */
};

由于 cv::Mat 和 numpy.ndarray 的数据存储方式都是 C 风格的行主序,所以使用 buffer_info 映射时比较一致而方便。考虑到常用的 cv::Mat 元素类型有 uchar、int、float, 常用的维度有 2d、3d,因此对这几种情况提供转换。

namespace pybind11 { namespace detail{template<>
struct type_caster<cv::Mat>{public:   PYBIND11_TYPE_CASTER(cv::Mat, _("numpy.ndarray")); //! 1. cast numpy.ndarray to cv::Mat    bool load(handle obj, bool){        array b = reinterpret_borrow<array>(obj);        buffer_info info = b.request();    int nh = 1;        int nw = 1;        int nc = 1;        int ndims = info.ndim;        if(ndims == 2){           nh = info.shape[0];           nw = info.shape[1];       } else if(ndims == 3){            nh = info.shape[0];           nw = info.shape[1];           nc = info.shape[2];        }else{            throw std::logic_error("Only support 2d, 2d matrix");            return false;       }       int dtype;        if(info.format == format_descriptor<unsigned char>::format()){            dtype = CV_8UC(nc);        }else if (info.format == format_descriptor<int>::format()){            dtype = CV_32SC(nc);       }else if (info.format == format_descriptor<float>::format()){           dtype = CV_32FC(nc);        }else{            throw std::logic_error("Unsupported type, only support uchar, int32, float"); return false;}   value = cv::Mat(nh, nw, dtype, info.ptr);return true;    }    //! 2. cast cv::Mat to numpy.ndarray    static handle cast(const cv::Mat& mat, return_value_policy, handle defval){        std::string format = format_descriptor<unsigned char>::format();size_t elemsize = sizeof(unsigned char);int nw = mat.cols;int nh = mat.rows;int nc = mat.channels();int depth = mat.depth();int type = mat.type();int dim = (depth == type)? 2 : 3;if(depth == CV_8U){format = format_descriptor<unsigned char>::format();elemsize = sizeof(unsigned char);}else if(depth == CV_32S){format = format_descriptor<int>::format();elemsize = sizeof(int);}else if(depth == CV_32F){format = format_descriptor<float>::format();elemsize = sizeof(float);}else{            throw std::logic_error("Unsupport type, only support uchar, int32, float");}        std::vector<size_t> bufferdim;std::vector<size_t> strides;if (dim == 2) {bufferdim = {(size_t) nh, (size_t) nw};strides = {elemsize * (size_t) nw, elemsize};} else if (dim == 3) {bufferdim = {(size_t) nh, (size_t) nw, (size_t) nc};strides = {(size_t) elemsize * nw * nc, (size_t) elemsize * nc, (size_t) elemsize};}return array(buffer_info( mat.data,  elemsize,  format, dim, bufferdim, strides )).release();
}};}}//! end namespace pybind11::detail

4. 创建 pycv 简单的调用 opencv 的库

有了上述的类型转换代码,就可以把现有的使用了 cv::Point、cv::Rect、及 cv::Mat 等类型的代码编译为 Python 的库。

如,可以定义 addpt, addrect, addmat, imwrite 这4个函数,分别实现点相加、矩形相加、矩阵元素相加,保存矩阵为图片等操作,然后封装为简单的 cv 库。

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <pybind11/stl.h>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>namespace pybind11 { namespace detail{//! 上述几个实现}}//!//! 点相加
cv::Point addpt(cv::Point& lhs, cv::Point&rhs){return cv::Point(lhs.x + rhs.x, lhs.y + rhs.y);
}//! 矩形相加
cv::Rect addrect(cv::Rect& lhs, cv::Rect&rhs){int x = std::min(lhs.x, rhs.x);int y = std::min(lhs.y, rhs.y);int width  = std::max(lhs.x + lhs.width-1,  rhs.x + rhs.width-1) - x + 1;int height = std::max(lhs.y + lhs.height-1, rhs.y + rhs.height-1) - y + 1;return cv::Rect(x, y, width, height);
}//! 矩阵相加
cv::Mat addmat(cv::Mat& lhs, cv::Mat& rhs){return lhs + rhs;
}//! 写入图片
void imwrite(std::string fpath, const cv::Mat& img){cv::imwrite(fpath, img);
}PYBIND11_MODULE(pycv, m){m.doc() = "pycv - A small OpenCV binding created using Pybind11";m.def("addpt", &addpt, "add two point");m.def("addrect", &addrect, "add two rect");m.def("addmat", &addmat, "add two matrix");    m.def("imwrite", &imwrite, "write into the file");
}

可以使用 cmake 创建工程,编译为动态库 pycv.so 或 pycv.pyd,然后使用 python 测试。

导入 python 后查看帮助:

>>> import pycv
>>> help(pycv)Help on module pycv:NAMEpycv - A small OpenCV binding created using Pybind11FUNCTIONSaddmat(...) method of builtins.PyCapsule instanceaddmat(arg0: numpy.ndarray, arg1: numpy.ndarray) -> numpy.ndarrayadd two matrixaddpt(...) method of builtins.PyCapsule instanceaddpt(arg0: tuple_xy, arg1: tuple_xy) -> tuple_xyadd two pointaddrect(...) method of builtins.PyCapsule instanceaddrect(arg0: tuple_xywh, arg1: tuple_xywh) -> tuple_xywhadd two rectimwrite(...) method of builtins.PyCapsule instanceimwrite(arg0: str, arg1: numpy.ndarray) -> Nonewrite into file

测试:

# 2019/11/23
# create by knight
import pycv
import numpy as npdef test():# 矩阵相加a = np.random.random((2,3)).astype(np.float32)b = np.ones((2,3)).astype(np.float32)c = pycv.addmat(a, b)print("a = n", a)print("b = n", b)print("c = n", c)# 点相加a = (1,2)b = (3,-1)c = pycv.addpt(a, b)print("{} + {} = {}".format(a, b, c))# 矩形相加a = (10, 20, 20, 10) # lt(10, 20) => rb(30, 30)b = (5, 5, 20, 20)   # lt(5, 5)   => rb(25, 25)c = pycv.addrect(a, b) # lt(5, 5)   => rb(30, 30) => wh(25, 25)print("a = n", a)print("b = n", b)print("c = n", c)if __name__ == "__main__":    test()

测试结果:

a = [[0.0108426  0.8217642  0.49240646][0.7879406  0.82095915 0.5334907 ]]
b = [[1. 1. 1.][1. 1. 1.]]
c = [[1.0108426 1.8217642 1.4924065][1.7879406 1.8209591 1.5334907]](1, 2) + (3, -1) = (4, 1)a = (10, 20, 20, 10)
b = (5, 5, 20, 20)
c = (5, 5, 25, 25)

5. 更多测试

将重新用 C++ 写的 openpose 后处理部分,同样转换为 python 库,并在 python 中调用测试,循环了5次,只计算后处理的时间,结果如下:

=> Finish 4 people, #1 pyb.pyfindHeatmapPafPose in 17.04 ms
=> Finish 4 people, #2 pyb.pyfindHeatmapPafPose in 15.27 ms
=> Finish 4 people, #3 pyb.pyfindHeatmapPafPose in 14.98 ms
=> Finish 4 people, #4 pyb.pyfindHeatmapPafPose in 15.01 ms
=> Finish 4 people, #5 pyb.pyfindHeatmapPafPose in 15.05 ms

使用 openpose 检测人体骨架关键点,得到的结果看上去还不错。

openpsoe 人体骨骼关键点检测后处理结果

参考链接:

1. https://en.wikipedia.org/wiki/C%2B%2B
2. https://en.wikipedia.org/wiki/Python_(programming_language)
3. https://github.com/pybind/pybind11
4. https://pybind11.readthedocs.io/en/stable/
5. https://github.com/CMU-Perceptual-Computing-Lab/openpose/
6. https://github.com/michalfaber/keras_Realtime_Multi-Person_Pose_Estimation


《完》

献给 @AliceInt-ZLJ by Knight

ndarray python 映射_191123 使用 Pybind11 和 OpenCV 创建 Python 库相关推荐

  1. python人脸识别理论_使用OpenCV和Python进行人脸识别

    介绍 人脸识别是什么?或识别是什么?当你看到一个苹果时,你的大脑会立刻告诉你这是一个苹果.在这个过程中,你的大脑告诉你这是一个苹果水果,用简单的语言来说就是识别.那么什么是人脸识别呢?我肯定你猜对了. ...

  2. opencv 训练人脸对比_【项目案例python与人脸识别】基于OpenCV开源计算机视觉库的人脸识别之python实现...

    " 本项目是一个基于OpenCV开源库使用python语言程序实现人脸检测的项目,该项目将从[项目基础知识](即人脸识别的基本原理).[项目实践](人脸识别所需要的具体步骤及其python程 ...

  3. 使用opencv和python进行智能图像处理_使用OpenCV在Python中进行图像处理

    编辑推荐: 本文将先讨论一些图像处理,然后再继续介绍可以方便使用图像处理的不同应用程序/场景,希望对您的学习有所帮助. 本文来自于tecdat ,由火龙果软件Alice编辑.推荐. 介绍 在本教程中, ...

  4. python找图片不同_用openCV和Python 实现图片对比,并标识出不同点的方式

    最近项目中需要实现两组图片对比,并能将两者的区别标识出来. 在网上搜索一大堆找到一篇大神的文章,最终实现该功能,在这里记录下: 想要实现此demo,首先我们得确保电脑上已安装 openCV 和 Pyt ...

  5. python实现对数转换_利用opencv在python平台上实现二值变换,伽马变换,对数变换,补色变换等...

    如何用 opencv 在 python 平台上实现灰度图像的二值化,对数变换, ,伽马变换以及补色变 换.代码如下 import cv2 import copy import math import  ...

  6. Python虚拟环境(一):基于virtualenv+virtualenvwrapper创建python虚拟环境

    文章目录 1. 概述 2. virtualenv + virtualenvwrapper 1. virtualenv 安装virtualenv 基本使用 2. virtualenvwrapper 安装 ...

  7. 《python》学习笔记(Day1),创建python发布

    一.创建发布,并使用模块中的函数. (1)添加系统环境变量.我的电脑----->属性----->高级----->环境变量----->系统变量,选择系统变量中的变量path,单击 ...

  8. 【Android 逆向】使用 Python 代码解析 ELF 文件 ( PyCharm 中创建 Python 程序 | 导入 ELFFile 库 | 解析 ELF 文件 )

    文章目录 一.PyCharm 中创建 Python 程序 二.导入 ELFFile 依赖库 三. 解析 ELF 文件 四. 博客源码 一.PyCharm 中创建 Python 程序 在 PyCharm ...

  9. python映射类型有哪些_什么是python中唯一的映射类型

    字典是python中唯一的映射类型,采用键值对(key-value)的形式存储数据.python对key进行哈希函数运算,根据计算的结果决定value的存储地址,所以字典是无序存储的,且key必须是可 ...

最新文章

  1. 调试linux内核前的多虚拟机网络配置(图文教程)
  2. SURF角点检测(python)
  3. kcbzps oracle_快速进行Oracle安装及配置
  4. 曲线 线性回归_GRAPHPAD作图技巧(二)--拟合曲线
  5. 如何安装_如何安装吸顶灯?吸顶灯安装注意事项
  6. vba实现粘贴复制功能
  7. java数独求交集方法,标准数独解题之旅(用一道数独题讲解最基本的5种解题技巧)(二)...
  8. 同源、跨域、跨站、SameSite与withCredentials
  9. SCI英语论文长难句攻略
  10. 洛谷p3398仓鼠找suger题解
  11. Python函数combination
  12. 夜神模拟器换完本机的ip连不上忘 fiddler也抓不到模拟器的包
  13. 矿小助 全局主题 | 一个插件实现网易云音乐主题效果 | Flutter
  14. android蓝牙传文件,安卓手机怎样使用蓝牙连接传输文件
  15. Kali Linux学习笔记—Web渗透(1)
  16. Zbrush一些基本操作
  17. 【疑难杂症】JavaScript执行期上下文
  18. 今年有多少周?今天是今年的第几天?第几周?
  19. python计算两个日期的相隔时间
  20. Opencv+Zbar二维码识别(标准条形码/二维码识别)

热门文章

  1. pve虚拟机导入gho_迁移WIN10和VMW虚拟机到ProXmoX VE(二):PVE设置和迁移windows
  2. uFrame近况(2016年4月8日更新)
  3. OpenShift 4 - 用KubeletConfig和ContainerRuntimeConfig分别修改集群节点的Kubelet和cri-o的配置
  4. OpenShift 4 - 通过DaemonSet在指定Node上运行守护程序
  5. 基于 .Net5.0 的快速开发框架,YuebonCore1.0.3 版已发布
  6. 如何创建从Visual Studio到Wolfram Mathematica的简单调用
  7. 全新设计的 Xcode 12
  8. .NET Core Web API:您需要了解的最少知识(第2部分,共2部分)
  9. 如何为项目和产品提供资源——优化工作时间、激励团队和预算
  10. 开源项目的名称背后都有哪些故事?