2017年12月18日 星期一

OpenQCam樹莓派開源迷你相機之ePTZ之應用實例

本專案是利用上次已完成之OpenQCam樹莓派迷你開源相機(https://omnixri.blogspot.tw/2017/12/openqcam.html)(如圖一右圖所示)所衍生的應用實例【ePTZ攝影機】,一樣也會使用到OpenCV,希望這個專案能提供大家多一點使用上的想法。完整專案已上傳GITHUB請參考https://github.com/OmniXRI/OpenQCam_ePTZ

傳統監視攝影機解析度通常不高,大約30萬畫像素(VGA)至二百萬畫像素(FHD)左右,若要監看較大範圍區域或特定一個小區域時常須搭配致動機構來使攝影機可以左右移動(PAN)、上下移動(Tilt)及調整光學倍率縮放(Zoom),一般稱為PTZ攝影機(如圖1左圖所示)。這類攝影機由於須要搭配致動機構(馬達、齒輪等)及控制迴路,所以通常價格也會高出一般固定式攝影機許多。隨著攝影機取像晶片的快速進步,通常解析度已高出顯示器許多。以常見FHD(2K)的顯示幕來說不過約二百萬畫像素(1920*1080),而4K顯示器雖然已可顯示八百萬畫像素,但高端攝影機早已高達五千萬甚至一億畫像素,而高過顯示器的解析度明顯就變成有些浪費。因此有許多廠商提出只需一隻固定式定焦(倍率不變)的高解析度攝影機,不須任何額外致動機構及元作,只要在高解析度影像中截剪(Crop)出顯示器所需解析度內容來顯示,即可取代部份PTZ攝影機使用場合,這樣的技術就稱為電子式或數位式PTZ,簡稱ePTZ。



圖1 左圖為傳統PTZ攝影機,右圖為利用OpenQCam所完成的ePTZ攝影機。

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


圖2 左圖為攝影機原始解析度及局部截切後解析度,右圖為LCD顯示解析度。

在工作前我們要先規畫巡行方式及速度,模仿機械式轉動的效果。以下列公式可求得巡行一趟的時間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)及步階移動間隔時間等參數選定時須仔細考慮,才不會造成畫面顯示時有嚴重跳動感。巡行時會由左上角開始,接著由左而右,由上而下,最後再回左上角,持續巡行,其流程如圖2左圖所示。



圖3 左圖為ePTZ巡行速度計算參數示意圖,右圖為巡行工作流程圖

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

表1 不同巡行尺度參數


CW
CH
EW
EH
SW
SH
總移動
Full
2592
1944
2592
1944
0
1
0
1
1
QVGA
2592
1944
320
240
71
32
113
8
256
VGA
2592
1944
640
480
122
16
122
12
192
HD
2592
1944
1280
960
82
16
123
8
128


舉例來說,以表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 ( https://omnixri.blogspot.tw/2017/12/openqcam.html )說明。本專案一樣使用到OpenCV作為影像處理用工具,其中應用到一個最重要的概念【感興趣區域(ROI)】,主要用途就是從一張影像中裁剪出一小塊影像或者反過來拿一小塊影像貼到指定位置。取出影像後再縮小至顯示尺寸(320*240),即可顯示於到LCD上,其主要程式如下說明。另外會將取得之全畫面縮小至120*90後貼到左上角。

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);

// 將原始影像縮小至120*90畫像素後貼至顯示區左上角
Mat matDispROI = matDisp(Rect(0, 0, 120, 90));
cv::Resize(matCap, matCapResize, cv::Size(120,90), 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及做為熟悉本平台軟硬體架構很好的範例,程式的相關註解詳見各程式原始碼。受限於個人能力有限,撰文上難免產生誤解或疏漏,如有任何問題歡迎留言或來信指教!

作者:歐尼克斯實境互動工作室 https://omnixri.blogspot.tw/ (Dec. 2017)

沒有留言:

張貼留言

【頂置】簡報、源碼、系列文快速連結區

常有人反應用手機瀏覽本部落格時常要捲很多頁才能找到系列發文、開源專案、課程及活動簡報,為了方便大家快速查詢,特整理連結如下,敬請參考! Edge AI Taiwan 邊緣智能交流區 全像顯示與互動交流區 台科大(NTUST) 人工智慧與邊緣運算實務 開南大學...