<学习opencv>跨平台和本机windows
阅读原文时间:2023年07月13日阅读:1

/*=========================================================================*/
// 跨平台和本机Windows
/*=========================================================================*/

HighGUI原生图形界面

HighGUI用户输入工具仅仅支持三种基本交互-特别是按键,图形区域上的鼠标点击以及简单轨道栏的使用。
这些基本功能通常足以进行简单的模型和调试,单对于面向最终用户的应用程序来说并不理想。
为此,你将想要使用基于Qt的界面或者其他一些功能更全面的UI工具包。

本机工具的主要优点是:它们快速且易于使用,并且不需要你安装任何其他的库。

使用cv::namedWindow()创建一个窗口
首先,我们希望能够创建一个窗口和节目一个图像在屏幕上使用HighGUI。
为我们完成第一部分的功能是cv::namedWindow(),它需要新窗口的名称和一个标志。
该名称显示在窗口的顶部,并且还用作可以传递给其他HighGUI函数的窗口的句柄。
该参数指示窗口是否应自动调整自身以适应我们把它的图像。这是完整的原型:

int cv::namedWindow(
const string&name , //用于标识窗口的句柄
int flags = 0 //用于告诉窗口自动调整大小
)

目前,唯一可用的有效选项flags 是将其设置为0 (默认值),
这表示用户可以调整窗口大小或将其设置为cv::WINDOW_AUTOSIZE 。
如果设置cv::WINDOW_AUTOSIZE,则HighGUI会在加载新图像时自动调整窗口大小以适应,但用户无法调整窗口大小。

一旦我们创建了一个窗口,我们通常会想要在其中放置一些东西。
但在我们这样做之前,让我们看看如何在不需要时摆脱窗口,
为此,我们需要使用cv::destroyWindows()一个函数,它的唯一参数是一个字符串:创建窗口时给出的名称。

int cv::destroyWindows(
const string&name //用于标识窗口的句柄
)

使用cv::imshow()绘制图像
加载一个图像并把它放在我们可以查看它的窗口。

void cv::imshow(
const string&name , //用于表示窗口的句柄
cv::InputArray image //要在窗口中显示的图像
)

更新窗口和cv::waitKey()
函数cv::waitKey()是等待对于一些键盘上按键的指定(可能是无限期)时间量,并在接收时返回该键值。
cv::waitKey()从任何打开的opencv窗口接受按键(但如果不存在这样的窗口,则不会起作用)。

int cv::waitKey(){
int delay = 0 //放弃之前的毫秒数(0='c从不')
};
cv::waitKey() 采用单个参数,delay 这是在自动返回之前等待按键的时间量(以毫秒为单位)。
如果延迟设置为0 ,cv::waitKey() 则会无限期地等待按键。
如果在delay 毫秒过去之前没有按键,cv::waitKey() 则返回–1 。
还有一个不太明显的功能cv::waitKey() ,即为任何开放的OpenCV窗口提供更新机会。
这意味着,如果您不打电话cv::waitKey() ,您的图像可能永远不会在您的窗口中绘制,
或者您的窗口在移动,调整大小或未被覆盖时可能会表现得异常(并且非常糟糕)

示例| 创建一个窗口并在窗口中显示图像
————————————————————————————————————————————————————————————————
int main(int argc, char **argv) {

cv::namedWindow(argv\[1\],1) ;  
cv::Mat = cv::imread(argv\[1\]) ;  
cv::imshow(argv\[1\],img) ;

while(true) {  
    if (cv::waitKey(100 / \*milliseconds \*/)==27) break ;  
}  
cv::destroyWindows(argv\[1\]) ;

exit(0) ;  

}

为了方便期间,我们使用文件名作为窗口名称。

其他与窗口相关的函数:
void cv::moveWindow(const char* name, int x,int y) ;
简单的移动屏幕上的一个窗口,使其左上角位于像素位置:x,y

void cv::destroyAllWindows(void) ;
作为一个有用的清理函数,它关闭所有窗口并释放相关内存。

int cv::startWindowThread(void) ;
尝试启动一个自动更新窗口的线程,并处理调整大小等等。返回值0表示不能启动任何线程。

鼠标事件:
与键盘事件不同,鼠标时间由更传统的回调机制处理。
这意味着,要启用对鼠标单击的相应,我们必须首先编写一个回调例程,opencv可以在鼠标事件发生时调用该例程。
完成后,我们必须使用opencv注册回调,从而告知opencv,只要用户在特定窗口上执行鼠标操作,这就是正确的操作函数。

回调可以是任何采用正确参数集并返回正确类型的函数。
我们必须能够告诉函数,被用作回调究竟发生了什么类型的事件以及它发生的位置。
如果用户在鼠标事件发生时按下shift或Alt等键,也必须告知该功能。
指向这样一个函数的指针被称为cv::MouseCallback。

以下是回调函数必须匹配的确切原型。
void your_mouse_callback(
int event , //事件类型

int x,  //x-mouse 事件的位置  
int y,  //y-mouse 事件的位置  
int flags  
void\* param  //来自cv::setMouseCallback()参数

表9-1 鼠标事件类型
————————————————————————————————————————————————————————————————
事件 | 数值
————————————————————————————————————————————————————————————————
cv::EVENT_MOUSEMOVE | 0
————————————————————————————————————————————————————————————————
cv::EVENT_LBUTTONDOWN | 1
————————————————————————————————————————————————————————————————
cv::EVENT_RBUTTONDOWN | 2
————————————————————————————————————————————————————————————————
cv::EVENT_MBUTTONDOWN | 3
————————————————————————————————————————————————————————————————
cv::EVENT_LBUTTONUP | 4
————————————————————————————————————————————————————————————————
cv::EVENT_RBUTTONUP | 5
————————————————————————————————————————————————————————————————
cv::EVENT_MBUTTONUP | 6
————————————————————————————————————————————————————————————————
cv::EVENT_LBUTTONDBLCLK | 7
————————————————————————————————————————————————————————————————
cv::EVENT_RBUTTONDBLCLK | 8
————————————————————————————————————————————————————————————————
cv::EVENT_MBUTTONDBLCLK | 9
————————————————————————————————————————————————————————————————

第二个和第三个参数将设置x坐标和y坐标事件。

第四个参数的flags是一个位字段,其中各个位表示事件发生时存在的特殊条件。
表9-2 鼠标事件标志
————————————————————————————————————————————————————————————————
flags | 数值
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_LBUTTON | 1
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_RBUTTON | 2
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_MBUTTON | 4
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_CTRLKEY | 8
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_SHIFTKEY | 16
————————————————————————————————————————————————————————————————
cv::EVENT_FLAG_ALTKEY | 32
————————————————————————————————————————————————————————————————

最后一个参数是一个void指针,可用于让opencv以指针的形式向您需要的任何类型的结构传递附加信息。

接下来,我们需要注册一个回调的函数。该函数被调用cv::setMouseCallback,它需要三个参数。

void cv::setMouseCallback(
const string& windowName , //用于标识窗口的句柄
cv::MouseCallback on_mouse , //回调函数
void * param = NULL //回调fn的附加参数
) ;

第一个参数是回调将附加到的窗口的名称;只有该特定窗口中的事件才会触发此回调。
第二个参数是你的回调函数。
第三个参数param允许我们指定在param执行时应该给回调的信息。

例 | 使用鼠标在屏幕上绘制框的程序
————————————————————————————————————————————————————————————————
#include
//定义我们将安装的回调函数

//鼠标事件
void my_mouse_callback()
{
int event, int x, int y, int flags, void* param
};

Rect box ;
bool drawing_box = false ;

void drawing_box(cv::Mat& img, cv::Rect box) {
cv::rectangle (
img,
box.tl() ,
box.br() ,
cv::Scalar(0x00,0x00,0xff) /* red */
);
}

void help() {
std::cout << "Call: ./ch4_ex4_1\n" <<
" shows how to use a mouse to draw regions in an image" << std::endl ;
}

int main()
{
help() ;

box = cv::Rect(-1,-1,0,0) ;  
cv::Mat image(200,200,CV\_8UC3),temp ;  
image.copyTo(temp) ;

box = cv::Rect(-1,-1,0,0) ;  
image = cv::Scalar::all(0) ;

cv::namedWindow("Box Example") ;

cv::setMouseCallback(  
    "Box Example",  
    my\_mouse\_callback,  
    (void\*)&image  
);

//主程序循环,这里我们复制工作图像。  
//对于临时图像,如果有用户正在绘图。  
//那么将当前设想的框放到该临时图像上  
for (;;)  
{  
    image.copyTo(temp) ;  
    if(drawing\_box) draw\_box(temp, box) ;  
    cv::imshow("Box Example", temp) ;  
    //显示临时图像,等待15ms进行按键 然后重复  
    if (cv::waitKey(15) == 27) break ;  
}  
return 0 ;  

}

//按下左键,开始一个方框
//当用户释放该按键时,我们将框添加到当前图像。

void my_mouse_callback(int event, int x, int y, int flags, void* param){

cv::Mat &image = \*(cv::Mat\*)param ;  
switch(event) {  
    case cv::EVENT\_MOUSEMOVE:{  
        if(drawing\_box) {  
            box.width = x - box.x ;  
            box.height = y - box.y ;  
        }  
    }  
    break ;

    case cv::EVENT\_LBUTTONUP:{  
        drawing\_box = false ;  
        if (box.width < 0) {  
            box.x += box.width ;  
            box.width \*= -1    ;  
        }

        if (box.height < 0) {  
            box.y += box.height ;  
            box.height \*= -1 ;  
        }  
        draw\_box(image, box) ;  
    }  
    break ;

}  

}

滑块,轨道杆和开关
HighGUI提供了一个方便的滑块元素。在HighGUI中, 滑块是叫做轨道栏 。
这是因为他们的重要(历史)意图是在视频回放中选择特定帧。
当然,一旦将轨道栏添加到HighGUI中,人们开始将它们用于滑块的所有常见事情,以及许多不寻常的事情
与父窗口一样,滑块被赋予唯一的名称(以字符串的形式),之后总是被引用 用这个名字。

用于创建轨迹栏的HighGUI例程是:
int cv::createTrackbar(
const string& trackbarName , //用于标识轨迹栏的句柄
const string& windowName , //用于标识窗口的句柄
int *value , //滑块初始位置
int count , //最右侧滑块的总计数
cv::TrackbarCallback onChange = NULL , //回调函数(可选)
void * param = NULL //回调fn的附加参加
);

接下来的两个参数是value一个指向整数的指针,该整数将自动设置为滑块移动到的值,以及滑块count最大值的数值。
最后一个参数是指向回调函数的指针移动滑块时自动调用。这与鼠标事件的回调完全类似。
如果使用,则必须使用回调函数具有指定的形式。
这些函数允许你从程序的任何位置读取或设置轨迹栏的值。

无按钮生存
不幸的是,HighGUI中的本机接口不提供对按钮的任何明确支持。
因此,在特别懒惰的情况下,通常的做法是使用仅具有两个位置的滑块。
在OpenCV示例中经常出现的另一个选项是使用键盘快捷键而不是按钮开关
只有滑块(轨道栏)两个position,“on”(1)和“off”(0)(count 即已设置为1)。

例| 使用轨迹栏创建用户可以打开和关闭的“开关”;
该程序播放视频并使用该开关创建暂定功能。
————————————————————————————————————————————————————————————————
//用户可以在屏幕上绘制框的示例程序
//
#include
#include
using namespace std ;

//使用轨迹栏创建用户可以打开和关闭的“开关”
//我们将此值设为全局,以便每个人都能看到它。

int g_switch_value = ;
void switch_off_function() {cout << "Pause\n";} ;
void switch_on_function() {cout <<"Run \ n";} ;

void switch_callback(int position, void *) {
if (position == 0) {
switch_off_function() ;
}else {
switch_off_function() ;
}
}

void help() {
cout << "Call:my.avi" << endl ;
cout << "显示在视频中放置暂定按钮" << endl ;
}

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

cv::Mat frame ;     //保存视频框架  
cv::VideoCapture g\_capture ;  
help() ;

if(argv < 2 || !g\_capture.open(argv\[1\])) {  
    cout << "无法打开" << argv\[1\] << "视频文件\\n" << endl ;  
    return -1 ;  
}

//命名主窗口  
cv::namedWindow("Example",1) ;  
//创建轨迹栏,并告诉它父窗口的名称。  

cv::createTrackbar(  
    "Switch" ,  
    "Example" ,  
    &g\_switch\_value,  
    1,  
    switch\_callback  
) ;

for (;;)  
{  
    if (g\_switch\_value) {  
        g\_capture >> frame ;  
        if(frame.empty()) break ;  
    }  
    if (cv::waitKey(10)==27) break ;  
}  
return 0 ;  

}

文字叠加
Qt GUI提供的另一个选项是能够在您正在显示的图像顶部放置一个短暂的横幅。
此横幅称为叠加层,并在其周围显示阴影框以便于阅读。
如果您只想抛出视频上的帧数或帧速率等简单信息,
或者您正在查看的图像的文件名,这是一个非常方便的功能。
您可以在任何窗口上显示叠加,即使您正在使用cv::GUI_NORMAL。
int cv::displayOverlay(
const string& name, //用于标识窗口
const string& test, //用于显示的文本
int delay //显示文本的毫秒数(0='forever')
)
cv::displayOverlay()函数接受三个参数。
第一个是要在其上显示覆盖的窗口的名称。
第二个参数是您希望在窗口中显示的任何文本。
(这里有一个警告词:文本的大小是固定的,所以如果你试图在其中塞进太多的东西,它就会溢出。默认情况下,文本总是居中对齐。)
第三个也是最后一个参数delay是覆盖将保持在原位的时间量(以毫秒为单位)。
如果延迟设置为0,那么覆盖将无限期地保持在原处,
或者至少直到您用另一个对cv::displayOverlay()的调用重写它为止。
通常,如果在上一个调用的延迟计时器过期之前调用cv::displayoverlay(),
则会删除上一个文本并替换为新文本,并且无论新调用之前计时器中还有什么,计时器都会重置为新的延迟值。

将您自己的文本写入状态栏
除了叠加层,您还可以将文本写入状态栏。
默认情况下,状态栏包含有关鼠标指针所在像素的信息(如果有)。
您可以在中看到状态栏包含x,y位置以及制作图形时指针下方像素的RGB颜色值。
您可以使用以下cv::displayStatusBar() 方法将此文本替换为您自己的文本:

int cv::displayStatusBar(
const string& name , //用于标识窗口的句柄
const string& test , //要显示的文本
int delay //显示文本的毫秒数字(0='forever')
)

与之不同cv::displayOverlay(),cv::displayStatusBar()只能在使用cv::GUI_EXTENDED标志创建的窗口上使用(即,首先具有状态栏的窗口)。
当delay 计时器到期时(如果未将其设置为0 ),则默认的x,y 和RGB文本将重新出现在状态栏中

属性窗口
在HighGUI原生界面的上一节中,我们看到可以向Windows添加轨迹栏。
图9中的轨道栏是使用cv::createTrackbar() 我们之前看到的相同命令创建的。
唯一真正的区别是图中的轨道栏比我们使用非Qt接口创建的轨道栏更漂亮。
然而,Qt界面中的重要新概念是我们还可以在属性窗口中创建轨迹栏。
我们只是像往常一样创建轨迹栏,但是通过指定一个空字符串作为轨道栏将附加到的窗口名称。

int contrast = 128;
cv :: createTrackbar(“Contrast:”,“”,&contrast,255,on_change_contrast);
例如,此片段将在属性窗口中创建一个标记为“对比度:”的轨迹栏,其值将从最开始的值开始128 ,最大值为255 。
每当调整滑块时,on_change_contrast()都会调用回调。

创建按钮:
使用cv::createButton():
Qt界面提供的最有用的新功能之一是能够创建按钮。
这包括普通按钮,无线电式(互斥)按钮和复选框。创建的所有按钮始终位于控制面板中。
所有三种类型的按钮都使用相同的方法创建:
int cv::createButton(
const string& buttonName , //用于标识轨迹栏的句柄
cv::ButtonCallback onChange = NULL //回调按钮事件
void* params , //(可选)用于按钮事件的参数
int buttonType = cv::PUSH_BUTTON, //PUSH_BUTTON或RADIOBOX
int initialState = 0 //开始按钮的状态
)

该按钮需要一个名称,该名称buttonName 将出现在按钮上或旁边。
如果您愿意,可以忽略此参数并简单地提供一个空字符串,
在这种情况下,按钮名称将以序列化方式自动生成(例如,“按钮0”,“按钮1”等)。
第二个参数onChange 是一个回调,只要单击该按钮就会调用该回调。
这种回调的原型必须与声明匹配cv::ButtonCallback ,即:

void your_button_callback(
int state , //标识按钮事件类型
void * params //来自cv::createButton()的参数
);

该buttonType参数可以取三个值之一:cv::PUSH_BUTTON ,cv::RADIOBOX 或cv::CHECKBOX 。
第一个对应于您的标准按钮-您单击它,它会调用您的回调。
对于复选框,值将是1 或0 取决于是选中还是取消选中该框。
对于单选按钮也是如此,除了当您单击单选按钮时,对于您刚刚单击的按钮以及现在未单击的按钮(由于单选按钮的互斥特性),都会调用回调。
同一行中的所有按钮 - 下一个描述的按钮栏 - 被假定为同一互斥群组的一部分。

cv::namedWindow("Image",cv::GUI_EXPANDED);
cv::displayOverlay("Image",file_name,0);
cv :: createTrackbar(“Trackbar0”,“Image”,&mybar0,255);
cv :: createTrackbar(“Trackbar1”,“Image”,&mybar1,255);
cv :: createButton(“”,NULL,NULL,cv :: PUSH_BUTTON);
cv :: createButton(“”,NULL,NULL,cv :: PUSH_BUTTON);
cv :: createButton(“”,NULL,NULL,cv :: PUSH_BUTTON);
cv :: createTrackbar(“Trackbar2”,“”,&mybar1,255);
cv :: createButton(“Button3”,NULL,NULL,cv :: RADIOBOX,1);
cv :: createButton(“Button4”,NULL,NULL,cv :: RADIOBOX,0);
cv :: createButton(“Button5”,NULL,NULL,cv :: CHECKBOX,0);

您将注意到Trackbar0 并Trackbar1 在被调用的窗口中创建"Image"
,同时Trackbar2 在未命名的窗口(属性窗口)中创建。
前三个cv::createButton() 调用没有给出按钮的名称,
您可以在图9-5中看到 自动分配的名称放在按钮上。
您还将在图9-5中 注意到前三个按钮在一行中,而第二个三个按钮在另一行中。这是因为轨迹栏。

按钮是一个接一个地创建的,每个按钮都在其前一个版本的右侧,
直到(除非)创建了一个轨迹栏。由于轨迹栏占用了整行,因此在按钮下方会有自己的行。
如果创建了更多按钮,则此后它们将显示在新行上。

文字和字体
要使用Qt界面编写文本,必要首先创建一个cvFont对象.要在屏幕上放置一些文本,就可以使用该对象.
字体是通过以下功能创建的:cv::fontQt()
CvFont fontQt(//返回字体表征结构
const string& fontName , //例如“Times”
int pointSize, //字体大小,使用“point”系统
cv::Scalar color = cv:: // BGR颜色为标量
int weight = cv::FONT_NORMAL ,//字体粗细,1-100
int spacing = 0 //各个字符之间的空格
)

Qt-font权重及其关联值的预定义别名

相机捕捉常数 | 数值

cv::FONT_LIGHT | 25

cv::FONT_NORMAL | 50

cv::FONT_DEMIBOLD | 63

cv::FONT_BOLD | 75

cv::FONT_BLACK | 87

你可以把文字显示在图像上
cv::addText(
cv::Mat& image , //要写入的图像
const string& test, //要写入的文本
cv::Point location , //文本左下角的位置
CvFont* font //Opencv字体
)

设置和获取窗口属性
当你使用Qt后端时,创建时窗口集的许多状态属性都是可以查询的,并且即使在窗口创建之后
也可以更改(设置)其中许多属性.
void cv::setWindowProperty(
const string& name, //用于标识窗口的句柄
int prop_id, //用于标识窗口属性
double prop_value //要设置属性的值
)

double cv::getWindowProperty(
const string& name , //用于标识窗口的句柄
int prop_id //标识窗口属性
)
要获取窗口属性,只需要调用cv::getWindowProperty()并提供name
窗口以及prop_id 您感兴趣的属性的属性ID(参数)
同样,您可以使用cv::setWindowProperty()相同的属性ID设置窗口属性。

可获取和可设置的窗口属性

名称 | 描述

cv::WIND_PROP_FULL_SIZE | 设置cv::WINDOW_FULLSCREE为全屏窗口或cv::WINDOW_NORMAL常规窗口

cv::WIND_PROP_AUTOSIZE |设置为cv::WINDOW_AUTOSIZE使窗口自动调整大小
以显示图像, 或cv::WINDOW_NORMAL 将图像大小设置为窗口。

设置为cv::WINDOW_AUTOSIZE 使窗口自动调整大小以显示图像,或cv::WINDOW_NORMAL 将图像大小设置为窗口。

cv::WIND_PROP_ASPECTRATIO | 设置为cv::WINDOW_FREERATIO允许窗口具有任何宽高比

(作为用户调整大小的结果)或cv::WINDOW_KEEPRATIO允许用户调整大小仅影响绝对大小(而不是宽高比)。

保存和恢复窗口状态
Qt界面还允许保存和恢复窗口的状态.因为它不仅包括窗口的位置和大小,
还包括所有轨道栏和按钮的状态。接口状态与cv::saveWindowParameters() 函数一起保存,
该函数使用一个参数指示要保存的窗口:
void cv::saveWindowParameters(
const string& name //用于标识窗口的句柄
);
保持窗口状态后,可以使用补充cv::loadWindowParameters()功能恢复它:
void cv::loadWindowParameters(
const string& name //用于标识窗口的句柄
)

与OpenGL交互
Qt界面允许您执行的最令人兴奋的事情之一是使用OpenGL生成图像并将图像叠加在您自己的图像之上。16 这对于可视化和调试机器人或增强现实应用程序,
或者您尝试从图像生成三维模型并希望在原始图像上查看结果的任何地方都非常有效。
创建一个OpenGL绘图函数的回调,然后将其注册到接口.
cv::Opencv负责其余部分,然后在每次绘制窗口时调用回调.
(包括每当您调用时cv::imshow() ,就像使用视频流的连续帧一样)
你的回调应该与原型匹配cv::OpenGLCallback(),这意味着它应该类似于以下内容:
void your_opengl_callback(
const string& windowName, //用于标识窗口的句柄
cv::OpenGLCallback回调 , //OpenGL回调例程
void * params = NULL //(可选)回调函数
)

在OpenGL中绘制了一个立方体,用变量(rotx 和roty)替换了该多维数据集中的固定旋转角度

void on_opengl(void* param){
glMatrixModel(GL_MODELVIEN);
glLoadIdentity();
glTranslated(0.0,0.0,-1.0);
glRotatef(rotx,10,0,0) ;
glRotatef(roty,0,1,0);
glRotatef(0,0,0,1);

static const int coords\[6\]\[4\]\[3\] = {  
    {{+ 1,-1,-1},{-1,-1,-1},{-1,+ 1,-1},{+ 1,+ 1,-1}},  
    {{+ 1,+ 1,-1},{-1,+ 1,-1},{-1,+ 1,+ 1},{+ 1,+ 1,+ 1}},  
    {{+ 1,-1,+ 1},{+ 1,-1,-1},{+ 1,+ 1,-1},{+ 1,+ 1,+ 1}},  
    {{-1,-1,-1},{-1,-1,+ 1},{-1,+ 1,+ 1},{-1,+ 1,-1}},  
    {{+ 1,-1,+ 1},{-1,-1,+ 1},{-1,-1,-1},{+ 1,-1,-1}},  
    {{-1,-1,+ 1},{+ 1,-1,+ 1},{+ 1,+ 1,+ 1},{ -  1,+ 1,+ 1}}  
};

for (int i = 0; i < 6; ++i){  
    glColor3ub(i\*20, 100 + i \* 10, i \* 42);  
    glBegin(GL\_GUADS);  
    for (int j = 0; j < 4; ++j){  
        glVertex3d(  
            0.2 \* coords\[i\]\[j\]\[0\];  
            0.2 \* coords\[i\]\[j\]\[1\];  
            0.2 \* coords\[i\]\[j\]\[2\];  
        );  
    }  
    glEnd();  
}  

}

将Opencv与完整GUI工具包集成
甚至是Opencv的内置Qt在我们开发代码或探索算法时,接口仍然只是实现一些简单任务的便携方式.
当真正构建面向最终用户的应用程序时,原生UI和基于Qt的界面都不会这么做.
如果您想将OpenCV与功能更全面的工具包一起使用,探索如何处理将要出现的问题是很有用的。
我们实际上在这里探索的这几个应该给你足够的洞察力,
你可以毫不费力地找出在其他类似环境中要做什么的反复出现的问题。
主要问题是如何将OpenCV图像转换为工具包对图形所期望的形式,
以及知道工具包中的哪个小部件或组件将为您显示工作。

OpenCV和Qt的一个例子
创建一个Qt应用程序并添加我们的QMoviePlayer小部件.
一个示例程序ch4_qt.cpp,接受一个表示视频文件的参数;

该视频文件将在我们将定义的Qt对象内重放,称为QMoviePlayer

#include
#include
#include

int main(int argc, char* argv[]){
QApplication app(argc,argv);

QMoviePlayer mp;  
mp.open(argv\[1\]);  
mp.show();  
return app.exec(;)  

}

QMoviePlayer对象头文件QMoviePlayer.hpp


#include“ui_QMoviePlayer.h”
#include
#include

using namespace std;

class QMoviePlayer: public QWidget{
Q_OBJECT;
public:
QMoviePlayer(Qwidget* parent = NULL);
virtual ~QMoviePlayer(){;}

bool open(string file);

private:  
UI::QMoviePlayer ui;  
cv::Mat m\_cv\_img;  
QTimer\* m\_timer;

void paintEvent(QPaintEvent\* q);  
void \_copyImage(void);

public slots:  
void nextFrame();  

};

QMoviePlayer对象源文件:QMoviePlayer.cpp

#include
#include
#include

QMoviePlayer::QMoviePlayer(QWidget* parent):QWidget(parent){
ui.setupUi(this);
}
用于QMoviePlayer调用setup函数的顶级构造函数,该函数是为UI成员自动构建的.
bool QMoviePlayer::open(string file){
if (!m_cap.open(file)) return false ;
//如果我们打开文件,请立即设置所有内容:
//

m\_cap.read(m\_cv\_img);  
m\_qt\_img = QImage(  
    QSize(m\_cv\_img.cols, m\_cv\_img,row),  
    QImage::Format\_RGB88  
);  
ui.frame->setMinimumSize(m\_qt\_img.width(), m\_qt\_img.height());  
ui.frame->setMaximumSize(m\_qt\_img.width(), m\_qt\_img.height());  
\_copyImage();

m\_timer = new QTimer(this);  
connect(  
    m\_timer,  
    SIGNAL(timeout()),  
    this,  
    SLOT(nextFrame())  
);  
m\_timer->start(1000. / m\_cap.get(cv::CAP\_PROP\_FPS));  
return true;  

}
该QMoviePlayer::_copyImage()函数负责将图像从缓冲区复制m_cv_img到Qt图像缓冲区m_qt_img 。
首先,我们定义一个cv::Mat 名为的对象cv_hreader_to_qt_image.
当我们定义该对象时,我们实际上告诉它为其数据区域使用哪个区域,
并将其交给Qt QImage 对象的数据区域m_qt_img.bits() 。
然后我们调用cv::cvtColor 复制,它处理OpenCV更喜欢BGR排序的微妙之处,而Qt更喜欢RGB。
void QMoviePlayer::nextFrame(){
//如果捕获对象未打开,则无需执行任何操作
if (!m_cap.isOpened()) return ;
m_cap.read(m_cv_img);
_copyImage();

this->update();  

}

该QMoviePlayer::nextFrame() 函数实际上处理后续帧的读取。
回想一下,只要QTimer到期,就会调用此例程。
它将新图像读入OpenCV缓冲区,调用QMoviePlayer::_copyImage()将其复制到Qt缓冲区,
然后QWidget 对此进行更新调用(这样Qt就知道某些内容已经发生了变化)。
void QMoviePlayer::paintEvent(QPaintEvent* e){
QPainter painter(this);
painter.drawImage(QPoint(ui.frame->x(), ui.frame->y()),m_qt_img);
}
最后但并非最不重要的是QMoviePlayer::paintEvent() 功能。
只要有必要实际绘制QMoviePlayer 小部件,Qt就会调用它。
这个函数只创建一个QPainter并告诉它绘制当前的Qt图像m_qt_img(从屏幕的角落开始)。

[OpenCV和wxWidgets的一个例子]

接受一个表示视频文件的参数; 该视频文件将在我们将定义的wxWidgets对象中重放,称为WxMoviePlayer

#include "wx/wx.h"
#include "WxMoviePlayer.hpp"
//应用程序类,wxWidgets中的顶级对象
//
class MyApp:public wxApp {
上市:
virtual bool OnInit();
};

//在幕后创建main()函数并附加MyApp的东西
//
DECLARE_APP(MyApp);
IMPLEMENT_APP(MyApp);

//初始化MyApp时,请执行以下操作。
//
bool MyApp::OnInit(){
wxFrame * frame = new wxFrame(NULL,wxID_ANY,wxT(“ch4_wx”));
frame->show(true);

WxMoviePlayer\* mp = new WxMoviePlayer(  
    frame,  
    wxPoint(-1,-1),  
    wxSize(640,480)  
);  
mp->open(wxString(argv\[1\]));  
mp->Show(true);

return true;  

}
我们要做的第一件事就是为我们的应用程序创建一个类定义,我们从库类派生出来wxApp 。
我们class唯一不同的是它会MyApp::OnInit() 用我们自己的内容重载函数。声明后class MyApp,我们调用两个宏:DECLARE_APP()和IMPLEMENT_APP() 。

WxMoviePlayer对象头文件WxMoviePlayer.hpp

#include“opencv2 / opencv.hpp”
#include“wx / wx.h”
#include
#define TIMER_ID 0

using namespace std ;
class WxMoviePlayer : public wxWindow {

public:
WxMoviePlayer(
wxWindow* parent,
const wxPoint& pos,
const wxSize& size
);
virtual ~WxMoviePlayer() {};
bool open( wxString file );

private:

cv::VideoCapture m\_cap;  
cv::Mat          m\_cv\_img;  
wxImage          m\_wx\_img;  
wxBitmap         m\_wx\_bmp;  
wxTimer\*         m\_timer;  
wxWindow\*        m\_parent;

void \_copyImage( void );

void OnPaint( wxPaintEvent& e );  
void OnTimer( wxTimerEvent& e );  
void OnKey(   wxKeyEvent&   e );

protected:
DECLARE_EVENT_TABLE();
};
该WxMoviePlayer 对象派生自wxWindow,它是wxWidgets用于在屏幕上可见的任何事物的泛型类.
我们有三个事件处理方法,OnPaint() ,onTimer() ,和OnKey() ;
这些将分别处理绘图,从视频中获取新图像,以及使用Esc键关闭文件。
最后, 除了OpenCV 类型的图像之外,您还会注意到有一个类型wxImage的对象和一个类型的对象。
在wxWidgets中,位图 (与操作系统相关)与图像不同wxBitmapcv:Mat(它们是与设备无关的图像数据表示)。当我们查看代码文件WxMoviePlayer.cpp时,这两者的确切作用将很清楚

WxMoviePlayer对象源文件WxMoviePlayer.cpp

#include“WxMoviePlayer.hpp”

BEGIN_EVENT_TABLE(WxMoviePlayer,wxWindow)
EVT_PAINT(WxMoviePlayer :: OnPaint)
EVT_TIMER(TIMER_ID,WxMoviePlayer :: OnTimer)
EVT_CHAR(WxMoviePlayer :: OnKey)
END_EVENT_TABLE()
我们要做的第一件事是设置与各个事件相关联的回调。
我们通过wxWidgets框架提供的宏来完成此任务。
WxMoviePlayer :: WxMoviePlayer(
wxWindow * parent,
const wxPoint&pos,
const wxSize&size
):wxWindow(parent,-1,pos,size,wxSIMPLE_BORDER){
m_timer = NULL;
m_parent = parent;
}

void WxMoviePlayer :: OnPaint(wxPaintEvent&event){
wxPaintDC dc(this);

if(!dc.Ok())return;

int x,y,w,h;
dc.BeginDrawing();
dc.GetClippingBox(&x,&y,&w,&h);
dc.DrawBitmap(m_wx_bmp,x,y);
dc.EndDrawing();

return;
}
WxMoviePlayer::OnPaint() 只要窗口需要在屏幕上重新绘制,就会调用该例程。
void WxMoviePlayer::_copyImage( void ) {
m_wx_bmp = wxBitmap( m_wx_img );

Refresh( FALSE ); // indicate that the object is dirty
Update();
}

bool WxMoviePlayer::open( wxString file ) {

if( !m_cap.open( std::string( file.mb_str() ) )) {
return false;
}

// 如果我们打开文件,请立即设置所有内容:
//
m_cap.read( m_cv_img );

m_wx_img = wxImage(
m_cv_img.cols,
m_cv_img.rows,
m_cv_img.data,
TRUE //静态数据,不要在delete()上释放
);

_copyImage();

m_timer = new wxTimer( this, TIMER_ID );
m_timer->Start( 1000. / m_cap.get( cv::CAP_PROP_FPS ) );

return true;

}
该WxMoviePlayer::open() 方法还做了几件重要的事情。
第一个是实际打开cv::VideoCapture 对象,但还有很多工作要做。
接下来,从播放器读取图像并用于创建wxImage“指向”OpenCV cv::Mat 图像的对象。

void WxMoviePlayer::OnTimer( wxTimerEvent& event ) {

if( !m_cap.isOpened() ) return;

m_cap.read( m_cv_img );
cv::cvtColor( m_cv_img, m_cv_img, cv::BGR2RGB );
_copyImage();

}

那个处理程序不会做太多; 主要是它只是从视频中读取一个新帧,将该帧从BGR转换为RGB进行显示,
然后调用我们的WxMoviePlayer::_copyImage() ,为我们制作下一个位图。

void WxMoviePlayer::OnKey( wxKeyEvent& e ) {
if( e.GetKeyCode() == WXK_ESCAPE ) m_parent->Close();
}

最后,我们有任何按键的处理程序。它只是检查该键是否是Esc键,如果是,则关闭程序。
请注意,我们不关闭WxMoviePlayer 对象,而是关闭父框架。

我们的自定义View类的示例头文件

class COpenCVTestView : public CWindowImpl {

public:
DECLARE_WND_CLASS(NULL)

bool OpenFile(std::string file);
void _copyImage();

BOOL PreTranslateMessage(MSG* pMsg);

BEGIN_MSG_MAP(COpenCVTestView)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_TIMER, OnTimer)
END_MSG_MAP()

// 处理程序原型(如果需要,取消注释参数:
// LRESULT MessageHandler(
// UINT /*uMsg*/,
// WPARAM /*wParam*/,
// LPARAM /*lParam*/,
// BOOL& /*bHandled*/
// );
// LRESULT CommandHandler(
// WORD /*wNotifyCode*/,
// WORD /*wID*/,
// HWND /*hWndCtl*/,
// BOOL& /*bHandled*/
// );
// LRESULT NotifyHandler(
// int /*idCtrl*/,
// LPNMHDR /*pnmh*/,// BOOL& /*bHandled*/
// );

LRESULT OnPaint(
UINT /*uMsg*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& /*bHandled*/
);
LRESULT OnTimer(
UINT /*uMsg*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& /*bHandled*/
);
LRESULT OnEraseBkgnd(
UINT /*uMsg*/,
WPARAM /*wParam*/,
LPARAM /*lParam*/,
BOOL& /*bHandled*/
);

private:
cv::VideoCapture m_cap;
cv::Mat m_cv_img;

RGBTRIPLE* m_bitmapBits;
};

这里的结构与前面的wxWidgets示例非常相似。
视图代码之外的唯一更改是Open 菜单项处理程序,
它将在您的CMainFrame 类中。它需要调用view类来打开视频:

LRESULT CMainFrame::OnFileOpen(
WORD /*wNotifyCode*/,
WORD /*wID*/,
HWND /*hWndCtl*/,
BOOL& /*bHandled*/) {
WTL::CFileDialog dlg(TRUE);
if (IDOK == dlg.DoModal(m_hWnd)) {
m_view.OpenFile(dlg.m_szFileName);
}
return 0;
}

bool COpenCVTestView::OpenFile(std::string file) {

if( !m_cap.open( file ) ) return false;

// 如果我们打开文件,请立即设置所有内容:
//
m_cap.read( m_cv_img );

// 可以在这里创建一个DIBSection,但我们只是为原始位分配内存
//
m_bitmapBits = new RGBTRIPLE[m_cv_img.cols * m_cv_img.rows];

_copyImage();

SetTimer(0, 1000.0f / m_cap.get( cv::CAP_PROP_FPS ) );

return true;
}

void COpenCVTestView::_copyImage() {

// 将图像数据复制到位图中
//
cv::Mat cv_header_to_qt_image(
cv::Size(
m_cv_img.cols,
m_cv_img.rows
),
CV_8UC3,
m_bitmapBits
);
cv::cvtColor( m_cv_img, cv_header_to_qt_image, cv::BGR2RGB );
}

LRESULT COpenCVTestView::OnPaint(
UINT /* uMsg */,
WPARAM /* wParam */,
LPARAM /* lParam */,
BOOL& /* bHandled */
) {
CPaintDC dc(m_hWnd);

WTL::CRect rect;
GetClientRect(&rect);

if( m_cap.isOpened() ) {

BITMAPINFO bmi = {0};  
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);  
bmi.bmiHeader.biCompression = BI\_RGB;  
bmi.bmiHeader.biWidth       = m\_cv\_img.cols;

// 请注意,位图默认为自下而上,使用负高度  
// 代表自上而下  
//  
bmi.bmiHeader.biHeight = m\_cv\_img.rows \* -1;

bmi.bmiHeader.biPlanes = 1;  
bmi.bmiHeader.biBitCount = 24;  // 32 if you use RGBQUADs for the bits

dc.StretchDIBits(  
  0,                     0,  
  rect.Width(),          rect.Height(),  
  0,                     0,  
  bmi.bmiHeader.biWidth, abs(bmi.bmiHeader.biHeight),  
  m\_bitmapBits,  
  &bmi,  
     DIB\_RGB\_COLORS,  
  SRCCOPY  
);

} else {

dc.FillRect(rect, COLOR\_WINDOW);

}

return 0;
}

LRESULT COpenCVTestView::OnTimer(
UINT /* uMsg */,
WPARAM /* wParam */,
LPARAM /* lParam */,
BOOL& /* bHandled */
) {
// 如果捕获对象未打开,则无需执行任何操作
if( !m_cap.isOpened() ) return 0;

m_cap.read( m_cv_img );
_copyImage();

Invalidate();

return 0;
}

LRESULT COpenCVTestView::OnEraseBkgnd(
UINT /* uMsg */,
WPARAM /* wParam */,
LPARAM /* lParam */,
BOOL& /* bHandled */
) {
//因为我们在OnPaint处理程序中完全绘制了窗口,所以请使用
//一个空的后台处理程序
return 0;
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器