这也是之前做的一个项目。主要目的是一个粗的电线里面有三种颜色的线在一起,需要通过机器视觉检测出来相互之间的位置,保证黄色线在最上面。他们有个专门的电机旋转电线,需要给到控制电机的PLC需要旋转的角度。我们主要负责做识别检测部分和上位机操作软件。
硬件主要为摄像头,工控机,串口转MODBUS。PLC控制不是我们负责的,我们只需要通过MODBUS把需要旋转的角度发送过去即可。只知道他们那边的PLC用的是中研五轴十轴模块。
摄像头采用了映美精的GigE彩色工业相机。工控机采用i5处理器的小型工控机。
软件初始时的设计思路如图所示:
这是最初根据需求设计的思路,后面开始野蛮生长。。
下面是初始用GUI design设计的基本GUI界面,后面也完全变了样:
后面又加入了许多新的功能和界面,最终界面最后再展示吧。
我们开发采用界面与算法分开的方式。我主要负责界面和逻辑的编写,几乎就是除了算法的所有部分,而我的同学专心测试算法。上位机软件部分用QT编写,算法的话是opencv,他为了方便先用python的库写好大概,测试没问题后,在对着人工转成C++。
用到的QT版本是5.9.3,opencv是4.1。
通过这个项目确实学习到了很多关于QT和C++的实操。根据代码里面的各个模块值得讲的来讲学习到的东西吧。
因为用到了QT的串口功能,所以要在.pro里加入:
QT += serialbus serialport widgets
还有要用到的管理员权限,后面开机自启,还有关闭能顺便关机都会用到:
#权限 需要以管理员运行creator
QMAKE_LFLAGS += /MANIFESTUAC:\"level=\'requireAdministrator\' uiAccess=\'false\'\" #以管理员运行
QMAKE_LFLAGS += /SUBSYSTEM:WINDOWS,\"5.01\" #VS2013 在XP运行
然后在QT里加入新的模块记得直接项目右击添加新的文件,才会自动导入到.pro里。
还有引入库的方法,因为后面要用到opencv的库。现在本机上装上opencv的包,这里用的4.1版。然后项目右击,添加库,外部库,选择库的路径。完了它会在.pro下自动添加相应代码。但之前也遇到了问题,添加之后还是无法正常编译,于是又查了写方法,手动在.pro里添加了如下代码:
INCLUDEPATH += D:\OPENCV\opencv4.1\build\include
INCLUDEPATH += D:\OPENCV\opencv4.1\build\include\opencv2
CONFIG(debug, debug|release): {
LIBS += -LD:\OPENCV\opencv4.1\build\x64\vc14\lib \
#-lopencv_world410 \
-lopencv_world410d \
} else:CONFIG(release, debug|release): {
LIBS += -LD:\OPENCV\opencv4.1\build\x64\vc14\lib \
-lopencv_world410 \
#-lopencv_world410d \
}
注意release的和debug的一定要分开,不要涂省事都包进去,之前就是这点没区分,调试的时候好好的,想发布一输出release版就报错。
引入的映美精库和modbus库:
LIBS += $$quote(D:\QT\wire\Library\TIS_UDSHL11_x64.lib)
LIBS += $$quote(D:\QT\wire\Library\TIS_UDSHL11d_x64.lib)
win32: LIBS += -L$$PWD/Library/ -llibmodbus-5
改软件的图标,把图标放在代码目录里,如1.icon,加入:
RC_ICONS = 1.ico
主要是自启相关的功能,跟系统相关的交互。之前调试的时候一不小心,同时测了开机自启和自动关机…结果开机就启动了程序然后立马关机,成了一个病毒一样的东西。后来终于找到方法结束罪恶的循环,开机之前进入安全模式,就会没有自启,然后进到软件文件夹里把软件删了。
加入开机自启本来想通过命令行加入参数启动时才开启自启(通过 int main(int argc, char *argv[])),可是试了以下好像没能实现,具体原因也不知道,最后就做成了默认自启。
开机自启(要先#include “windows.h”):
void appAutoRun(bool bAutoRun)
{
QString appName = QApplication::applicationName();//程序名称
QString appPath = QApplication::applicationFilePath();// 程序路径
appPath = appPath.replace("/","\\");
//HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
QSettings *reg=new QSettings("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",QSettings::NativeFormat);
if (bAutoRun)
{
QString val = reg->value(appName).toString();// 如果此键不存在,则返回的是空字符串
if(val != appPath)
reg->setValue(appName,appPath);
}
else
{
reg->remove(appName);
}
reg->deleteLater();
}
防止程序重复启动:
bool checkOnly()
{
// 创建互斥量
HANDLE m_hMutex = CreateMutex(NULL, FALSE, L"fortest_abc123" );
// 检查错误代码
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// 如果已有互斥量存在则释放句柄并复位互斥量
CloseHandle(m_hMutex);
m_hMutex = NULL;
// 程序退出
return false;
}
else
return true;
}
在main函数的最前面加入程序重复的检测,如果返回false,则直接不再执行后面的,return 0。
顺便补习下这部分知识
CreateMutex只是创建了一把锁,作用是找出当前系统是否已经存在指定进程的实例。如果没有则创建一个互斥体。
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 指向安全属性的指针
BOOL bInitialOwner, // 初始化互斥对象的所有者
LPCTSTR lpName // 指向互斥对象名的指针
);
修改IP(需要#include ):
void IpConfig()
{
QProcess *process = new QProcess();
//网卡名称
QString name = "\"以太网\" ";
QString cmd = "netsh interface ipv4 set address name = " + name + "source = static address = 218.192.162.1 ";
qDebug()<<"cmd = "<start(cmd); //执行dos命令
process->waitForFinished(); //等待执行完成
delete process;
}
通过QT里的向命令行输入指令实现。自动关机也是同样的原理。
主逻辑界面如图:
这里要先说个教训,之前没什么经验,有些类实例化命名的方式太不规范,导致后面分不清谁是谁,无形中增加了之后工作的难度。所以以后项目中一定要吸取教训,命名能让人看懂大致意思,还有写好注释,面得过段时间再看到时根本想不起是什么。
因为工控机只使用这个软件不作他用,所以界面全屏始终在最上方,并且加入电子时钟。
不再列代码了,后面会上传到资源列表和github,主要列一下实现的功能和有用的技术点。
1. 窗口全屏且置顶
2. 相机未打开时那块区域黑色背景(QPalette实现)
3. 电子时钟(QTimer、QLCDNumber实现)
4. 不断跳转的检测数量和历史检测数(通过读写本地配置文件和检测算法的信号槽实现)
最关键的技术点还是信号槽吧,也算是QT的精髓了
这里主要讲两个吧,第一个是范例,在ui界面的定义的按钮,点击打开对应窗口的:
connect(ui->pushButton_3, SIGNAL(clicked(bool)), this, SLOT(debug()));
参数1是ui里的按钮对象,参数2信号点击,参数3本界面的对象,slot槽函数,在.h里的public slots或private slots要提前定义,后面当成本类的成员方法函数写出来。
第二个是在别的窗口或类里抛出了信号量(注意,信号量要定义在抛出信号量的类里 如
public:
signals:
void finish(int);
void no_roi();)
需要发出信号量时,在对应地方:
emit finish(int);
如果有参数的,信号槽要这样写:
connect(Listener1::Instance(), SIGNAL(finish(int)), this, SLOT(show1(int)));;
在本类里接收做出反应:
connect(&w3, SIGNAL(grab_signal()), this, SLOT(grab1()));
参数1是要在本类.h里声明发出信号的对应类的对象,这个类至少要继承于QObject,并在类里定义signals: void grab_signal(); 把信号定义在里面。参数2是发出信号的名称,参数3本界面的对象,参数4对应的槽函数。
主要是实现读取本地图片形成列表,通过存图时命名显示出旋转角度和哪个相机所拍等信息。
实现功能:
1. 列表(通过ui->qTableWidget、QTableWidgetItem实现,qTableWidget真的很方便)
2. 排序(QHeaderView实现)
3. 列表上双击打开和右键菜单(doubleClicked(QModelIndex) 信号槽 和 customContextMenuRequested(QPoint) 信号槽)
4. 读文件(QImage load实现)
5. 列表下一页(verticalScrollBar)
专门存储的数据类,读写数据到本地。关键点是QSettings,这点列代码吧。
写:
QSettings ini(filepath, QSettings::IniFormat);
ini.beginGroup(QString("System_Data"));
QString key;
key.sprintf("password");
ini.setValue(key, p);
key.sprintf("deflection");
ini.setValue(key, deflection1);
ini.endGroup();
读:
QString key;
QSettings ini(filepath, QSettings::IniFormat);
ini.beginGroup(QString("System_Data"));
key.sprintf("password");
password = ini.value(key).toString();
key.sprintf("deflection");
deflection = ini.value(key).toString();
ini.endGroup();
虚拟键盘,有些地方值得写一下:
改变焦点信号槽
//绑定全局改变焦点信号槽
connect(qApp, SIGNAL(focusChanged(QWidget *, QWidget *)),
this, SLOT(focusChanged(QWidget *, QWidget *)));
QT的提供了很多全局的信号,在程序的任何位置都可以接收处理
当系统焦点发生改变的时候,就会发出focusChanged信号,系统内其他程序都可以接收这个信号
定义槽函数,如
void Keyboard::FocusChanged(QWidget *, QWidget *nowWidget)
{
if (nowWidget->inherits("QLineEdit"))
{
... ...
}
}
可以在槽函数里判断,过滤想要的控件。
通过全局改变焦点信号槽识别出当前指向的是否为输入栏。
事件过滤器
这里有两篇应用参考:
https://jingyan.baidu.com/article/a378c960c9003bb32928304b.html
https://blog.csdn.net/wang13342322203/article/details/81532207
在监测的代码里执行需要的行为. 这可以用event Filter来达到. 设置一个event filter有两个步骤:
通过事件过滤器识别出鼠标的按下而释放从而在输入栏旁边弹出虚拟键盘,还有捕捉输入栏不是焦点窗口时关闭虚拟键盘。
还用一个语法foreach,用于遍历按键是按钮列表里的哪一个,如:
QList btn = this->findChildren();
foreach (QPushButton * b, btn) {
connect(b, SIGNAL(clicked()), this, SLOT(btn_clicked()));
}
foreach (varItem , Items) // foreach(variable ,container)
类似于C++11里的 for(i:range)
其中btn_clicked()还用到了一个关键字sender(),指信号槽发送者。是QT自带的方法。
QPushButton *btn = (QPushButton *)sender();
相机类,通过映美精官方相机的SDK编写,用于操作相机的相关功能。由于都是根据官方开发的,官方那部分就不再细讲,主要讲一下自己开发的部分几个点:
1. 单例模式
因为相机只有一个,不同对象间数据应该共享同一个,不应该不同地方创建不同对象所指的相机数据不同。所以应使用单例模式,保证对象始终是一个。后面才发现可以直接不创建对象,在别的地方对类进行操作。
在public里直接以static创建类名:
//实现单例模式,即使不创建对象仍能够使用 保证了不同类之间使用了同一个对象,便利于信号与槽的传递,与主界面逻辑的沟通
static TIS_Camera *Instance(){
if (!_instance) {
_instance = new TIS_Camera;
}
return _instance;
}
在private里定义:
static TIS_Camera *_instance; //实例对象
重要的,在cpp里开头也要定义,不然会报错:
TIS_Camera *TIS_Camera::_instance = 0;
2. 显示相机信号用套接字而不是不停截图显示
之前不知道怎么把相机的内容实时显示在界面上,用了个方法,不断的抓图然后显示,估计一秒钟三十下吧,结果自己的电脑运行起来都非常卡,只能另寻他法。刚巧在网上找到了用HWND窗口句柄的方法。
在Windows中,句柄是一个系统内部数据结构的引用。例如当你操作一个窗口,或说是一个Delphi窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号窗口,就此你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口最小化等等。
换句话说,句柄是一种内部代码,通过它能引用受系统控制的特殊元素,如窗口、位图、图标、内存块、光标、字体、菜单等。
定义时,将要显示相机内容的QWidget传入,将本身相机的句柄赋予QT里布局好的句柄:
void TIS_Camera::Camera(QWidget *win){
HWND appwnd;
appwnd = (HWND)win->winId();
// Set the window that should display the live video.
Grab1.setHWND(appwnd);//
}
使用时:
cam.Camera(ui->widget_2);
3. CVmat转QImage
用来把opencv的图片格式转换为QT的图片格式:
QImage TIS_Camera::cvMat2QImage(const cv::Mat& mat, bool clone, bool rb_swap)
{
const uchar *pSrc = (const uchar*)mat.data;
// 8-bits unsigned, NO. OF CHANNELS = 1
if (mat.type() == CV_8UC1)
{
//QImage image(mat.cols, mat.rows, QImage::Format_Grayscale8);
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8);
if (clone) return image.copy();
return image;
}
// 8-bits unsigned, NO. OF CHANNELS = 3
else if (mat.type() == CV_8UC3)
{
// Create QImage with same dimensions as input Mat
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888);
if (clone)
{
if (rb_swap) return image.rgbSwapped();
return image.copy();
}
else
{
if (rb_swap)
{
cv::cvtColor(mat, mat, CV_BGR2RGB);
}
return image;
}
}
else if (mat.type() == CV_8UC4)
{
qDebug() << "CV_8UC4";
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32);
if (clone) return image.copy();
return image;
}
else
{
qDebug() << "ERROR: Mat could not be converted to QImage.";
return QImage();
}
}
注意opencv图片也有格式区别,之前不知道,图片一只显示不出来,后来才发现是CV_8UC4的问题。
openCV的类型可以阅读以下内容:CV_8U:无符号整型8位,C4:四个信道。
4. 相机回调里的try…catch
之前算法老出问题,一旦出问题就会导致主界面崩溃,调试的时候都直接崩溃。这显然不是我们想看到的,所以引入了try.catch异常检测机制,一方面能在出错的时候弹出对话框,增加程序的稳定性,另一方面也能方便排查到底是哪里出了问题。
使用方法是在调用每一个可能出现问题的函数前先try把他们包裹进去,再在后面catch写出可能出现问题的类型,然后throw。或者某些判断之后直接throw。
接着在外边调用这个类的大方法前也加入try,catch()里写需要捕捉的错误代码,然后{}里处理。用这个方法结合信号槽和对话框确实有效解决了程序崩溃的问题。
其他部分涉及的技术点基本都有些重复,不再展开写了,或许后续想到后再补充。
补充:还有个非常好用的一键把程序所需要的dll打包拉去的脚本:
for /r "%cd%" %%i in (*.exe) do (
D:\QT\Qt5.9.3\5.9.3\msvc2015_64\bin\windeployqt.exe "%%~nxi")
pause
:: 以::表示注释 该批处理语句 云鬟查找当前目录下exe文件名 执行 windeployqt 复制dll 自行修改相应目录
本次项目基本掌握了QT程序的开发流程,熟悉了C++的语法,以及一些协作开发工具的使用,比如git(主要用的TortoiseGit,真是超级好用),腾讯工蜂。也仔细思考了多人协作开发的相关流程,如何分别拉去创建分支和合并,如何协调开发和修BUG等等。算是收获颇丰,也意识到一个人的精力是有限的,一直编一会儿代码都会很容易疲惫,需要协调好码代码和休息的时间。还有深刻理解了那句话,程序应该百分之八十的时间用来设计,百分之二十的时间把设计转化为代码。而不是一头扎进去就是写,写之前把一切想清楚,设计好,才是真正有效率的编程方式。
资源地址:
github地址:https://github.com/ypg666/Wire_Rotate