在最近做的行人检测项目中,由于需要训练分类器,而分类器的训练又需要有一个一定长宽的样本。为了方便样本的采集,因此实现了这样的一个截图程序。该程序的主要功能是加载视频到程序中,程序可以对视频进行播放、暂停的控制。程序可以在播放界面中直接拖动,画出选区,然后确认保存即可将截取的图片保存到指定路径中并且重新设置截取出来的图片的大小,以适应分类 器训练的需要。该程序的开发环境为Qt4.8.5+qtcreator2.8.0,其中还用到了opencv中的一些函数。
程序的主要界面如下:
主界面
播放视频并截图
属性设置
关于信息
以上便是程序的一些界面。
当时在需要写这个程序的时候,心中便已经有了一个大概的思路了,搭建整个框架也很简单,只用了一天的时间。但是在实际的写代码过程中,还是遇到了一些问题。其中,主要遇到的问题就是:(1)如何在QLabel中画出感兴趣区域并实时显示截取框,最后保存截取下来的图片。(2)如何在属性设置对话框和主界面之间进行参数传递。一下内容主要记录这两个问题中,我的解决方法。
一、如何在QLabel中画出感兴趣区域并实时显示截取框、最后保存截取下来的图片
该程序一开始,并不是设计成从视频中截取样本的,最初是设计成从图片中截取出需要的样本。但是后来导师希望可以从视频中截取样本,因此最后做出了修改。这也是为什么会用到opencv的原因。因为之前写的程序已经可以实现从图片中截取样本,而视频,其实是多幅图像连续播放形成的。所以我只要在原来程序的基础上,将视频的每一帧都放到控件上,就可以实现视频的截取了。而opencv是一个开源的计算机视觉库,对图像和视频有很好的处理能力(在这里用opencv其实是大材小用了,但是这样可以很大程序的复用之前写的代码)。所以可以用opencv中的函数来读取出视频中的每一帧。
因为处理视频和处理图片其实是一个样的,所以这里说说怎么实现从图片中截图的。
在Qt中,要想“显示”某些信息包括文字、图片,自然想到类QLabel了。但是QLabel只是一个基础类,它不能够完全满足我们需要的功能。所以需要继承下来,并进行功能扩充。在这里感觉到了C++中继承的魅力。我完全不知道QLabel的实现机制,仅仅知道它提供的一些函数接口,但是我可以继承下来并进行自定义以实现自己需要的功能。
MyLabel头文件定义:
//自定义图片显示区域及截图功能//MyLabel.h #ifndef MYLABEL_H #define MYLABEL_H #include <QLabel> #include <QMenu> #include <QAction> class MyLabel:public QLabel { Q_OBJECT public: MyLabel(); ~MyLabel(); void setAddress(QString addr); void setImgW(int num); void setImgH(int num); void set(int a,int b,int c,int d); private: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *ev); void mouseMoveEvent(QMouseEvent *ev); void mouseReleaseEvent(QMouseEvent *ev); void saveImage(); int x,y,wid,hei; QString address; int imgW,imgH; QPixmap grapPix; bool mouseState; QMenu *popMenu; QAction *save,*cancle; void initMenu(); void showMenu(QPoint pos); private slots: void saveSlot(); void cancleSlot(); }; #endif // MYLABEL_H
MyLabel实现文件的完整定义:
#include "mylabel.h" #include <QLabel> #include <QPainter> #include <QMouseEvent> #include <QRect> #include <QDateTime> #include <iostream> #include <math.h> MyLabel::MyLabel() { address="./images/"; imgW=; imgH=; initMenu(); mouseState=false; //图片显示面板美化 connect(save,SIGNAL(triggered()),this,SLOT(saveSlot())); connect(cancle,SIGNAL(triggered()),this,SLOT(cancleSlot())); this->setStyleSheet("border-width:2px;border-style:solid;border-color:#770576;"); } MyLabel::~MyLabel() { } void MyLabel::setAddress(QString addr) { address=addr; } void MyLabel::setImgW(int num) { imgW=num; } void MyLabel::setImgH(int num) { imgH=num; } void MyLabel::set(int a, int b, int c, int d) { x=a; y=b; wid=c; hei=d; } void MyLabel::paintEvent(QPaintEvent *event) { QLabel::paintEvent(event);//调用父类的event以显示背景 QPainter painter(this); painter.setPen(QPen(Qt::red,)); painter.drawRect(QRect(x,y,wid,hei)); } void MyLabel::mousePressEvent(QMouseEvent *ev) { x=ev->x(); y=ev->y(); } void MyLabel::mouseMoveEvent(QMouseEvent *ev) { if(!(this->pixmap())) { x=-;y=-; wid=-; hei=-; return ; } QPoint pos=ev->pos(); mouseState=true; wid=ev->x()-x; hei=ev->y()-y;this->repaint(); } void MyLabel::mouseReleaseEvent(QMouseEvent *ev) { if(!(this->pixmap())) { x=-;y=-; wid=-; hei=-; return ; } QRect rect; if(ev->x()>x&&ev->y()>y){ rect.setRect(x+,y+,abs(wid)-,abs(hei)-); } else if(ev->x()>x&&ev->y()<y){ rect.setRect(x,ev->y(),abs(wid)-,abs(hei)-); } else if(ev->x()<x&&ev->y()>y){ rect.setRect(ev->x(),y,abs(wid)-,abs(hei)-); } else{ rect.setRect(ev->x()+,ev->y()+,abs(wid)-,abs(hei)-); } //rect.setRect(ev->x(),ev->y(),abs(wid)-1,abs(hei)-1); grapPix=QPixmap::grabWidget(this,rect);//截取矩形区域内图像 QPoint temp; temp.setX(ev->x()); temp.setY(ev->y()); if(mouseState==true){ showMenu(temp); mouseState=false; } } //2015年7月23日晚上9:30 void MyLabel::saveImage() { if(!(this->pixmap())) { x=-;y=-; wid=-; hei=-; return ; } grapPix=grapPix.scaled(QSize(imgW,imgH), Qt::KeepAspectRatio);//等比例放大或缩小到指定大小 QDateTime time=QDateTime::currentDateTime(); QString str=time.toString("yyyyMMddhhmmss"); QString temp=address+str+".png"; grapPix.save(temp,"PNG"); } void MyLabel::initMenu() { popMenu=new QMenu(this); save=new QAction("save",this); cancle=new QAction("cancle",this); popMenu->addAction(save); popMenu->addAction(cancle); } void MyLabel::showMenu(QPoint pos) { popMenu->exec(QCursor::pos()); } void MyLabel::saveSlot() { saveImage(); ,-,-,-); this->repaint(); } void MyLabel::cancleSlot() { ,-,-,-); this->repaint(); }
通过上面的类继承也可以看出,我们需要重载一些函数,还需要增加一些成员函数来实现我们需要的功能。
首先需要在QLabel中显示一个矩形框,这个怎么实现呢?我们需要重载paintEvent函数来实现,其具体实现如下:
void MyLabel::paintEvent(QPaintEvent *event) { QLabel::paintEvent(event);//调用父类的event以显示背景 QPainter painter(this); painter.setPen(QPen(Qt::red,)); painter.drawRect(QRect(x,y,wid,hei)); }
其中,x,y表示矩形框左上角的坐标。wid、hei表示矩形框的宽和高。通过上面的重载,便可在Label上显示矩形框。
那么下面的问题是:如何实时显示矩形框。这里需要重载mouseMoveEvent函数。该函数实现了怎样的效果,感兴趣的伙伴可以试试,感受一下。你可以在该函数内qDebug一个数或者鼠标的坐标,然后用你继承的这个类声明一个对象,并在这个label上按下并移动你的鼠标,看看输出的是什么东西。我的重载代码如下:
void MyLabel::mouseMoveEvent(QMouseEvent *ev) { if(!(this->pixmap())) { x=-;y=-; wid=-; hei=-; return ; } QPoint pos=ev->pos(); mouseState=true; wid=ev->x()-x; hei=ev->y()-y; this->repaint(); }
其他的是一些辅助的操作,比如判断空间是否是空的,如果是则不执行该函数。如果不是,则更新矩形框的信息,并保存到成员变量中,这些信息用来绘制矩形框。上面的代码中,最主要的就是this->repaint();这个函数会立即重绘当前控件。因此,只要鼠标一移动,就会调用该函数,并重新绘制窗口,从而实现了实时显示矩形框。
以上步骤只是将感兴趣区域选择出来并在空间上显示。关于如何截取出感兴趣区域并保存到本地,Qt也提供了很好的支持。我的保存截取图片的 代码如下:
void MyLabel::mouseReleaseEvent(QMouseEvent *ev) { if(!(this->pixmap())) { x=-;y=-; wid=-; hei=-; return ; } QRect rect; if(ev->x()>x&&ev->y()>y){ rect.setRect(x+,y+,abs(wid)-,abs(hei)-); } else if(ev->x()>x&&ev->y()<y){ rect.setRect(x,ev->y(),abs(wid)-,abs(hei)-); } else if(ev->x()<x&&ev->y()>y){ rect.setRect(ev->x(),y,abs(wid)-,abs(hei)-); } else{ rect.setRect(ev->x()+,ev->y()+,abs(wid)-,abs(hei)-); } //rect.setRect(ev->x(),ev->y(),abs(wid)-1,abs(hei)-1); grapPix=QPixmap::grabWidget(this,rect);//截取矩形区域内图像 QPoint temp; temp.setX(ev->x()); temp.setY(ev->y()); if(mouseState==true){ showMenu(temp); mouseState=false; } }
我的截取图片的代码主要放在mouseReleaseEvent函数中,含义是鼠标释放时截取感兴趣的区域并保存到一个QPixmap中,至于是否保存到本地,可以在弹出菜单中决定。其中主要的函数是:grapPix=QPixmap::grabWidget(this,rect)该函数会将rect区域中的图片截取下来并保存到QPixmap中。前面的四个if-else语句只是逻辑上的一些判断,以适应从不同方向画出的选区都能够正确的截取出图片(比如,从左上角往右下角画、从右上角网左下角画等等)。
二、如何在属性设置对话框和主界面之间进行参数传递
这部分主要涉及的其实是Qt中如何通过信号传递参数。这个在网上也有较多的介绍。
在这个程序中,需要用到的信号传递参数的地方是,属性设置对话框。当属性设置对话框设置好属性并点击确定的时候,会发送一个信号,该信号在主界面窗口中和某个槽进行绑定,从而实现了参数的设置。
属性对话框头文件定义如下:
//自定义属性设置对话框,模态对话框 //result属性会返回该窗口所点击的按钮的按钮名,只能取值"ok"或"cancle" //imgWidth和imgHeight会保存输入框中输入的值 #ifndef ATTRIBUTEDIALOG_H #define ATTRIBUTEDIALOG_H #include <QWidget> #include <QLabel> #include <QPushButton> #include <QLineEdit> class AttributeDialog : public QWidget { Q_OBJECT public: AttributeDialog(); ~AttributeDialog(); int getImgWidth(); int getImgHeight(); QString getResult(); private: int imgHight,imgWidth; QString result; QString address; QLabel *heightLabel,*widthLabel,*danwei1,*danwei2,*addrLabel; QLabel *note; QPushButton *ok,*cancle,*chooseAddr; QLineEdit *widthLE,*heightLE,*addrLE; void initGui(); int qStringToInt(QString str); private slots: void okSlot(); void cancleSlot(); void chooseAddrSlot(); signals: void finishSignal(QString str,int w,int h,QString addr); }; #endif // ATTRIBUTEDIALOG_H
完整实现如下:
#include "attributedialog.h" #include <QHBoxLayout> #include <QVBoxLayout> #include <QMessageBox> #include <QFileDialog> AttributeDialog::AttributeDialog() { imgHight=; imgWidth=; result="cancle"; address="./images/"; ,); this->setWindowTitle("attribute"); initGui(); this->setWindowModality(Qt::ApplicationModal); connect(ok,SIGNAL(clicked()),this,SLOT(okSlot())); connect(cancle,SIGNAL(clicked()),this,SLOT(cancleSlot())); connect(chooseAddr,SIGNAL(clicked()),this,SLOT(chooseAddrSlot())); } AttributeDialog::~AttributeDialog() { } int AttributeDialog::getImgWidth() { return imgWidth; } int AttributeDialog::getImgHeight() { return imgHight; } QString AttributeDialog::getResult() { return result; } void AttributeDialog::initGui() { note=new QLabel; note->setText("set the size of the picture which\nyou want to save!\n______________________________________"); heightLabel=new QLabel; widthLabel=new QLabel; addrLabel=new QLabel; danwei1=new QLabel; danwei1->setText("px"); danwei1->setFixedWidth(); danwei2=new QLabel; danwei2->setText("px"); danwei2->setFixedWidth(); widthLabel->setText("width:"); widthLabel->setFixedWidth(); heightLabel->setText("height:"); heightLabel->setFixedWidth(); addrLabel->setText("path:"); addrLabel->setFixedWidth(); ok= new QPushButton; cancle=new QPushButton; chooseAddr=new QPushButton; ok->setText("ok"); cancle->setText("cancle"); chooseAddr->setText("..."); chooseAddr->setFixedWidth(); widthLE=new QLineEdit; heightLE=new QLineEdit; addrLE=new QLineEdit; widthLE->setText("); heightLE->setText("); addrLE->setText(address); //从上到下设置三个水平布局管理器 QHBoxLayout *hb1=new QHBoxLayout; QHBoxLayout *hb2=new QHBoxLayout; QHBoxLayout *hb3=new QHBoxLayout; QHBoxLayout *hb4=new QHBoxLayout; hb1->addWidget(widthLabel); hb1->addWidget(widthLE); hb1->addWidget(danwei1); hb2->addWidget(heightLabel); hb2->addWidget(heightLE); hb2->addWidget(danwei2); hb3->addWidget(addrLabel); hb3->addWidget(addrLE); hb3->addWidget(chooseAddr); hb4->addWidget(ok); hb4->addWidget(cancle); //对话框总体布局管理器 QVBoxLayout *vb=new QVBoxLayout; vb->addWidget(note); vb->addLayout(hb1); vb->addLayout(hb2); vb->addLayout(hb3); vb->addLayout(hb4); this->setLayout(vb); } int AttributeDialog::qStringToInt(QString str) { bool ok; int res; res=str.toInt(&ok,); return res; } void AttributeDialog::okSlot() { result="ok"; QString temp1,temp2; temp1=widthLE->text(); temp2=heightLE->text(); if(temp1==NULL||temp2==NULL){ QMessageBox *warning=new QMessageBox; warning->setText("NULL is not allow"); warning->setWindowTitle("warning"); return ; } imgWidth=qStringToInt(temp1); imgHight=qStringToInt(temp2); emit finishSignal("ok",imgWidth,imgHight,address); this->close(); } void AttributeDialog::cancleSlot() { result="cancle"; emit finishSignal("cancle",imgWidth,imgHight,address); this->close(); } void AttributeDialog::chooseAddrSlot() { QString temp=QFileDialog::getExistingDirectory(this, tr("choose save Directory"),"./",QFileDialog::ShowDirsOnly);// if(temp=="") return ;//如果空则直接退出 ]!='\\'){ temp=temp+'\\'; } address=temp; addrLE->setText(address); }
在该定义中可以看到,定义了一个信号叫finishSignal,并且该信号带参数QString、int、int。那么如何将这个信号发送出去呢?当你在某个时刻需要发送信号,只需要一条语句就够了,即:emit+信号名(对应参数)。在该程序中,当在属性设置对话框中点击 ok后,就会发送信号。所以我的信号发送实现如下:
void AttributeDialog::okSlot() { result="ok"; QString temp1,temp2; temp1=widthLE->text(); temp2=heightLE->text(); if(temp1==NULL||temp2==NULL){ QMessageBox *warning=new QMessageBox; warning->setText("NULL is not allow"); warning->setWindowTitle("warning"); return ; } imgWidth=qStringToInt(temp1); imgHight=qStringToInt(temp2); emit finishSignal("ok",imgWidth,imgHight,address);/*信号发送语句*/ this->close(); }
知道了如何发送信号,如何在信号中带上参数。那么如何使用信号中的参数呢?我们先定义一个简单的类(不必要的信息都省略掉了):
class Widget : public QWidget{ Q_OBJECT public: ...private: //该函数用来显示属性设置对话框 void showAttr();private slots: void testSlot(QString str,int w,int h,QString addr); }; //下面是简单的实现void Widget::showAttr(){ AttributeDialog *temp=new AttributeDialog; temp->show(); //这里就是如何使用信号中的参数了,可以发现,只要写出信号中的参数类型就可以了,不需要写出具体的参数 connect(temp,SIGNAL(finishSignal(QString,int,int)),this,SLOT(testSlot(QString,int,int)));} void testSlot(QString str,int w,int h,QString addr){ //在这里,可以直接使用形参 ...}
上面是一个简单的事例,如何使用自定义的信号,如何用connect函数连接带参数信号和槽。
三、小结
该程序最主要的功能还是从视频中截取出样本,并根据需要设置成指定的大小,因为分类器(比如Adaboost分类器)的训练需要归一化的样本。这个程序为我收集样本提供了很大的方便。同时在另一个车牌识别项目中,也需要收集车牌的样本,这个程序又再一次发挥了作用。
代码写得实在太烂,一开始还在考虑要不要贴出这么多的代码。但是想了想,还是贴出来吧。只有让别人批评自己写得不好的地方,才会有更大的进步。代码写出来就是让人看的,让人看得清楚明白了,才是优秀的代码。如果代码让人看得一头雾水,那只能说代码确实太烂。我贴了这么多代码,可能在一定程度上造成了“视觉污染”。但是我渴望的是进步。写得不好的,希望批评指教,谢谢。
原创博客,转载请注明出处:http://www.cnblogs.com/xiongmao-cpp/