單目相機測距之一

其他參考

  1. 基於OpenCV實現二維碼發現與定位
  2. Zbar+ROS+opencv二維碼識別與定位研究(一)

  3. 基於二維碼的單目視覺測距移動機器人定位研究

  4. (opencv+Qt)的QR碼精確定位與識別完全解析(精度可達±0.1mm,±0.1°)

  5. 單目攝像機測距(python+opencv)

  6. PnP 單目相機位姿估計(二):solvePnP利用二維碼求解相機世界座標 

  7. PnP 單目相機位姿估計(三):二維碼角點檢測

基於二維碼的室內定位技術(一)——原理

轉自https://zhou-yuxin.github.io/articles/2017/%E5%9F%BA%E4%BA%8E%E4%BA%8C%E7%BB%B4%E7%A0%81%E7%9A%84%E5%AE%A4%E5%86%85%E5%AE%9A%E4%BD%8D%E6%8A%80%E6%9C%AF%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E5%8E%9F%E7%90%86/index.html

https://zhou-yuxin.github.io/

哎,不知道怎麼說呢。自從朱富帥丟下了這個鍋,我就沒有安寧過。大致說一下這個項目吧——一個小車,前面裝了一個攝像頭,當車看到一個二維碼時,就要朝二維碼開過去,而且需要保證最後是正對二維碼中心頂上去。這個需求來自於導師要的自動無線充電的功能。無線充電需要比較高的對準精度,而目前的室內定位技術(基本是基於無線電的)還沒這麼高的精度,因此需要搞出一個基於機器視覺的。這個需求中,先不說路徑規劃的問題,首先要解決的就是基於二維碼的定位問題。

第一步,很顯然,就是通過攝像頭不停讀取圖像,然後識別二維碼,得到二維碼的各種信息。讀攝像頭和識別二維碼在前一篇《OpenCV+Zbar通過攝像頭實時識別二維碼》中已經講得很清楚了。是的,我就是爲了這一篇做鋪墊的。得到了攝像頭的四個頂點在圖片中的座標,應該就能夠通過數學運算計算出我們想要的信息。說白了,就是通過這樣一張圖片中,二維碼的扭曲狀態來計算出二維碼的位姿。

如果你接觸過單目定位技術,那麼應該馬上就想到了,這就是一個PnP(Perspective n Point)問題。解PnP問題的方法很多,不過基本沒有直接解法,都是迭代解法,因爲PnP最終是一個n元二次方程(每個方程都是一個餘弦定理)。不管是實驗觀察還是參考文獻,都會發現PnP方程組對於噪聲是很敏感的,攝像頭和二維碼都保持不動,結果可以看到識別出來的角度不停跳動,有時跳動還很劇烈。箇中五味雜陳,我就不再累述。總之,我需要搞出一種針對當前需求的基於二維碼的定位技術。

通用的PnP解法誤差太大,我就應該多多利用該需求的應用環境的特殊性。如果基於這麼兩個假設:

(1)用作定位的二維碼都規矩地處於一個與地面垂直的平面上;

(2)攝像頭的視線平行於地面。

那麼問題似乎會簡便很多。所謂規矩,就是說二維碼的上下兩條邊水平,而左右兩條邊豎直。其實這個假設基本是成立的。因爲一般二維碼都會貼在牆上,而且底邊與地面平行。OK,有了這個假設,我們就來看一下二維碼的左右兩條邊沿的成像有什麼規律。

以攝像頭的光心爲原點,正前方爲Z軸正方向,正上方爲Y軸正方向,正右方爲X軸正方向,建立空間直角座標系。二維碼左右邊沿就是兩條與Y軸平行的線段。假設攝像頭上下視角爲θ,那麼在Z座標爲z的地方,其視野高度爲

如下圖中所示:

長度爲L的線段(圖中紅色線段)相對於視野高度的比例爲

而該比例應該等於圖片中線段的長度l比上圖片的高度h。即

 

二維碼的邊長L是已知的,圖片的高度h也是已知的。左邊沿和右邊沿在圖片中的長度l1和l2可以通過識別出來的四個頂點座標計算出來,而上下視角θ是個常數,可以通過測量得到。把左邊沿的Z軸座標記作z1,把右邊沿的Z軸座標記作z2,那麼可以得到

 

既然知道了左右邊沿的z座標,那麼二維碼中心點的z座標就是其均值,即

 

好,接下來看俯視圖:

(注意,z是二維碼邊沿到攝像機距離,o爲攝像機位置

左右邊沿z座標之差比上二維碼邊長L,就是sinβ,其中β就是二維碼的法向量與Z軸的夾角。如果規定二維碼偏左(如圖中這樣)時β爲負,否則爲正,那麼有

於是有了二維碼的姿態信息β。

二維碼的中心的z座標z0有了,那麼如果能夠知道二維碼的中心所在鉛垂線與原點構成的平面和Z軸形成的夾角,那麼二維碼的中心所在的鉛垂線就可以唯一確定了。由之前的研究我已經知道,如果能夠得知某個點在圖片上的2D座標,那麼我就能夠得知該點在空間中的方向。那麼如何得知二維碼的中心點在圖片中的座標呢?

一開始我想到的是,在二維碼的四個頂點中取兩個對角點,取均值應該就是中心點的座標。但是!這種想法是錯誤的!因爲當二維碼所在平面不垂直與成像平面時,線段的長度關係就會扭曲。不過,直線經過成像變換依舊是直線。那麼二維碼的兩條對角線的交點就是二維碼的中心點呀!所以思路就是:在圖片上,計算兩條對角線的直線方程,然後求交點的2D座標,然後轉換成空間中的方向!

假設四個頂點點分別爲P0(x0,y0)、P1(x1,y1)、P2(x2,y2)和P3(x3,y3),Zbar保證了它們的分別是圖中的這四個點:

設直線P0P2的方程爲

可以解得

設直線P1P3的方程爲

可以解得

而兩條直線的交點的橫座標爲

 

接下來看成像模型:

其中θ'是水平視角。在空間中,線段MP比上線段MN的比例爲

而在拍攝的圖片上,該比例應該爲

所以有

解得

而水平視角的正切值和上下視角的正切值就是圖像寬和高之比,即

代入上式,解得

 

至此,α、β和z0都已經直接確定了。那麼在上述兩個假設下的二維碼位姿信息就唯一確定了。

 

如果已經得知二維碼自身在世界座標系中的座標和朝向,那麼就可以通過α、β和z0得知小車自身在世界座標系中的座標和朝向。

基於二維碼的室內定位技術(二)——實現

《基於二維碼的室內定位技術(一)——原理》中我已經講解了計算α、β和z0的方法了,這裏我就要實現它。

大致的思路是這樣的:

(1)使用攝像頭獲取一幀;

(2)識別攝像頭中的二維碼;

(3)如果二維碼的內容以“QRLocation,”開頭,則繼續第(4)步,否則返回第(1)步;

(4)識別“QRLocation,”後面的小數,作爲二維碼的邊長

(5)識別二維碼左右兩條邊沿,如果邊沿太傾斜,則返回第(1)步,否則繼續第(6)步;

(6)使用二維碼的四個頂點座標按前文所述的算法計算α、β和z0。

直接上代碼,代碼裏註釋很詳細,而且我的代碼一向很乾淨~

QRLocation.h

#ifndef QRLOCATION_H
#define QRLOCATION_H

/*
二維碼的內容必須符合格式:
QRLocation,<qrSize>
其中<qrSize>是一個實數,表示二維碼邊長
*/

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <zbar.h>

//二維碼傾斜閾值
#define QRLOCATION_INCLINATION_THRESHOLD 0.1
//調試窗口標題
#define QRLOCATION_DEBUGUI_TITLE "debugui"

//二維碼位姿
typedef struct QRPose
{
    //二維碼中心所在鉛垂線與O點構成的平面和Z軸形成的夾角
    double a;
    //二維碼所在平面與X軸構成的夾角
    double b;
    //二維碼中心到XOY平面的距離
    double z;
}
QRPose_t;

//二維碼定位算法
class QRLocation
{

public:
    //初始化,第一個參數爲攝像頭編號,第二個參數爲攝像頭上下視角,第三個參數爲是否開啓調試窗口
    bool init(int webcamIndex,double hViewAngle,bool debugUI);
    //獲取二維碼位姿
    bool getQRPose(QRPose_t* qrPose);
    //銷燬
    bool destroy();

private:
    //攝像頭
    CvCapture* capture;
    //攝像頭上下視角
    double hViewAngle;
    //是否開啓調試窗口
    bool debugUI;
    //灰度圖
    IplImage* grayFrame;
    //圖片掃描器
    zbar::ImageScanner scanner;

private:
    //計算位姿(格式合法性判斷)
    bool getQRPose(zbar::Image::SymbolIterator symbol,QRPose_t* qrPose);
    //計算位姿(算法)
    bool getQRPose(zbar::Image::SymbolIterator symbol,double qrSize,QRPose_t* qrPose);

};

#endif

 

QRLocation.cpp

#include "QRLocation.h"
#include <string.h>
#include <stdio.h>

using namespace std;
using namespace zbar;

bool QRLocation::init(int webcamIndex,double hViewAngle,bool debugUI)
{
    //打開攝像頭
    capture=cvCreateCameraCapture(webcamIndex);
    //攝像頭不存在
    if(!capture)
        return false;
    this->hViewAngle=hViewAngle;
    this->debugUI=debugUI;
    grayFrame=0;
    //配置zbar圖片掃描器
    scanner.set_config(zbar::ZBAR_NONE,zbar::ZBAR_CFG_ENABLE,1);
    //如果開啓調試,則創建窗口,名稱爲“debugui”,自動調整大小
    if(debugUI)
        cvNamedWindow(QRLOCATION_DEBUGUI_TITLE,CV_WINDOW_AUTOSIZE);
}

bool QRLocation::getQRPose(QRPose_t* qrPose)
{
    //從攝像頭中抓取一幀
    IplImage* frame=cvQueryFrame(capture);
    //圖像爲空
    if(!frame)
        return false;
    //如果灰度圖沒有創建,就創建一個和原圖一樣大小的灰度圖(8位色深,單通道)
    if(!grayFrame)
        grayFrame=cvCreateImage(cvGetSize(frame),IPL_DEPTH_8U,1);
    //原圖轉灰度圖
    cvCvtColor(frame,grayFrame,CV_BGR2GRAY);
    //如果開啓調試,則顯示灰度圖
    if(debugUI)
    {
        cvShowImage(QRLOCATION_DEBUGUI_TITLE,grayFrame);
        cvWaitKey(50);
    }
    //創建zbar圖像
    Image image(frame->width,frame->height,"Y800",grayFrame->imageData,frame->width*frame->height);
    //掃描圖像,識別二維碼,獲取個數
    int symbolCount=scanner.scan(image);
    //獲取第一個二維碼
    Image::SymbolIterator symbol=image.symbol_begin();
    //遍歷所有識別出來的二維碼
    while(symbolCount--)
    {
        //能夠識別
        if(getQRPose(symbol,qrPose))
            return true;
        //下一個二維碼
        ++symbol;
    }
    return false;
}

bool QRLocation::getQRPose(Image::SymbolIterator symbol,QRPose_t* qrPose)
{
    //首先得是一個二維碼
    if(symbol->get_type_name()!="QR-Code")
        return false;
    //獲取內容
    char data[128];
    strncpy(data,symbol->get_data().c_str(),sizeof(data)-1);
    data[sizeof(data)-1]=0;
    //內容得是以“QRLocation,”開頭
    if(strncmp(data,"QRLocation,",11)!=0)
        return false;
    //獲取二維碼邊長
    double qrSize=0;
    sscanf(data+11,"%lf",&qrSize);
    if(qrSize==0)
        return false;
    //計算位姿
    return getQRPose(symbol,qrSize,qrPose);
}

bool QRLocation::getQRPose(Image::SymbolIterator symbol,double qrSize,QRPose_t* qrPose)
{
    //獲得四個點的座標
    double x0=symbol->get_location_x(0);
    double y0=symbol->get_location_y(0);
    double x1=symbol->get_location_x(1);
    double y1=symbol->get_location_y(1);
    double x2=symbol->get_location_x(2);
    double y2=symbol->get_location_y(2);
    double x3=symbol->get_location_x(3);
    double y3=symbol->get_location_y(3);
    //左邊沿縱向差
    double leftH=y1-y0;
    //右邊沿縱向差
    double rightH=y2-y3;
    //必須保證0點高於1點,3點高於2點
    if(leftH<0||rightH<0)
        return false;
    //左邊沿橫向差
    double leftW=abs(x0-x1);
    //右邊沿橫向差
    double rightW=abs(x2-x3);
    //不能太傾斜
    if(max(leftW/leftH,rightW/rightH)>QRLOCATION_INCLINATION_THRESHOLD)
        return false;
    //上下視角一半的正切值,因爲一直要用,所以先計算出來
    double tanHalfView=tan(hViewAngle/2);
    double leftLen=sqrt(leftH*leftH+leftW*leftW);
    double rightLen=sqrt(rightH*rightH+rightW*rightW);
    //左邊沿的深度
    double leftZ=grayFrame->height*qrSize/tanHalfView/2/leftLen;
    //右邊沿的深度
    double rightZ=grayFrame->height*qrSize/tanHalfView/2/rightLen;
    //得到中心點的深度
    double z=(leftZ+rightZ)/2;
    //計算b的正弦值
    double sinB=(leftZ-rightZ)/qrSize;
    if(sinB>1)
        return false;
    //得到b
    double b=asin(sinB);
    //兩條對角線的係數和偏移
    double k1=(y2-y0)/(x2-x0);
    double b1=(x2*y0-x0*y2)/(x2-x0);
    double k2=(y3-y1)/(x3-x1);
    double b2=(x3*y1-x1*y3)/(x3-x1);
    //兩條對角線交點的X座標
    double crossX=-(b1-b2)/(k1-k2);
    //計算a的正切值
    double tanA=tanHalfView*(2*crossX-grayFrame->width)/grayFrame->height;
    //得到a
    double a=atan(tanA);
    qrPose->a=a;
    qrPose->b=b;
    qrPose->z=z;
    return true;
}

bool QRLocation::destroy()
{
    //釋放灰度圖
    cvReleaseImage(&grayFrame);
    //銷燬窗口
    cvDestroyWindow(QRLOCATION_DEBUGUI_TITLE);
    //釋放內存
    cvReleaseCapture(&capture);
}

 

測試程序:

QRLocationTest.cpp

#include "QRLocation.h"
#include <stdio.h>

int main()
{
    QRLocation qrLoc;
    if(!qrLoc.init(1,0.60,true))
        return 1;
    QRPose_t pose;
    while(true)
    {
        if(qrLoc.getQRPose(&pose))
        {
            double aInDegree=pose.a*180/3.1415;
            double bInDegree=pose.b*180/3.1415;
            printf("a=%.2lf,b=%.2lf,z=%.2lf\n",aInDegree,bInDegree,pose.z);
        }
    }
}

代碼中使用的攝像頭的索引是1,如果你的電腦只有1個攝像頭,要改爲0。0.60是我的這個攝像頭的上下視角的弧度。不同的攝像頭上下視角不同,需要測量。

Makefile:

all:$(subst src/,obj/,$(subst .cpp,.o,$(wildcard src/*.cpp)))
	g++ $^ -o QRLocationTest `pkg-config opencv --libs --cflags opencv` -lzbar

obj/%.o: src/%.cpp
	g++ -c $^ -o $@

clean:
	rm obj/*

嗯,所有代碼放在src子目錄下,然後創建一個obj子目錄用來存在.o文件。make之後,產生一個可執行文件QRLocationTest。以普通權限就可以運行了。運行後,在攝像頭前面放置一張二維碼,比如下面這張:

如果你的顯示器正常的話,這個二維碼的邊長應該5.7cm,它的內容則是“QRLocation,5.7”。

攝像頭以一定的傾斜角拍攝二維碼,場景如下:

 

可以看到控制檯的輸出:

 

可以看到輸出還是比較準確的~

 

使用Zbar定位、識別二維碼

之前整個項目用的是Java,結果在樹莓派上運行速度很慢。現在要用C++來改寫。Java平臺上我是使用ZXing來識別二維碼的,ZXing是一個純Java實現,所以也就沒法用在C++中。於是我找到了Zbar,它的核心是用C寫的。據說Zbar的性能是ZXing的兩倍以上~

要使用Zbar,首先肯定是要安裝Zbar庫。當然也可以使用源碼安裝,雖然我沒有成功......

sudo apt install libzbar-dev

之後,就可以編寫一個測試代碼:

scan_image.c

#include <stdio.h>
#include <stdlib.h>
#include <png.h>
#include <zbar.h>

#define zbar_fourcc(a, b, c, d)                 \
        ((unsigned long)(a) |                   \
         ((unsigned long)(b) << 8) |            \
         ((unsigned long)(c) << 16) |           \
         ((unsigned long)(d) << 24))

#if !defined(PNG_LIBPNG_VER) || \
    PNG_LIBPNG_VER < 10018 ||   \
    (PNG_LIBPNG_VER > 10200 &&  \
     PNG_LIBPNG_VER < 10209)
  /* Changes to Libpng from version 1.2.42 to 1.4.0 (January 4, 2010)
   * ...
   * 2. m. The function png_set_gray_1_2_4_to_8() was removed. It has been
   *       deprecated since libpng-1.0.18 and 1.2.9, when it was replaced with
   *       png_set_expand_gray_1_2_4_to_8() because the former function also
   *       expanded palette images.
   */
#define png_set_expand_gray_1_2_4_to_8 png_set_gray_1_2_4_to_8
#endif

zbar_image_scanner_t *scanner = NULL;

/* to complete a runnable example, this abbreviated implementation of
 * get_data() will use libpng to read an image file. refer to libpng
 * documentation for details
 */
static void get_data (const char *name,
                      int *width, int *height,
                      void **raw)
{
    FILE *file = fopen(name, "rb");
    if(!file) exit(2);
    png_structp png =
        png_create_read_struct(PNG_LIBPNG_VER_STRING,
                               NULL, NULL, NULL);
    if(!png) exit(3);
    if(setjmp(png_jmpbuf(png))) exit(4);
    png_infop info = png_create_info_struct(png);
    if(!info) exit(5);
    png_init_io(png, file);
    png_read_info(png, info);
    /* configure for 8bpp grayscale input */
    int color = png_get_color_type(png, info);
    int bits = png_get_bit_depth(png, info);
    if(color & PNG_COLOR_TYPE_PALETTE)
        png_set_palette_to_rgb(png);
    if(color == PNG_COLOR_TYPE_GRAY && bits < 8)
        png_set_expand_gray_1_2_4_to_8(png);
    if(bits == 16)
        png_set_strip_16(png);
    if(color & PNG_COLOR_MASK_ALPHA)
        png_set_strip_alpha(png);
    if(color & PNG_COLOR_MASK_COLOR)
        png_set_rgb_to_gray_fixed(png, 1, -1, -1);
    /* allocate image */
    *width = png_get_image_width(png, info);
    *height = png_get_image_height(png, info);
    *raw = malloc(*width * *height);
    png_bytep rows[*height];
    int i;
    for(i = 0; i < *height; i++)
        rows[i] = *raw + (*width * i);
    png_read_image(png, rows);
}

int main (int argc, char **argv)
{
    if(argc < 2) return(1);

    /* create a reader */
    scanner = zbar_image_scanner_create();

    /* configure the reader */
    zbar_image_scanner_set_config(scanner, 0, ZBAR_CFG_ENABLE, 1);

    /* obtain image data */
    int width = 0, height = 0;
    void *raw = NULL;
    get_data(argv[1], &width, &height, &raw);

    /* wrap image data */
    zbar_image_t *image = zbar_image_create();
    zbar_image_set_format(image, zbar_fourcc('Y','8','0','0'));
    zbar_image_set_size(image, width, height);
    zbar_image_set_data(image, raw, width * height, zbar_image_free_data);

    /* scan the image for barcodes */
    int n = zbar_scan_image(scanner, image);

    /* extract results */
    const zbar_symbol_t *symbol = zbar_image_first_symbol(image);
    for(; symbol; symbol = zbar_symbol_next(symbol))
    {
        /* do something useful with results */
        zbar_symbol_type_t typ = zbar_symbol_get_type(symbol);
        const char *data = zbar_symbol_get_data(symbol);
        printf("decoded %s symbol \"%s\"\n",zbar_get_symbol_name(typ), data);
        int pointCount=zbar_symbol_get_loc_size(symbol);
        printf("point count: %d\n",pointCount);
        int i;
        for(i=0;i<pointCount;i++)
        {
            int x=zbar_symbol_get_loc_x(symbol,i);
            int y=zbar_symbol_get_loc_y(symbol,i);
            printf("point%d=(%d,%d)\n",i,x,y);
        }
    }
    
    /* clean up */
    zbar_image_destroy(image);
    zbar_image_scanner_destroy(scanner);

    return(0);
}

 

 

這段代碼是我基於官方給的examples/scan_image.c略做修改得到的。官方的最新代碼中,zbar_fourcc這個宏定義是zbar.h自帶的,可是我通過apt安裝的zbar庫可能版本落後,沒有這個宏定義,所以我就只能複製了一份過來。

我承認一開始看這段代碼還是有點抵觸的,但是研究透徹了以後覺得還是很清晰的。這段代碼獲取命令行的參數作爲一個文件名,然後使用png格式讀取圖片,並轉換成一張灰度圖,然後使用zbar庫來識別圖中的碼(可能是條形碼也可能是二維碼),依次輸出每一個碼的類型、內容和定位點。

gcc scan_image.c -lpng -lzbar -o scan_image

一句命令就可以編譯這段代碼了,然後產生一個scan_image可執行文件。接下來準備一張包含二維碼的png圖片,比如下面這張:

1.png

 

然後運行scan_image:

./scan_image 1.png

可以看到如下輸出:

 

說明zbar成功識別了圖中的二維碼。當然,還可以使用這一張P過的圖片:

3.png

 

然後運行scan_image:

./scan_image 3.png

可以看到如下輸出:

 

如果讀了get_data()的代碼,那麼可以發現它的作用是把一張png圖像轉成了raw數組,而這個raw數組是這麼來的:

void *raw = NULL;

//...

*raw = malloc(*width * *height);

而*width和*height分別是圖像的寬和高。因此,一個字節代表了一個像素,所以我立刻猜測這個字節很可能就是這個像素的灰度值。接下來我做了一個實驗來驗證我的想法。我在

get_data(argv[1], &width, &height, &raw);

之後添加了這麼一段代碼:

printf("size=%dx%d\n",width,height);
int x,y;
for(y=0;y<10;y++)
{
    for(x=0;x<10;x++)
    {
        int pixel=((unsigned char*)raw)[y*width+x];
        printf("%x ",pixel);
    }
    printf("\n");
}

這段代碼先打印圖片的寬和高,然後把最左上角一個10x10的方塊內的值打印出來。接着,我把1.png中(0,0)位置的像素點塗成了#808080,(1,0)位置的像素點塗成了#ababab,(2,0)位置的像素點塗成了#ff0000。如圖:

再次運行,程序輸出結果爲:

 

很明顯,raw數組中保存的確實是灰度矩陣。至於RGB是用何種算法變成灰度的,還需要具體研究一下。不過至此已經能夠知道,只要獲取了一個灰度矩陣,就能用Zbar方便地解析其中的條碼或者二維碼了。



OpenCV+Zbar通過攝像頭實時識別二維碼

在上一篇《使用Zbar定位、識別二維碼》中,我已經能夠通過Zbar從一個灰度矩陣中識別出二維碼(其實還包括條形碼)。這一篇文章要更進一步,通過攝像頭不停拍攝圖片,然後交給Zbar來識別其中的二維碼(如果存在的話)。

在Linux上調用攝像頭拍照,我本來想用的是V4L2。但是V4L2實在太複雜了,一時間搞不定,所以轉而使用OpenCV。本來對於OpenCV還是有點抵觸的,但是自從今天下午用OpenCV寥寥幾行代碼就實現了攝像頭拍攝,我對OpenCV有了新的認識,一下子喜歡多了,大致學完驅動開發就學OpenCV!

=============階段一:OpenCV調用攝像頭============

先上一個OpenCV調用攝像頭實時顯示圖像的程序,非常簡單:

opencv_webcam.cpp

#include <opencv/cv.h>
#include <opencv/highgui.h>

int main()
{
    //打開0號攝像頭,從攝像頭中獲取視頻
    CvCapture *capture=cvCreateCameraCapture(0);
    //攝像頭不存在
    if(!capture)
        return 1;
    //創建窗口,名稱爲“debug”,自動調整大小
    cvNamedWindow("debug",CV_WINDOW_AUTOSIZE);
    while(1)
    {
        //從攝像頭中抓取一幀
        IplImage* frame=cvQueryFrame(capture);
        //在窗口上顯示
        if(frame)
            cvShowImage("debug",frame);
        //延時50ms,如果按了ESC就退出
        if(cvWaitKey(50)==27)
            break;
    }
    //銷燬窗口
    cvDestroyWindow("debug");
    //釋放內存
    cvReleaseCapture(&capture);
    return 0;
}

 

要編譯這段代碼,需要首先安裝OpenCV庫。很簡單:

sudo apt install libopencv-dev

然後就是調用g++編譯即可:

g++ opencv_webcam.cpp -o opencv_webcam `pkg-config opencv --libs --cflags opencv`

見證奇蹟的時刻到了!運行程序:

./opencv_webcam

 

 

非常穩定吧~點擊關閉按鈕是無法把窗口關掉的,直接按下Esc鍵就能退出。

==============階段二:圖像灰度化==============

《使用Zbar定位、識別二維碼》中提到,Zbar接受灰度矩陣作爲輸入數據。那麼接下來就需要把圖像灰度化,然後獲得灰度矩陣。

OpenCV中把圖片轉換爲灰度圖非常簡單,只需要使用函數:

void cvCvtColor(const IplImage* src,IplImage* dst,int code);

比如上面那個例子中,如果要在界面上顯示灰度圖像,可以這樣:

opencv_webcam.cpp

#include <opencv/cv.h>
#include <opencv/highgui.h>

int main()
{
    //打開0號攝像頭,從攝像頭中獲取視頻
    CvCapture *capture=cvCreateCameraCapture(0);
    //攝像頭不存在
    if(!capture)
        return 1;
    //創建窗口,名稱爲“debug”,自動調整大小
    cvNamedWindow("debug",CV_WINDOW_AUTOSIZE);
    //灰度圖
    IplImage* grayFrame=0;
    while(1)
    {
        //從攝像頭中抓取一幀
        IplImage* frame=cvQueryFrame(capture);
        //圖像不爲空
        if(frame)
        {
            //如果灰度圖沒有創建,就創建一個和原圖一樣大小的灰度圖(8位色深,單通道)
            if(!grayFrame)
                grayFrame=cvCreateImage(cvGetSize(frame),IPL_DEPTH_8U,1);
            //原圖轉灰度圖
            cvCvtColor(frame,grayFrame,CV_BGR2GRAY);
            //顯示灰度圖
            cvShowImage("debug",grayFrame);
        }
        //延時50ms,如果按了ESC就退出
        if(cvWaitKey(50)==27)
            break;
    }
    //釋放灰度圖
    cvReleaseImage(&grayFrame);
    //銷燬窗口
    cvDestroyWindow("debug");
    //釋放內存
    cvReleaseCapture(&capture);
    return 0;
}

.運行後顯示的就是灰度圖了:

==============階段三:OpenCV+Zbar==============

只差最後一步——把OpenCV和Zbar銜接在一起了。由於opencv使用的是C++版本,那麼Zbar也用C++版本吧,反倒是看着簡單。很簡單,直接看代碼吧:

opencv_zbar.cpp

#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <zbar.h>
#include <iostream>

using namespace std;
using namespace zbar;

int main()
{
    //打開0號攝像頭,從攝像頭中獲取視頻
    CvCapture *capture=cvCreateCameraCapture(0);
    //攝像頭不存在
    if(!capture)
        return 1;
    //創建窗口,名稱爲“debug”,自動調整大小
    cvNamedWindow("debug",CV_WINDOW_AUTOSIZE);
    //灰度圖
    IplImage* grayFrame=0;
    //創建zbar圖像掃描器
    ImageScanner scanner;
    //配置zbar圖片掃描器
    scanner.set_config(ZBAR_NONE,ZBAR_CFG_ENABLE,1);
    while(1)
    {
        //從攝像頭中抓取一幀
        IplImage* frame=cvQueryFrame(capture);
        //圖像不爲空
        if(frame)
        {
            //如果灰度圖沒有創建,就創建一個和原圖一樣大小的灰度圖(8位色深,單通道)
            if(!grayFrame)
                grayFrame=cvCreateImage(cvGetSize(frame),IPL_DEPTH_8U,1);
            //原圖轉灰度圖
            cvCvtColor(frame,grayFrame,CV_BGR2GRAY);
            //顯示灰度圖
            cvShowImage("debug",grayFrame);
            //創建zbar圖像
            Image image(frame->width,frame->height,"Y800",grayFrame->imageData,frame->width*frame->height);
            //掃描圖像,識別二維碼,獲取個數
            int symbolCount=scanner.scan(image);
            //獲取第一個二維碼
            Image::SymbolIterator symbol=image.symbol_begin();
            //遍歷所有識別出來的二維碼
            while(symbolCount--)
            {
                //輸出二維碼內容
                cout<<"'"<<symbol->get_data()<<"'"<<endl;
                //獲取定位點個數
                int pointCount=symbol->get_location_size();
                //遍歷所有定位點
                for(int i=0;i<pointCount;i++)
                    cout<<'('<<symbol->get_location_x(i)<<','<<symbol->get_location_y(i)<<")"<<endl;
                //下一個二維碼
                ++symbol;
            }
        }
        //延時50ms,如果按了ESC就退出
        if(cvWaitKey(50)==27)
            break;
    }
    //釋放灰度圖
    cvReleaseImage(&grayFrame);
    //銷燬窗口
    cvDestroyWindow("debug");
    //釋放內存
    cvReleaseCapture(&capture);
    return 0;
}

沒有什麼深奧難懂的地方。唯一需要注意的是代碼中用紅色標記出來的地方。Zbar中,Image構造函數的第三個參數爲“Y800”,表明是灰度圖,那麼第四個參數就是一個灰度矩陣。當然,這裏說是矩陣,既可以是一個二維數組,也可以是一個一維數組,只要按照“一行一行”的順序、每一個像素佔用一字節就行了。而恰巧,OpenCV中灰度圖也是這個順序,而且當圖片是灰度圖時,imageData字段就是這麼一個灰度數組。所以直接代入即可。

使用如下命令編譯:

g++ opencv_zbar.cpp -o opencv_zbar `pkg-config opencv --libs --cflags opencv` -lzbar

然後運行:

./opencv_zbar

程序運行後,如果在攝像頭前擺放一個能夠識別的二維碼,那麼就會不斷輸出二維碼的內容和四個頂點的座標:



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