從PyOpenCV到CV2的類型轉換

原文地址:從PyOpenCV到CV2 作者:HyryStudio

安裝cv2

http://hyry.dip.jp/files/opencv.zip

採用cv2重寫的《Python科學計算》中的實例程序

讀者可以在下面的頁面中搜索“opencv”,並根據Python版本下載對應的安裝程序。

http://www.lfd.uci.edu/~gohlke/pythonlibs/

非官方的Windows系統Python擴展庫

安裝完畢之後,運行下面的程序,測試是否安裝正確。

import cv2
import sys

try:
    filename = sys.argv[1]
except:
    filename = "lena.jpg"
img = cv2.imread( filename )
cv2.namedWindow("demo1")
cv2.imshow("demo1", img)
cv2.waitKey(0)
cv和cv2

本章所介紹的代碼均採用如下載入方式:

>>> import cv2
>>> from cv2 import cv

cv2擴展庫是針對OpenCV 2.x API創建的,它直接採用NumPy的數組對象表示圖像,因此和PyOpenCV相比,不再需要在數組和Mat對象之間相互轉換了。

爲了兼容OpenCV 1.x API,在cv下提供了原來的OpenCV 1.x API的擴展庫。如果讀者發現cv2下缺少某個功能,可以使用cv下提供的函數。

cv2下的函數直接對NumPy的數組進行操作,而cv則對兩種表示圖像的cvmat和iplimage對象進行操作。如果需要混用這兩套API中的函數,就需要在它們之間進行轉換,下面讓我們看一個在這些類型之間轉換的例子。

>>> array = cv2.imread("lena.jpg")
>>> iplimage = cv.LoadImage("lena.jpg")
>>> cvmat = cv.LoadImageM("lena.jpg")

首先通過cv2.imread()讀入的圖像使用NumPy數組表示,而通過cv.LoadImage()讀入的圖像爲iplimage對象,通過cv.LoadImage()讀入的是cvmat對象。

>>> type(array), array.shape, array.dtype
(<type 'numpy.ndarray'>, (393, 512, 3), dtype('uint8'))
>>> iplimage
<iplimage(nChannels=3 width=512 height=393 widthStep=1536 )>
>>> cvmat
<cvmat(type=42424010 8UC3 rows=393 cols=512 step=1536 )>

下表列出了在這三種對象之間轉換的方法:

在數組、iplimage以及cvmat之間轉換
類型轉換 方法
array→cvmat cv.fromarray(array)
cvmat→array np.asarray(cvmat)
cvmat→iplimage cv.GetImage(cvmat)
iplimage→cvmat iplimage[:],或cv.GetMat(iplimage)

如果需要在array和iplimage之間轉換,可以通過cvmat作爲橋樑,例如:

>>> array2 = np.asarray(iplimage[:])
>>> np.all(array == array2)
True

由於iplimage類型需要其數據保存在連續的內存空間之中,因此使用切片獲得的數組需要複製之後才能轉換爲iplimage:

>>> cv.GetImage(cv.fromarray(array[::2,::2,:]))
<ERROR: expected a single-segment buffer object>
<iplimage(nChannels=3 width=256 height=197 widthStep=3072 )>
>>> cv.GetImage(cv.fromarray(array[::2,::2,:].copy()))
<iplimage(nChannels=3 width=256 height=197 widthStep=768 )>

在《Python科學計算》的OpenCV實例所用到的函數中,只有pyrSegmentation()不在cv2中,因此使用了cv下的PyrSegmentation(),並在恰當的地方進行圖像類型的轉換。

opencv_pyrSegmentation.py

使用cv.PyrSegmentation()進行圖像分割

由於OpenCV 1.x API已經逐漸被淘汰,後續的章節將只詳細介紹cv2的使用方法。

cv2與PyOpenCV

cv2中的函數名與PyOpenCV的相同,部分常量名有所不同。但是cv2中的函數所需的參數類型儘量使用數組或者一些Python的標準數據類型。因此cv2中沒有Mat、Point、Size、Vec等各種數據類型,而是用列表、元組或數組表示這些數據類型。因此使用cv2中的函數比PyOpenCV更加便捷,然而你需要清楚cv2的數據類型轉換規則,這樣才能將正確的數據專遞給函數。

分析cv2的源程序

爲了瞭解cv2所做的數據轉換工作,需要我們分析cv2的源程序。下載OpenCV的源程序,解壓之後,可以在“opencvmodulespythonsrc2”路徑下找到cv2相關的源程序。cv2中各個包裝函數是通過cv2.py自動生成的:在命令行中切到“src2目錄下,並運行命令“cv2.py . ”,將在當前目錄下生成OpenCV的包裝函數。如果執行提示失敗,可在此目錄下創建一個空的“opencv_extra_api.hpp”文件之後再試。

所有的包裝函數都在自動生成的“pyopencv_generated_funcs.h”中定義。而這些包裝函數會調用“cv2.cpp”中的衆多pyopencv_to()和pyopencv_from()函數,實現Python和OpenCV的各種類型轉換工作。若不能確定包裝函數使用何種Python數據類型,可以查看包裝函數的內容,例如下面是運行cv2.line()時所調用的C語言函數。

pyopencv_generated_funcs.h, cv2.cpp

在這兩個文件中定義了cv2的包裝函數和各種類型轉換函數

static PyObject* pyopencv_line(PyObject* , PyObject* args, PyObject* kw)
{
    PyObject* pyobj_img = NULL;
    Mat img;
    PyObject* pyobj_pt1 = NULL;
    Point pt1;
    PyObject* pyobj_pt2 = NULL;
    Point pt2;
    PyObject* pyobj_color = NULL;
    Scalar color;
    int thickness=1;
    int lineType=8;
    int shift=0;

    const char* keywords[] = { "img", "pt1", "pt2", "color", "thickness","lineType", "shift", NULL };
    if( PyArg_ParseTupleAndKeywords(args, kw, "OOOO|iii:line", (char**)keywords,&pyobj_img, &pyobj_pt1,
        &pyobj_pt2, &pyobj_color, &thickness, &lineType, &shift) &&
        pyopencv_to(pyobj_img, img) &&
        pyopencv_to(pyobj_pt1, pt1) &&
        pyopencv_to(pyobj_pt2, pt2) &&
        pyopencv_to(pyobj_color, color) )
    {
        ERRWRAP2( cv::line(img, pt1, pt2, color, thickness, lineType, shift));
        Py_RETURN_NONE;
    }

    return NULL;
}

OpenCV中的line()所需的4個參數類型爲:Mat、Point、Point和Scalar,程序中使用4個pyopencv_to()將Python的數據轉換爲這些類型。pyopencv_to()有衆多重載函數,例如上述的類型轉換實際上會調用如下三個函數:

static int pyopencv_to(const PyObject* o, Mat& m, const char* name = "<unknown>", bool allowND=true);
static inline bool pyopencv_to(PyObject* obj, Point& p, const char* name = "<unknown>");
static bool pyopencv_to(PyObject *o, Scalar& s, const char *name = "<unknown>");

其中Mat對應的pyopencv_to()將數組轉換爲Mat對象,其代碼實現比較複雜,暫時忽略。我們看看Point的轉換函數:

static inline bool pyopencv_to(PyObject* obj, Point& p, const char* name = "<unknown>")
{
    if(!obj || obj == Py_None)
        return true;
    if(PyComplex_CheckExact(obj))
    {
        Py_complex c = PyComplex_AsCComplex(obj);
        p.x = saturate_cast<int>(c.real);
        p.y = saturate_cast<int>(c.imag);
        return true;
    }
    return PyArg_ParseTuple(obj, "ii", &p.x, &p.y) > 0;
}

稍微分析一下此程序可知,它可以將Python的複數和元組轉換爲Point對象。例如100+200j或者(100,200)。

Scalar對應的函數爲:

static bool pyopencv_to(PyObject *o, Scalar& s, const char *name = "<unknown>")
{
    if(!o || o == Py_None)
        return true;
    if (PySequence_Check(o)) {
        PyObject *fi = PySequence_Fast(o, name);
        if (fi == NULL)
            return false;
        if (4 < PySequence_Fast_GET_SIZE(fi))
        {
            failmsg("Scalar value for argument '%s' is longer than 4", name);
            return false;
        }
        for (Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(fi); i++) {
            PyObject *item = PySequence_Fast_GET_ITEM(fi, i);
            if (PyFloat_Check(item) || PyInt_Check(item)) {
                s[(int)i] = PyFloat_AsDouble(item);
            } else {
                failmsg("Scalar value for argument '%s' is not numeric", name);
                return false;
            }
        }
        Py_DECREF(fi);
    } else {
        if (PyFloat_Check(o) || PyInt_Check(o)) {
            s[0] = PyFloat_AsDouble(o);
        } else {
            failmsg("Scalar value for argument '%s' is not numeric", name);
            return false;
        }
    }
    return true;
}

可以看出這個函數能將長度小於等於4的序列,整數、浮點數轉換爲Scalar類型。對於整數和浮點數,它將保存進Scalar對象的第0個元素。

如果讀者不清楚某個函數所需的參數類型,可以仿照上述方法從“pyopencv_generated_funcs.h”中的包裝函數和對應的pyopencv_to()轉換函數找到答案。

在PyOpenCV中爲了保存處理結果,我們需要創建一個空的Mat對象,並將其傳遞給處理函數。處理函數會爲此Mat對象添加處理結果。在cv2中一切變得簡單了,處理結果可以通過函數的返回值獲得。如果需要讓處理結果保存到指定的數組之中,也可以將數組傳遞給dst參數。下面看一個例子,我們希望調用blur()對圖像進行模糊處理,從blur()的文檔我們可以看到如下參數調用說明:

C++:
blur(InputArray src, OutputArray dst, Size ksize,
     Point anchor=Point(-1,-1), int borderType=BORDER_DEFAULT )

Python:
cv2.blur(src, ksize[, dst[, anchor[, borderType]]]) → dst

可以看到在C++中ksize是一個Size對象,Size和Point類似,因此Python中只需要傳遞一個元組即可。dst參數是可選參數,下面我們用代碼測試一下:

>>> img = cv2.imread("lena.jpg")
>>>
>>> img2 = cv2.blur(img, (5,5))
>>>
>>> img3 = np.empty_like(img)             # 先分配一個相同大小的數組
>>> img4 = cv2.blur(img, (5,5), dst=img3) # 然後通過dst參數指定保存結果數組
>>>
>>> np.all(img2==img3)
True
>>> img3 is img4                         # 當指定dst參數時,返回值和dst參數是同一個對象
True
常用的類型轉換

下面列出一些我在將書中的實例程序移植到cv2下時總結的類型轉換,讀者可以參照本節的內容分析移植之後的程序。

下面通過幾個例子說明參數的傳遞方法。

在PyOpenCV的實例中,計算直方圖統計的calcHist()參數十分複雜,而由於cv2的自動類型轉換功能,calcHist()的用法變得簡單多了。cv2中calcHist()的幫助文檔如下:

calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) -> hist

由於在C++中,同樣的函數名可以對應多種參數的函數實現,因此我們需要確定cv2中所調用的C++函數類型,下面是“pyopencv_generated_funcs.h”中對calcHist()進行包裝的函數。

static PyObject* pyopencv_calcHist(PyObject* , PyObject* args, PyObject* kw)
{
    PyObject* pyobj_images = NULL;
    vector_Mat images;
    PyObject* pyobj_channels = NULL;
    vector_int channels;
    PyObject* pyobj_mask = NULL;
    Mat mask;
    PyObject* pyobj_hist = NULL;
    Mat hist;
    PyObject* pyobj_histSize = NULL;
    vector_int histSize;
    PyObject* pyobj_ranges = NULL;
    vector_float ranges;
    bool accumulate=false;
    ...
}

通過這些參數類型,可以在OpenCV源程序中找到其對應的C++函數:

void cv::calcHist( InputArrayOfArrays images, const vector<int>& channels,
             InputArray mask, OutputArray hist,
             const vector<int>& histSize,
             const vector<float>& ranges,
             bool accumulate );

可以看出images參數對應vector_Mat類型、channels參數vector_int類型、mask對應Mat類型、histSize對應vector_int類型、ranges對應vector_float類型。而可選hist參數則用來指定輸出結果的數組。

這些vector_*類型在“cv2.cpp”中定義:

typedef vector<uchar> vector_uchar;
typedef vector<int> vector_int;
typedef vector<float> vector_float;
typedef vector<double> vector_double;
typedef vector<Point> vector_Point;
typedef vector<Point2f> vector_Point2f;
typedef vector<Vec2f> vector_Vec2f;
typedef vector<Vec3f> vector_Vec3f;
typedef vector<Vec4f> vector_Vec4f;
typedef vector<Vec6f> vector_Vec6f;
typedef vector<Vec4i> vector_Vec4i;
typedef vector<Rect> vector_Rect;
typedef vector<KeyPoint> vector_KeyPoint;
typedef vector<Mat> vector_Mat;
typedef vector<vector<Point> > vector_vector_Point;
typedef vector<vector<Point2f> > vector_vector_Point2f;
typedef vector<vector<Point3f> > vector_vector_Point3f;

由此可知這些都是vector<Type>類型,可以通過序列對象指定參數,下面是調用calcHist()的實例程序,其中使用了列表序列和元組序列:

import cv2
import numpy as np

img = cv2.imread("lena.jpg")

result = cv2.calcHist([img],
                     channels = (0,1),
                     mask = None,
                     histSize = (30, 20),
                     ranges = (0, 256, 0, 256))

hist, _x, _y = np.histogram2d(img[:,:,0].flatten(), img[:,:,1].flatten(),
    bins=(30,20), range=[(0,256),(0,256)])

print np.all(hist == result)

請注意由於ranges對應vector<float>類型,因此和np.histogram2d()不同,不用指定多層嵌套的數據結構。至於calcHist()的C++程序是如何使用ranges中的數據的,請參考其源程序。

發佈了27 篇原創文章 · 獲贊 30 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章