文章類型
開發平台
解決方案
關注主題
文章類型
開發平台
解決方案
關注主題

【Tutorial】OpenQCam 在ePTZ攝影機的應用

作者:Jack Hsu

這次的文章是利用上次 OpenQCam 樹莓派迷你開源相機(下圖右方所示)所衍生的應用實例【ePTZ 攝影機】,一樣也會使用到 OpenCV,希望能提供大家多一點想法。

通常傳統監視攝影機解析度並不高,大約 30 萬畫像素(VGA)至二百萬畫像素(FHD)左右。若要監看較大範圍或特定小區域,會需要用到 PTZ 攝影機,由於攝影機要搭配致動機構(馬達、齒輪等)來使攝影機可以左右移動(PAN)、上下移動(Tilt)及調整光學倍率縮放(Zoom),通常價格也會高出一般固定式攝影機許多。

隨著攝影機取像晶片的進步,解析度已高出顯示器許多。常見 FHD(2K)約二百萬畫像素(1920*1080); 4K 顯示器大約是八百萬畫像素;高端攝影機可以高達五千萬甚至一億畫像素,但高過顯示器的解析度明顯就變成有些浪費。因此,許多廠商提出只需一隻固定式定焦(倍率不變)的高解析度攝影機, 不須任何額外致動機構及元作,在高解析度影像中截剪(Crop)出顯示器所需 內容來顯示,即可取代部份 PTZ 攝影機使用場合,而這樣的技術就稱為電子式或數位式 PTZ,簡稱 ePTZ。

左圖為傳統 PTZ 攝影機,右圖為利用 OpenQCam 所完成的 ePTZ 攝影機。(圖片來源:Jack Hsu提供)

在上一專案中,我們使用了一個五百萬畫像素(2592*1944)的攝像頭模組和一片 QVGA(320*240)的 LCD 顯示屏,剛好很適合應用在 ePTZ 上。如下圖所示,取像時為 2592*1944 畫像素,我們可選擇性的截切 一小塊再丟給 LCD 顯示;當截切內容大於 LCD 顯示解析度時, 則先把影像縮小至 320*240 畫像素再顯示即可。

左圖為攝影機原始解析度及局部截切後解析度,右圖為 LCD 顯示解析度。(圖片來源:Jack Hsu提供)

在工作前我們要先規畫巡行方式及速度,模仿機械式轉動的效果。以下列公式可求得巡行一趟的時間 T。

T=((CW–EW)/SW)*((CH-EH)/SH)*ST (公式一)

CW: 取像水平解析度, CH: 取像垂直解析度。

EW: 影像截切水平解析度, EH: 影像截切垂直解析度。

SW: 水平移動步階, SH: 垂直移動步階。

ST: 為步階移動間隔時間,可自由指定,通常希望以 1 個影格(Frame)時間 1/30 秒為間隔,但實際上會因 Pi Zero 處理速度來不及可能須降至 1/15~1/5 秒。

為了使巡行速度能滿足週期內完成一輪且畫面顯示平順,因此影像水平截切尺寸 (EW, EH)、移動距離(SH, SW)及步階移動間隔時間等參數選定時須仔細考慮,才不會造成畫面顯示時有嚴重跳動感。巡行時會由左上角開始,接著由左而右, 由上而下,最後再回左上角,持續巡行。

左圖為 ePTZ 巡行速度計算參數示意圖,右圖為巡行工作流程圖(圖片來源:Jack Hsu提供)

我們簡單設計幾個巡行尺度及速度(如表 1)。第一種尺度為全視野顯示,即不巡行,暫停一秒;第二至四種為 ePTZ(裁切)顯示,顯示時除顯示目前裁切及巡行內容,會左上角顯示全視野影像(80*60),方便了解目前巡行位置。一般橫向移動不宜幅度過大避免產生跳動感,而縱向移動大家比較無感,可以移動較大行程,節省整體巡行時間。此外,大家也可依自己想要的裁切尺寸和速度來調整, 但設計上儘量讓數字能整除會較方便估計巡行時間。

表 1 不同巡行尺度參數(來源:Jack Hsu提供)

舉例來說,以表 1 中取像後裁切出 VGA 解析度影像,縮小至 QVGA 解析度後再 顯示為例,假設處理一次步階移動時間(ST)為 60ms,所需巡行一趟時間 T 如下所 示。

T = ((CW – EW) / SW) * ((CH-EH) / SH) * ST
= ((2592 – 640) / 122) * ((1994 – 480) / 122) * 60ms = 16 * 12 * 60ms = 11.520 sec

再來,僅針對主程式部份進行說明。其它部份請參見前一專案 OpenQCam 說明。本專案一樣使用到 OpenCV 作為影像處理用工具,其中應用到一個最重要的概念【感興趣區域(ROI)】, 主要用途就是從一張影像中裁剪出一小塊影像或者反過來拿一小塊影像貼到指 定位置。取出影像後再縮小至顯示尺寸(320*240),即可顯示於到 LCD 上。另外會將取得之全畫面縮小至 80*60 後貼到左上角。

Mat matCap;// 儲存原始影像

cap >> matCap;// 從攝像頭取得影像並存到 matCap 中

//取得原始影像 matSrc 中指定位置及大小(Rect(左上 X 座標,左上 Y 座標,寬度, 高度))的影像到 matROI
Mat matROI = matSrc(Rect(begin_x, begin_y, width, height));

// 宣告 LCD 顯示及原始影像縮小圖用矩陣

Mat matDisp;

Mat matCapResize;

// 將取得之 ROI 影像縮小至 320*240 畫像素並放到 matDisp 中
// 一般縮放採雙線性內差(CV_INTER_LINEAR)就夠用,如果不在乎影像品質將縮 放速度加快一點,可採用 CV_INTER_NEAREST 內插法。
cv::Resize(matROI, matDisp, cv::Size(320, 240), 0, 0, CV_INTER_LINEAR);

// 將原始影像縮小至 80*60 畫像素後貼至顯示區左上角

Mat matDispROI = matDisp(Rect(0, 0, 80, 60));

cv::Resize(matCap, matCapResize, cv::Size(80,60), 0, 0, CV_INTER_NEAREST);

matCapResize.CopyTo(matDispROI);

延伸閱讀 — 完整程式

main.cpp

#include <stdio.h> #include <iostream> #include “ILI9341.h” #include <opencv2/opencv.hpp> #include <ctime>

#define MAX_W 2592

#define MAX_H 1944

using namespace std; using namespace cv;

int main(int argc, char **argv) {

// 巡行參數 0:原始解析度 / 1:QVGA / 2:VGA / 3:HD int para_num = 0; // 巡行參數編號 int EW[4] = {2592, 320, 640, 1280}; // 裁切影像寬度

// 垂直移動距離 int EH[4] = {1944, 240, 480, 960}; int SW[4] = {2592, 71,122,82}; int SH[4] = {1994, 113, 122, 123}; int curr_x = 0; // 目前巡行起點 X int curr_y = 0; // 目前巡行起點 Y int curr_ew = EW[0]; // 目前裁切影像寬度 int curr_eh = EH[0]; // 目前裁切影像高度 int curr_sx = SW[0]; // 目前水平移動距離 int curr_sh = SH[0]; // 目前垂直移動距離 // 裁切影像高度 // 水平移動距離 double t0,t1,t2; int result = LCD_Inital(); // LCD 初始化

switch(result){ // 顯示初始化錯誤訊息 case 0:

printf(“LCD inital OK.\n”);

break; case 1:

printf(“BCM2835 inital failed. Are you running as root??\n”);

break; case 2:

printf(“GPIO inital failed. Are you running as root??\n”);

break; case 3:

printf(“SPI inital failed. Are you running as root??\n”);

break; case 4:

printf(“ILI9341 inital failed. Please check PCB connection is OK.\n”);

break; default:

printf(“LCD inital failed.\n”);

}

if(result != 0) return 0;

VideoCapture cap(0); // 啟動攝像頭連續取像

if (!cap.isOpened()) { // 若無法開啟則結束 cerr << “ERROR: Unable to open the camera” << endl; return 0;

}

cap.set(CV_CAP_PROP_FRAME_WIDTH, 2592); // 設定攝像頭輸入為最大解析度 cap.set(CV_CAP_PROP_FRAME_HEIGHT,1944);

Mat matCap; Mat matCapResize; Mat matROI; Mat matDisp; int count = 0;

cout << “Start grabbing !” << endl; LCD_SetHorizontalDisplay(); // 設定 LCD 為橫式顯示

char strFps[10]; // 儲存速度字串 t1 = (double)getTickCount(); // 取得目前時間

while(1) { for(curr_y=0; curr_y<MAX_H-EH[para_num]; curr_y += SH[para_num]){

for(curr_x=0; curr_x<MAX_W-EW[para_num]; curr_x += SW[para_num]){ t0 = t1; // 儲存舊時間 t1 = (double)getTickCount(); // 儲存目前時間

cap >> matCap; // 將取得的影像複製到 matCap

if(para_num == 0){ resize(matCap, matDisp, cv::Size(320,240), 0, 0, INTER_LINEAR); // 將影

像縮至 QVGA 解析度到顯示區 }

else{ matROI = matCap(Rect(curr_x,curr_y,curr_ew,curr_eh)); // 裁切指定位

置大小影像 resize(matROI, matDisp, cv::Size(320,240), 0, 0, INTER_LINEAR); // 將影

像縮至 QVGA 解析度到顯示區 }

Mat matDispROI = matDisp(Rect(0, 0, 120, 90)); // 指定 ROI 區

rectangle(matCap, cv::Point(curr_x, curr_y), cv::Point(curr_x+EW[para_num],curr_y+EH[para_num]),

Scalar(0,0,255), 20); // 在原影像上畫 ROI 框 resize(matCap, matCapResize, cv::Size(120,90), 0, 0, INTER_NEAREST); //

將原始影像縮至 120*90 matCapResize.copyTo(matDispROI); // 貼至顯示區左上角

// 計算兩次執行時間差 取倒數即為每秒幀數 顯示在 LCD 左上角 // 若不需顯示則註解掉下面兩行 sprintf(strFps,”%2.1f FPS”, 1.0 / ((t1-t0) / getTickFrequency())); putText(matDisp, strFps, Point(200,20), FONT_HERSHEY_DUPLEX, 0.8,

Scalar(0,255,0),1);

// 將影像逐行顯示在 LCD 上 for(int i = 0; i < matDisp.rows; i++){

char *ptrS = matDisp.ptr<char>(i);

ILI9341_WriteLineBGR2RGB565(ptrS, matDisp.cols); }

// 若無法取得影像則結束程式 if (matCap.empty()) {

cerr << “ERROR: Unable to grab from the camera” << endl;

return 0;

}

count ++;

if(count%2 == 0) // 工作中 LED1 閃爍 bcm2835_gpio_write(PIN_GPIO_LED_R, 1); // 點亮 LED1

else bcm2835_gpio_write(PIN_GPIO_LED_R, 0); // 熄減 LED1

// 若 SW1 按下則切換到下一組巡行參數 if(bcm2835_gpio_lev(PIN_GPIO_SW1) != 0){

delay(10); // 去除按鍵彈跳 curr_x = MAX_W; // 強迫離開雙 for loop curr_y = MAX_H; //

}

// 若 SW2 按下則綠燈閃一下結束程式 if(bcm2835_gpio_lev(PIN_GPIO_SW2) != 0){

bcm2835_gpio_write(PIN_GPIO_LED_G, 1); // 點亮 LED2 delay(50); bcm2835_gpio_write(PIN_GPIO_LED_G, 0); // 熄減 LED2 bcm2835_gpio_write(PIN_GPIO_LED_R, 0); // 熄減 LED1 cap.release();

BCM2835_End(); cout << “Done!” <<endl; return 0;

}

if(curr_x >= MAX_W) break;

} // end of for loop curr_x

if(curr_y >= MAX_H) break;

} // end of for loop curr_y para_num ++;

if(para_num >= 4) para_num = 0;

curr_ew = EW[para_num]; // 目前裁切影像寬度 curr_eh = EH[para_num]; // 目前裁切影像高度 curr_sx = SW[para_num]; // 目前水平移動距離 curr_sh = SH[para_num]; // 目前垂直移動距離

} // end of while

}

接著執行本專案已預建編譯指令批次檔 go.sh 進行編譯,等待約30 秒後即可完成,產生執行檔ePTZ,由於BCM2835 須要較高權限,所以執行時要加上 sudo, 完整操作如下所示。

sudo ./go.sh (編譯程式)
sudo ./ePTZ (執行程式)

執行後,會依序執行上述表 1 中四種尺度巡行參數,當按下 SW1 時會直接跳到下一 組參數,每按一次則依序循環切換巡行參數,巡行時 LED1(紅)閃爍表示工作中, 當按下 SW2 時可結束程式。經實測在未進行程式優化下,執行速度大約有 4.0~4.5 FPS,相當於移動一個步階大約 200~220ms。

本專案是延續前一專案OpenQCam及做為熟悉本平台軟硬體架構很好的範例,程式的相關註解詳見各程式原始碼。受限於個人能力有限,撰文上難免產生誤解或疏漏,如有任何問題歡迎留言或來信指教!

(本文同步發表於歐尼克斯實境互動工作室(OmniXRI)文章連結;責任編輯:葉于甄。)

許 哲豪

許 哲豪

工作經驗超過二十年,主要專長機電整合、電腦視覺、人機互動、人工智慧、專利分析及新創輔導。曾任機電整合工程師、機器視覺研發副理、技轉中心商業發展經理。目前擔任多家公司兼任技術顧問並積極推廣實境互動相關技術。
許 哲豪

上一篇: | 下一篇:

468 ad

我想回應

你的電子郵件位址並不會被公開。 必要欄位標記為 *

成城共創股份有限公司版權所有、轉載必究.Copyright(c) 2017 MakerPRO