深度学习图像分拣是个比较繁琐的工作,windwos文件夹预览图像放大倍数有限,而单独地查看图像来回切换各个文件夹很容易乱。因此在空闲尝试做了个小软件。
一、功能需求
- 具备类似windows图像查看软件的缩放,拖拽,快捷键切换功能;
- 具备一键分拣,即快捷键分拣到对应文件夹功能;
- 后续测试中发现快捷键分拣很容易手滑,因此加入了后撤和重做。
二、软件平台
QT5.12.0/C++/OPENCV3.4.0
三、UI界面
主界面包括三个菜单,一图像窗口,一下拉页,一日志窗口。
3.1 菜单UI
3.1.1 打开图像路径
功能:
- 打开包含图像的文件夹,读取所有图片后缀文件列表;
- 显示第一张。
代码:
QString openFile = QFileDialog::getExistingDirectory(this, "choose src Directory", "/");
openFile.toLocal8Bit().constData();
QStringList imageList; QString ImgPath; QDir imgDir ;
if (openFile != NULL)
{
ImgPath = openFile;
imgDir.setPath(ImgPath);
imageList << "*.bmp" << "*.jpg" << "*.png" << "*.tif";
imgDir.setNameFilters(imageList);
imgCount = imgDir.count();
if (imgCount == 0)
{
return;
}
else
{
QString imageName = ImgPath + "/" + imgDir[0];
matOpen = imread(imageName.toLocal8Bit().toStdString());
}
else
{
return;
}
3.1.2 打开图像路径
功能: 选择/创建主路径文件夹。
代码:
QString openFile = QFileDialog::getExistingDirectory(this, "choose src Directory", "/");
openFile.toLocal8Bit().constData();
if (openFile != NULL)
{
MainPath = openFile;
}
else
{
reutn;
}
3.1.3 创建子类路径
界面:
功能:输入需要分拣的图像类别,’,'分隔,然后根据类别名称新建子类文件夹。
代码:
//输入类以','分割
//vector <string> classes 类别文件夹名
string tmpClass = ui>lineEdit_classes>text().toLocal8Bit().toStdString();
char *input = (char *)tmpClass.c_str();
char *token = std::strtok(input, ",");
classes.clear();
while (token != NULL)
{
classes.push_back(token);
token = std::strtok(NULL, ",");
}
//在主路径下创建以子类名称命名的文件夹
if (MainPath != NULL)
{
for (int i = 0; i < classes.size(); i++)
{
QString tmpClassPath = MainPath + '/' + QString::fromStdString(classes[i]);
QDir *folder = new QDir;
folder->mkdir(tmpClassPath);
}
}
3.2 编辑UI
3.2.1 撤销/重做
功能:
- 撤销即取消图像移动操作;
- 重做即取消撤销操作;
- 清空日志窗口。
代码:
//当前图像源路径和目标路径添加到hisProcess,撤销即删除已经移动的图像,
//his_count+1,重做即做反操作,his_count-1;
//清空日志不做赘述。
//撤销操作
//int his_count; //历史操作计数
//vector <QStringList> hisProcess; //历史记录
if (hisProcess.size() != his_count)
{
QFile::remove(hisProcess.at(hisProcess.size() - his_count - 1).last());
his_count += 1;
}
else
{
return;
}
//重做操作
if (his_count != 0)
{
QFile::copy(hisProcess.at(hisProcess.size() - his_count).first(), hisProcess.at(hisProcess.size() - his_count).last());
his_count -= 1;
}
else
{
return;
}
3.3 提示menu
提示功能不做赘述,按需自写。
3.4 图像显示UI
图像显示部分起初只是一个单纯的QLabel来显示图像,后来在使用中发现还是有很多不方便,包括显示分拣classes信息(在textbrowser中无法置顶),单独的翻页按钮比较丑等。于是 对QLabel进行了修改。
主要包括以下几个功能:
- 显示图像;
- 点击左右边界附近切换上下张图;
- 图像的拖拽,缩放;
- 右键菜单实现翻页、保存。
3.4.1 显示图像
这个就不做赘述了。
3.4.2 点击左右边界附近切换上下张图
从类视图可以看到,imgShow 包含了两个按钮和一个显示ClassFile的QLabel。QT是不允许控件的叠加布局的,查阅了很多,都是建议重写布局方案,感觉太过繁琐。https://www.cnblogs.com/ybqjymy/p/13578255.html 提供了一种通过QWidgets布局的叠加控件方案,我的方案也是在此基础上进行。
-
新建一个QWidgets,添加一个QLabel和两个PushButton。两个PushButton先做水平布局,然后再跟QLabel做垂直布局,最后选择QWidgets的栅格布局,效果如图;
-
调整垂直布局和水平布局比例;
-
按钮删去所有文字,尺寸水平和垂直方向选择perferred(不删文字按钮水平方向无法最小化),背景透明设置 background:transparent,两个按钮中间加入若干spacer布局;
-
右键打开*.ui文件,找到新建的QWidget,class修改为QLabel,回到VS后,点击*.ui,重新编译(若使用QT自带的开发,回到软件后会提示已修改,是否重新加载,确认即可),到此widget修改的label已经可以显示图像了。
3.4.3 图像的拖拽,缩放
功能:
- 鼠标点击拖动图像;
- ‘ctrl’+滚轮缩放图像,右键还原;
- 代码参考了很多类似例子,方法可以参考:https://blog.csdn.net/Viciower/article/details/97648437.
代码:
zoomLabel.h
#include <QObject>
#include <QLabel>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QPainter>
#include <QDebug>
#include <QPixmap>
#include <iostream>
class my_zoomLabel :public QLabel
{
public:
my_zoomLabel(QWidget *parent = nullptr);
~my_zoomLabel();
void initPos();
void recievePix(QPixmap *pixmap);
protected:
void paintEvent(QPaintEvent *) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void mousePressEvent(QMouseEvent *ev) override;
void mouseReleaseEvent(QMouseEvent *ev) override;
void wheelEvent(QWheelEvent *event) override;
void changeWheelValue(QPoint event, int value);
private:
double m_scaleValue; //图片缩放倍数
QPointF m_drawPoint; //绘图起点
QPointF m_mousePoint; //鼠标当前位置点
QRect m_rectPixmap; //被绘图片的矩形范围
bool m_isMousePress; //鼠标是否按下
QPixmap pix; //作为图元显示的图片
const double EPS = 1e-6; //双精度浮点数比较
const double SCALE_VALUE = 0.1;
const double SCALE_MAX_VALUE = 10.0;
const double SCALE_MIN_VALUE = 0.2;
};
zoomLabel.cpp
#include "my_zoomLabel.h"
my_zoomLabel::my_zoomLabel(QWidget *parent) :QLabel(parent)
{
m_scaleValue = 1.0;
m_mousePoint = QPointF(0, 0);
m_drawPoint = QPointF(0, 0);
m_rectPixmap = QRect(0, 0, 0, 0);
m_isMousePress = 0;
}
my_zoomLabel::~my_zoomLabel()
{}
void my_zoomLabel::paintEvent(QPaintEvent *)
{
QPainter painter(this);
double width = this->width()*m_scaleValue;
double height = this->height()*m_scaleValue;
if (!pix.isNull())
{
QPixmap scalePixmap = pix.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); // 饱满缩放
m_rectPixmap = QRect(m_drawPoint.x(), m_drawPoint.y(), width, height); // 图片区域
painter.drawPixmap(m_rectPixmap, scalePixmap);
}
}
void my_zoomLabel::mouseMoveEvent(QMouseEvent *event)
{
if (m_isMousePress)
{
int x = event->pos().x() - m_mousePoint.x();
int y = event->pos().y() - m_mousePoint.y();
m_mousePoint = event->pos();
m_drawPoint = QPointF(m_drawPoint.x() + x, m_drawPoint.y() + y);
update();
}
}
void my_zoomLabel::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_isMousePress = true;
m_mousePoint = event->pos();
}
}
void my_zoomLabel::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton)
{
m_drawPoint = QPointF(0, 0);
m_scaleValue = 1.0;
update();
}
if (event->button() == Qt::LeftButton) m_isMousePress = false;
}
void my_zoomLabel::wheelEvent(QWheelEvent *event)
{
if (event->modifiers() == Qt::ControlModifier) //ctrl+滚轮缩放
{
int numDegrees = event->delta() / 8; // 滚动角度 - *8就是鼠标滚动的距离
int numSteps = numDegrees / 15; // 滚动步数 - *15就是鼠标滚动的角度
changeWheelValue(event->pos(), numSteps);
event->accept();
}
}
void my_zoomLabel::changeWheelValue(QPoint event, int numSteps)
{
m_scaleValue += numSteps * SCALE_VALUE;
if (m_scaleValue > (SCALE_MAX_VALUE + EPS))
{
m_scaleValue = SCALE_MAX_VALUE;
return;
}
if (m_scaleValue < (SCALE_MIN_VALUE - EPS))
{
m_scaleValue = SCALE_MIN_VALUE;
return;
}
if (m_rectPixmap.contains(event))
{
double x = m_drawPoint.x() - (event.x() - m_drawPoint.x()) / m_rectPixmap.width()*(this->width()*SCALE_VALUE)*numSteps;
double y = m_drawPoint.y() - (event.y() - m_drawPoint.y()) / m_rectPixmap.height()*(this->height()*SCALE_VALUE)*numSteps;
m_drawPoint = QPointF(x, y);
}
else
{
double x = m_drawPoint.x() - (this->width()*SCALE_VALUE)*numSteps / 2;
double y = m_drawPoint.y() - (this->height()*SCALE_VALUE)*numSteps / 2;
m_drawPoint = QPointF(x, y);
}
update();
}
void my_zoomLabel::initPos()
{
//初始化定位点
m_scaleValue = 1.0;
m_drawPoint = QPointF(0, 0);
}
void my_zoomLabel::recievePix(QPixmap * pixmap)
{
pix = *pixmap;
}
3.4.4 右键菜单实现翻页、保存
右键菜单通过代码添加,所以没法截图…
功能:包括上一页、下一页、保存图像。
代码:
//声明
QMenu* imgShow_Menu; //显示窗口右键菜单
QAction* imgShow_Action01;
QAction* imgShow_Action02;
QAction* imgShow_Action03;
//图像窗口右键菜单
imgShow_Action01 = new QAction(QString::fromLocal8Bit("上一张"), this);
imgShow_Action02 = new QAction(QString::fromLocal8Bit("下一张"), this);
imgShow_Action03 = new QAction(QString::fromLocal8Bit("保存图像"), this);
imgShow_Menu = new QMenu(this);
imgShow_Menu->addAction(imgShow_Action01);
imgShow_Menu->addAction(imgShow_Action02);
imgShow_Menu->addAction(imgShow_Action03);
connect(ui.label_imgShow, &QLabel::customContextMenuRequested, this, &imgQuickView::imgShow_Menu_Action01);
connect(imgShow_Action01, &QAction::triggered, this, &imgQuickView::action_preImg);
connect(imgShow_Action02, &QAction::triggered, this, &imgQuickView::action_nextImg);
connect(imgShow_Action03, &QAction::triggered, this, &imgQuickView::action_saveImg);
//上一张
if (imgIndex > 0 && imgIndex <= imgCount && imgDir[0] != NULL)
{
imgIndex -= 1;
QString preImageName = ImgPath + "/" + imgDir[imgIndex];
matOpen = imread(preImageName.toLocal8Bit().toStdString());
imgShow(&matOpen);
}
else
{
return;
}
//下一张
if (imgIndex < imgCount - 1 && imgDir[0] != NULL)
{
imgIndex += 1;
QString nextImageName = ImgPath + "/" + imgDir[imgIndex];
matOpen = imread(nextImageName.toLocal8Bit().toStdString());
imgShow(&matOpen);
}
else
{
return;
}
//保存图像
if (matOpen.empty())
{
return;
}
else
{
QString tmpPath = QFileDialog::getExistingDirectory(this, "choose src Directory", "/");
QString imgSavePath = tmpPath + "/" + QDateTime::currentDateTime().toString("yy_MM_dd_hh_mm_ss") + ".bmp";
cv::imwrite(imgSavePath.toStdString(), matOpen);
}
3.4 日志窗口
日志窗口采用的textBrowser控件,textBrowser.append()添加所需即可。
四、快捷键
功能:根据输入的class数量,绑定1-9数字键和指定文件夹。
代码:
//移动图像到指定文件夹
void moveImg(int i)
{
//复制原图到项目文件夹
sourceImg = ImgPath + "/" + imgDir[imgIndex];
tagImg = MainPath + '/' + QString::fromStdString(classes[i]) + "/" + imgDir[imgIndex];
QFile::copy(sourceImg, tagImg);
//历史记录
//QStringList.first放源路径,QStringList.last放目标路径
QStringList tmpQList;
tmpQList.append(sourceImg);
tmpQList.append(tagImg);
hisProcess.push_back(tmpQList);
his_count = 0;
}
void keyPressEvent(QKeyEvent * ev)
{
//撤销操作
if (ev->key() == Qt::Key_Z && ev->modifiers() == Qt::ControlModifier)
{
action_cancel();
}
else if (ev->key() == Qt::Key_Y && ev->modifiers() == Qt::ControlModifier)
{
action_redo();
}
//对应1-9移动图像到指定文件夹操作
if (!classes.empty())
{
//根据classes数量,将bool数组前n位置1,后续捕捉键盘事件加入bool标志位判断,
//即根据输入class的数量激活对应键盘快捷键
int classNum = classes.size();
for (int i = 0; i < classNum; i++) { keyBool[i] = 1; };
if (ev->key() == Qt::Key_1)
{
if (keyBool[0]) { moveImg(0); }
else { return; }
}
else if (ev->key() == Qt::Key_2)
{
if (keyBool[1]) { moveImg(1); }
else { return; }
}
else if (ev->key() == Qt::Key_3)
{
if (keyBool[2]) { moveImg(2); }
else { return; }
}
else if (ev->key() == Qt::Key_4)
{
if (keyBool[3]) { moveImg(3); }
else { return; }
}
else if (ev->key() == Qt::Key_5)
{
if (keyBool[4]) { moveImg(4); }
else { return; }
}
else if (ev->key() == Qt::Key_6)
{
if (keyBool[5]) { moveImg(5); }
else { return; }
}
else if (ev->key() == Qt::Key_7)
{
if (keyBool[6]) { moveImg(6); }
else { return; }
}
else if (ev->key() == Qt::Key_8)
{
if (keyBool[7]) { moveImg(7); }
else { return; }
}
else if (ev->key() == Qt::Key_9)
{
if (keyBool[8]) { moveImg(8); }
else { return; }
}
}
}
第一次发帖,大佬们轻拍。源码编辑过加密了,so~。 不过关键的思路应该都有了。然后我重写的QLabel 缩放、拖拽都一卡一卡的,不知道哪块有问题,有优化思路的可以留言沟通下。