[OPENCV] TrackBar and Mat

這次要介紹Trackbar的使用方式, 以及連帶的說明一下 Mat
先把完成品貼上來給心急的人看一下



相對應的程式碼是


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <opencv2/opencv.hpp>
#include <stdio.h>
using namespace cv;

#define WINDOW_NAME "Alpha"
const int slider_max = 100;

static void on_trackbar_alpha(int pos, void* userdata) {
    Mat* image = (Mat*) userdata;
    Mat new_image = image->clone();
    float alpha = (float) pos / slider_max;
    for (int y = 0; y < image->rows; y++) {
        for (int x = 0; x < image->cols; x++) {
            for (int c = 0; c < 3; c++) {
                new_image.at<Vec3b>(y, x)[c] = saturate_cast<uchar>(
                        (alpha * image->at<Vec3b>(y, x)[c]) * x / 512);
            }
        }
    }
    imshow(WINDOW_NAME, new_image);
}

int main_(int argc, char** argv) {
    Mat image = imread( argv[1] );
    namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE); // Create Window
    int init_alpha = 100;
    char TrackbarName[50];
    sprintf(TrackbarName, "Alpha Max %d", slider_max);
    createTrackbar(TrackbarName, WINDOW_NAME, &init_alpha,
            slider_max, on_trackbar_alpha, &image);
    imshow(WINDOW_NAME, image);
    waitKey(0);
    return 0;
}
如果還不熟悉怎麼load進 lena姐的, 請參照這篇基本影像操作

別被這密密麻麻的字嚇到, 讓喵來慢慢解說

Trackbar


首先我們將圖load進來, 然後創造一個window, 之後我們會使用 imshow來將兩個綁在一起

int main(int argc, char** argv) {
    Mat image = imread( argv[1] );
    imshow(WINDOW_NAME, image);
    ...
    waitKey(0);
    return 0;
}
到這邊都還是基本影像操作的內容

接著中間放進去的code, 就是 trackbar相關的了
使用 createTrackbar來創造出一個trackbar, 並且要放入相對應的參數

createTrackbar的原始宣告如下

CV_EXPORTS int createTrackbar(const String& trackbarname, const String& winname,
                              int* value, int count,
                              TrackbarCallback onChange = 0,
                              void* userdata = 0);

因此我們按照需求的填入
  1. trackbarname: Trackbar名稱
  2. winname: 視窗名稱
  3. value: 這邊value可以填0, 可以作為slider position初始值使用
  4. onChange: callback function
  5. userdata: userdata and value 都會在callback function當中傳入
當中需要說明一下的是callback function, 以下是他的原型

typedef void (*TrackbarCallback)(int pos, void* userdata);

因此需要"製作"這一個callback function
喵寫了一個如下

static void on_trackbar_alpha(int pos, void* userdata) {
    Mat* image = (Mat*) userdata;
    Mat new_image = image->clone();
    float alpha = (float) pos / slider_max;
    for (int y = 0; y < image->rows; y++) {
        for (int x = 0; x < image->cols; x++) {
            for (int c = 0; c < 3; c++) {
                new_image.at<Vec3b>(y, x)[c] = saturate_cast<uchar>(
                        (alpha * image->at<Vec3b>(y, x)[c]) * x / 512);
            }
        }
    }
    imshow(WINDOW_NAME, new_image);
}

這邊開始就會跟Mat這物件打交道了

Mat

在一開始看到的畫面, 雖然是應用上了trackbar, 但是呈現出的效果卻是跟Mat這物件有比較大的相關

這段code包含了兩個部分
  1. 調整 alhpa
  2. 根據 x position, 逐漸調亮
先說第一個部分
這段code大致上是

            new_image.at<Vec3b>(y, x)[c] = saturate_cast<uchar>(
                    (alpha * image->at<Vec3b>(y, x)[c]));

先把 x position的部分去除掉了

這邊有幾個特別東西要介紹

<Vec3b>

這關於 C++語言的 template在這邊不多加描述, 不過這用法可以看一下

typedef Vec<uchar, 3> Vec3b;

因此可以知道Vec3b 是 Vec<uchar,3>
接著看一下 new_image.at<Vec3b>

template<typename _Tp> inline
_Tp& Mat::at(int i0, int i1)
{
    CV_DbgAssert(dims <= 2);
    CV_DbgAssert(data);
    CV_DbgAssert((unsigned)i0 < (unsigned)size.p[0]);
    CV_DbgAssert((unsigned)(i1 * DataType<_Tp>::channels) < (unsigned)(size.p[1] * channels()));
    CV_DbgAssert(CV_ELEM_SIZE1(DataType<_Tp>::depth) == elemSize1());
    return ((_Tp*)(data + step.p[0] * i0))[i1];
}

因此這個式子 new_image.at<Vec3b>(y, x) 
會return Vec3b的物件
接著再搭配上 [c]
這變成是去看 Vec3b[c] --> Vector<uchar,3>[c]

根據定義這個在 matx.hpp當中

template<typename _Tp, int cn> inline
const _Tp& Vec<_Tp, cn>::operator [](int i) const
{
    CV_DbgAssert( (unsigned)i < (unsigned)cn );
    return this->val[i];
}

_Tp的類型是 uchar, 另外 val的定義是

_Tp val[m*n]; //< matrix elements

此時的 m = 3, n = 1, 所以上述式子相當於

uchar val[3*1]; //< matrix elements

至於這個 n=1的由來則是因為 Vec這物件本身是繼承 Matx, 並且維度為1

template<typename _Tp, int cn> class Vec : public Matx<_Tp, cn, 1>

根據這些定義, 我們可以理解到Matx在儲存 2D圖型pixel的方式是三個pixel放在一起
而他的排列方式是
val[0]: B
val[1]: G
val[2]: R

saturate_cast<uchar>

這本身沒什麼好多解釋的, 簡而言之就是把數值侷限到 uchar的範圍內

更深入探討

理解完上述這些事項之後
還有些事情是沒講述到的

new_image.at<Vec3b>(y, x)
這裡有發現到 y放在前面, 而x放到後面
另外在之前的code當中有看到這一段
return ((_Tp*)(data + step.p[0] * i0))[i1];
裡面多出了 step.p[0]這個東西

為了更多理解這個內容
我們把 setp.p[0]印出來之後會發現, 數值為 1536
這剛好是 512 * 3 = 1536
而 lena.png 是一個 512 x 512 pixel的圖案

因此就剛好回應 BGR 3個channel 乘上 一行有512個pixel
而一開始的程式碼, 喵把 x的位置加入做變化就是要特地去觀察
y放在前面, 而x放到後面的配置是否真的是正確 (正確!喵~)

另外這取值的方式, 有發現到 Vec3b取值的方法就像是直接在memory上面只分到3個 uchar
return ((_Tp*)(data + step.p[0] * i0))[i1];
記得這 _Tp其實是 Vec3b

這也是Matx的一個巧思, 在Matx的定義當中, 大多數都是方法, 只有一個變量
那就是 _Tp val[m * n]

可以做個實驗去印出 Vec3b本身的address以及Vec3b的大小
這結果會讓人很吃驚


    Vec3b test = Vec3b(1,2);
    printf("test addr 0x%x, 0x%x, size %d\n", &test, &test[0], sizeof(Vec3b));

結果:

test addr 0x55ae1390, 0x55ae1390, size 3

是不是開始有點佩服寫openCV的這些高手們~ 喵

這次對於code的闡述多了些
但希望每一個關節多闡述一點也可以讓大家一起欣賞這程式的美~
喵~

沒有留言:

張貼留言

OpenGL 閱讀筆記 (二) OpenGL基本操作

這邊虎喵跳過glfw/glew的初始化, 先來提一下OpenGL的基本操作方式 前面也提到過, OpenGL是一個類C的語言, 因此使用C/C++的攻城獅們應該會感到很熟悉. OpenGL的基本動作循環如下: 每一行code的解釋如下: // 本地變數,...