Python PyQt LabCD 实现

Created on: September 27, 2024

Answered using OpenAI 01 Mini by OpenAI01

Question

使用python和pyqt实现以下代码:
#include <iostream>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QDockWidget>
#include <QGraphicsView>
#include <QAction>
#include <QString>
#include <QDesktopServices>
#include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#include <QColorDialog>
#include <QFileDialog>
#include <QInputDialog>
#include <QtConcurrent/qtconcurrentrun.h>
#include <opencv2/opencv.hpp>
#include "labcd.h"
#include "utils/fileworker.h"
#include "utils/imgpress.h"
#include "widgets/annotationview.h"

LabCD::LabCD(QWidget* parent)
: QMainWindow(parent)
{
setting = new QSettings("./configs/setting.ini", QSettings::IniFormat);
isCN = setting->value("language").toString() != "EN";

/* 状态栏 */
QStatusBar* lcdStatusBar = statusBar();
messageState = new QLabel("", this); // 用于显示消息
QLabel* messageLocal = new QLabel("", this); // 用于显示坐标
lcdStatusBar->addWidget(messageState);
lcdStatusBar->addPermanentWidget(messageLocal);

/* 菜单栏 */
QMenuBar* lcdMenuBar = menuBar();
QMenu* fileMenu = new QMenu(tr("文件"), this);
QAction* opensAct = fileMenu->addAction(
QIcon(":/menu/resources/Folder.png"), tr("打开文件夹"));
opensAct->setShortcut(QKeySequence("Ctrl+O"));
connect(opensAct, &QAction::triggered, this, &LabCD::openDir);
fileMenu->addSeparator();
QAction* splitAct = fileMenu->addAction(
QIcon(":/menu/resources/Split.png"), tr("切分大图"));
splitAct->setShortcut(QKeySequence("Ctrl+B"));
connect(splitAct, &QAction::triggered, this, &LabCD::openBigImageFile);
QAction* mergeAct = fileMenu->addAction(
QIcon(":/menu/resources/Merge.png"), tr("合并大图"));
mergeAct->setShortcut(QKeySequence("Ctrl+M"));
connect(mergeAct, &QAction::triggered, this, &LabCD::mergeBigImage);
QAction* clearAct = fileMenu->addAction(
QIcon(":/menu/resources/ClearMask.png"), tr("清理空白标签"));
connect(clearAct, &QAction::triggered, this, &LabCD::clearEmptyMask);
QAction* convertAct = fileMenu->addAction(
QIcon(":/menu/resources/Convert.png"), tr("从标签建立标注"));
connect(convertAct, &QAction::triggered, this, &LabCD::convertMask2Json);
lcdMenuBar->addMenu(fileMenu);
QMenu* aboutMenu = new QMenu(tr("关于"), this);
QAction* githubAct = aboutMenu->addAction(
QIcon(":/menu/resources/Github.png"), tr("github主页"));
connect(githubAct, &QAction::triggered, [=]() {
QDesktopServices::openUrl(QUrl("https://github.com/geoyee/LabCD"));
});
QAction* helpAct = aboutMenu->addAction(
QIcon(":/menu/resources/Help.png"), tr("使用帮助"));
connect(helpAct, &QAction::triggered, [=]() {
QString tutorial = "https://github.com/geoyee/LabCD/tree/develop/docs/Usage_tutorial.md";
if (!isCN)
tutorial = "https://github.com/geoyee/LabCD/tree/develop/docs/Usage_tutorial_en.md";
QDesktopServices::openUrl(QUrl(tutorial));
});
helpAct->setShortcut(QKeySequence("Ctrl+H"));
lcdMenuBar->addMenu(aboutMenu);
QMenu* languMenu = new QMenu(tr("语言"), this);
QAction* setLangeAct;
if (isCN)
setLangeAct = languMenu->addAction(
QIcon(":/menu/resources/English.png"), tr("英文"));
else
setLangeAct = languMenu->addAction(
QIcon(":/menu/resources/Chinese.png"), tr("中文"));
connect(setLangeAct, &QAction::triggered, [=]() {
if (isCN)
{
setLangeAct->setIcon(QIcon(":/menu/resources/Chinese.png"));
setLangeAct->setText(tr("中文"));
setting->setValue("language", "EN");
}
else
{
setLangeAct->setIcon(QIcon(":/menu/resources/English.png"));
setLangeAct->setText(tr("英文"));
setting->setValue("language", "CN");
}
isCN = !isCN;
QMessageBox::information(
parent,
QObject::tr("提示"),
QObject::tr("重启软件后更新语言设置。")
);
});
lcdMenuBar->addMenu(languMenu);

/* 绘图界面 */
drawCanvas = new MultCanvas(this);
connect(drawCanvas->t1Canva->aView, &AnnotationView::mousePosChanged,
[=](double x, double y) {
messageLocal->setText(
tr("当前坐标:") + \
QString::fromStdString(std::to_string(x)) + ", " + \
QString::fromStdString(std::to_string(y)));
});
connect(drawCanvas->t2Canva->aView, &AnnotationView::mousePosChanged,
[=](double x, double y) {
messageLocal->setText(
tr("当前坐标:") + \
QString::fromStdString(std::to_string(x)) + ", " + \
QString::fromStdString(std::to_string(y)));
});
setCentralWidget(drawCanvas);

/* 图像文件列表 */
QDockWidget* filesDock = new QDockWidget(tr("数据列表"), this);
filesDock->setMinimumWidth(200);
filesDock->setAllowedAreas(Qt::RightDockWidgetArea);
fListWidget = new FileList(this);
// 保存图像
connect(fListWidget, &FileList::saveLastFileRequest, this, &LabCD::save);
// 加载图像
connect(fListWidget, &FileList::FileClickRequest,
[=](QString t1Path, QString t2Path, QString jsonPath) {
drawCanvas->loadImages(t1Path, t2Path, jsonPath);
updatePolysColor();
QFileInfo fileInfo(t1Path);
fileName = fileInfo.fileName();
messageState->setText(tr("加载图像:") + t1Path);
});
filesDock->setWidget(fListWidget);
addDockWidget(Qt::RightDockWidgetArea, filesDock);

/* 标签列表 */
QDockWidget* labelsDock = new QDockWidget(tr("标签列表"), this);
labelsDock->setMinimumWidth(200);
labelsDock->setAllowedAreas(Qt::RightDockWidgetArea);
labTableWidget = new LabelTable(this);
labelsDock->setWidget(labTableWidget);
connect(labTableWidget, &LabelTable::labelSelected, [=](Label* nowLabel) {
drawCanvas->labelSelected(nowLabel);
messageState->setText(tr("当前标签:[") + \
QString::fromStdString(std::to_string(nowLabel->getIndex())) + \
"] " + nowLabel->getName());
});
connect(labTableWidget, &LabelTable::colorChanged,
[=](int labelIndex, QColor newColor) {
// 更新界面上的多边形颜色
for (int i = 0; i < drawCanvas->t1Canva->aScene->polygonItems.count(); ++i)
{
if (drawCanvas->t1Canva->aScene->polygonItems[i]->labelIndex == \
labelIndex)
{
drawCanvas->t1Canva->aScene->polygonItems[i]->setColor(
newColor, newColor);
drawCanvas->t2Canva->aScene->polygonItems[i]->setColor(
newColor, newColor);
}
drawCanvas->t1Canva->aScene->setColor(newColor, newColor);
drawCanvas->t2Canva->aScene->setColor(newColor, newColor);
}
});
// 同步Json的加载
connect(drawCanvas->t1Canva, &Canvas::addJsonPoly, \
labTableWidget, &LabelTable::changeLabelDuotoAddPolyJson);
addDockWidget(Qt::RightDockWidgetArea, labelsDock);

/* 工具栏 */
QToolBar* lcdToolBar = new QToolBar(this);
QAction* saveAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Save.png"), tr("保存"));
saveAct->setShortcut(QKeySequence("Ctrl+S"));
connect(saveAct, &QAction::triggered, this, &LabCD::save);
lcdToolBar->addSeparator();
QAction* lastAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Last.png"), tr("上一张"));
connect(lastAct, &QAction::triggered, [=]() {
save();
fListWidget->gotoLastItem();
});
lastAct->setShortcut(QKeySequence("S"));
QAction* nextAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Next.png"), tr("下一张"));
nextAct->setShortcut(QKeySequence("F"));
connect(nextAct, &QAction::triggered, [=]() {
save();
fListWidget->gotoNextItem();
});
lcdToolBar->addSeparator();
QAction* enlargeAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Enlarge.png"), tr("放大"));
connect(enlargeAct, &QAction::triggered, [=]() {
drawCanvas->t1Canva->aView->scaleZoom(1.1); // 自动同步t2
});
QAction* narrowAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Narrow.png"), tr("缩小"));
connect(narrowAct, &QAction::triggered, [=]() {
drawCanvas->t1Canva->aView->scaleZoom(0.9); // 自动同步t2
});
QAction* fullAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Full.png"), tr("全幅缩放"));
fullAct->setShortcut(QKeySequence("Ctrl+F"));
connect(fullAct, &QAction::triggered, [=]() {
if (drawCanvas->imageWidth != 0 && drawCanvas->imageHeight != 0)
drawCanvas->t1Canva->resetZoom(
drawCanvas->imageWidth, drawCanvas->imageHeight); // 自动同步t2
});
lcdToolBar->addSeparator();
QAction* delPolyAct = lcdToolBar->addAction(
QIcon(":/tools/resources/DeletePolygon.png"), tr("删除多边形"));
connect(delPolyAct, &QAction::triggered, [=]() {
int f1Index = drawCanvas->t1Canva->aScene->findFocusPolygon();
int f2Index = drawCanvas->t2Canva->aScene->findFocusPolygon();
int delIndex = f1Index > f2Index ? f1Index : f2Index;
drawCanvas->t1Canva->aScene->delPoly(delIndex);
drawCanvas->t2Canva->aScene->delPoly(delIndex);
});
delPolyAct->setShortcut(QKeySequence("Backspace"));
QAction* delAllPolysAct = lcdToolBar->addAction(
QIcon(":/tools/resources/DeleteAllPolygons.png"), tr("删除所有多边形"));
connect(delAllPolysAct, &QAction::triggered, [=]() {
drawCanvas->t1Canva->aScene->removeAllPolygons();
drawCanvas->t2Canva->aScene->removeAllPolygons();
});
delAllPolysAct->setShortcut(QKeySequence("Delete"));
lcdToolBar->addSeparator();
QAction* crossColorAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Color.png"), tr("设置十字丝颜色"));
connect(crossColorAct, &QAction::triggered, this, &LabCD::setCrossPenColor);
lcdToolBar->addSeparator();
QAction* isCVAAct = lcdToolBar->addAction(
QIcon(":/tools/resources/Reference.png"), tr("打开变化参考图"));
isCVAAct->setCheckable(true);
// 完成
lcdToolBar->setMovable(false);
addToolBar(Qt::LeftToolBarArea, lcdToolBar);

/* 变化参考图 */
QDockWidget* refDock = new QDockWidget(tr("光谱变化向量强度参考图"), this);
refDock->setAllowedAreas(Qt::NoDockWidgetArea);
QLabel* imgRef = new QLabel(this);
refDock->setFloating(true);
refDock->hide();
connect(drawCanvas, &MultCanvas::addimgDiff, [=](cv::Mat imgDiff) {
if (isCVAAct->isChecked())
{
refNewHeight = refNewWidth * imgDiff.rows / imgDiff.cols;
cv::cvtColor(imgDiff, imgDiff, cv::COLOR_RGB2BGR);
cv::resize(imgDiff, imgDiff, cv::Size(refNewWidth, refNewHeight));
QImage qimg = QImage(
(const uchar*)(imgDiff.data), imgDiff.cols, imgDiff.rows,
imgDiff.cols * imgDiff.channels(), QImage::Format_RGB888
);
imgRef->setPixmap(QPixmap::fromImage(qimg));
refDock->setFixedSize(refNewWidth, refNewHeight);
refDock->show();
}
});
refDock->setWidget(imgRef);
addDockWidget(Qt::NoDockWidgetArea, refDock);

/* 界面设置 */
resize(1200, 600);
setWindowTitle(tr("LabCD - 遥感变化检测标注工具"));
setWindowIcon(QIcon(":/main/resources/Icon.png"));
}

LabCD::~LabCD()
{

}

void LabCD::openDir()
{
QStringList t1List;
QStringList t2List;
if (FileWorker::openImageDir(&t1List, &t2List, nullptr, this))
{
// 新建保存目录
QFileInfo fileInfo(t1List.at(0));
savePath = fileInfo.path();
savePath = savePath.replace("\\", "/");
savePath = savePath.section("/", 0, -2);
QString saveImgPath = savePath + "/GT";
FileWorker::createFolder(saveImgPath);
// 加载已有标签
QString jsonPath = savePath + "/label.json";
QFileInfo jsonFileInfo(jsonPath);
if (jsonFileInfo.isFile())
labTableWidget->importLabelFromFile(jsonPath);
// 加载图像
fListWidget->addFileNames(t1List, t2List);
fListWidget->gotoItem(0);
// 加载总进度
fListWidget->resetProgress();
}
}

void LabCD::openBigImageFile()
{
// 获取文件路径
QString fileName = QFileDialog::getOpenFileName(
this,
tr("打开大图像"),
QString(),
tr("栅格图像文件 (*.tif *.tiff)")
);
if (fileName == "")
return;
QString saveDir = QFileInfo(fileName).absolutePath() + \
QDir::separator() + "split_output";
saveDir = saveDir.replace("\\", "/");
FileWorker::createFolder(saveDir);
// 获取切分大小
bool blockOk = false;
int blockSize = QInputDialog::getInt(
this,
tr("设置"),
tr("设置切块大小"),
512, 1, 2048, 1,
&blockOk
);
if (!blockOk)
return;
QtConcurrent::run([=]() {
if (ImagePress::splitTiff(fileName, saveDir, blockSize, blockSize))
messageState->setText(tr("切分完成,保存至:") + saveDir);
else
messageState->setText(tr("切分失败,可能是不支持的类型或超出范围的切块大小"));
});
}

void LabCD::mergeBigImage()
{
QString dirPath = QFileDialog::getExistingDirectory(
this,
QObject::tr("打开图像文件夹"),
QString(),
QFileDialog::ShowDirsOnly
);
if (dirPath.isEmpty())
return;
QtConcurrent::run([=]() {
if (ImagePress::mergeTiff(dirPath))
messageState->setText(
tr("合并完成,保存至:") + dirPath + "/merge.tif");
else
messageState->setText(tr("合并失败"));
});
}

void LabCD::clearEmptyMask()
{
QStringList t1List;
QStringList t2List;
QStringList GTList;
if (FileWorker::openImageDir(&t1List, &t2List, &GTList, this))
{
QtConcurrent::run([=]() {
LabCD::_clearEmptyMask(t1List, t2List, GTList); });
messageState->setText(tr("清理完成"));
}
}

void LabCD::_clearEmptyMask(
QStringList t1List, QStringList t2List, QStringList GTList)
{
std::sort(t1List.begin(), t1List.end());
std::sort(t2List.begin(), t2List.end());
std::sort(GTList.begin(), GTList.end());
QFileInfo fInfo;
QString pathName;
for (int i = 0; i < GTList.size(); ++i)
{
if (ImagePress::maskIsEmpty(GTList.at(i)))
{
// 清理图像
QFile::remove(t1List.at(i));
QFile::remove(t2List.at(i));
// 清理标签
fInfo = QFileInfo(GTList.at(i));
pathName = fInfo.path() + "/" + fInfo.baseName();
QFile::remove(GTList.at(i));
QFile::remove(pathName + ".json");
QFile::remove(pathName + "_pseudo.png");
}
}
}

void LabCD::convertMask2Json()
{
QString dirPath = QFileDialog::getExistingDirectory(
this,
QObject::tr("打开标签文件夹"),
QString(),
QFileDialog::ShowDirsOnly
);
if (dirPath.isEmpty())
return;
QtConcurrent::run([=]() { LabCD::_convertMask2Json(dirPath); });
messageState->setText(tr("转换完成"));
}

void LabCD::_convertMask2Json(QString dirPath)
{
QDir maskDir(dirPath);
QStringList nameFilters;
nameFilters << "*.jpg" << "*.jpeg" << "*.png" << "*.tif" << "*.tiff";
QStringList maskList = (maskDir).entryList(
nameFilters, QDir::Readable | QDir::Files, QDir::Name);
QString maskPath;
for (int i = 0; i < maskList.size(); ++i)
{
maskPath = dirPath + "/" + maskList.at(i);
ImagePress::savePolygonFromMask(maskPath);
}
}

void LabCD::save()
{
if (drawCanvas->imageWidth != 0 && drawCanvas->imageHeight != 0)
{
QString saveImgPath = savePath + "/GT/" + fileName;
int labNum = labTableWidget->getLen();
drawCanvas->finished();
ImagePress::saveResultFromPolygon(
saveImgPath,
labNum,
drawCanvas->imageHeight,
drawCanvas->imageWidth,
drawCanvas->t1Canva->aScene->polygonItems,
drawCanvas->projs,
drawCanvas->trans
);
fListWidget->finishedCurrentItem();
messageState->setText(tr("保存图像:") + saveImgPath);
fListWidget->progressUpAdd();
}
}

void LabCD::setCrossPenColor()
{
QColor nowCrossColor = drawCanvas->getCrossPenColor();
QColor color = QColorDialog::getColor(
nowCrossColor, this, tr("设置十字丝颜色"), QColorDialog::ShowAlphaChannel
);
setting->setValue("cross_color", color);
drawCanvas->setCrossPenColor(color);
}

void LabCD::updatePolysColor()
{
for (int i = 0; i < drawCanvas->t1Canva->aScene->polygonItems.count(); ++i)
{
int idx = drawCanvas->t1Canva->aScene->polygonItems[i]->getLabelIndex();
QColor polyColor = drawCanvas->t1Canva->aScene->polygonItems[i]->getColor();
QColor labColor = labTableWidget->getColorByIndex(idx);
if (polyColor.rgb() != labColor.rgb())
{
drawCanvas->t1Canva->aScene->polygonItems[i]->setColor(
labColor, labColor);
drawCanvas->t2Canva->aScene->polygonItems[i]->setColor(
labColor, labColor);
}
}
}

void LabCD::closeEvent(QCloseEvent* ev)
{
// 保存标签
if (savePath != "")
{
QString jsonPath = savePath + "/label.json";
labTableWidget->exportLabelToFile(jsonPath);
}
// 保存界面
setting->setValue("layout_status", QByteArray(saveState()));
QMainWindow::closeEvent(ev);
}

Thought

翻译代码

将C++/Qt代码翻译成Python/PyQt是一项不小的挑战,涉及多个自定义类的转换。可以逐步翻译关键部分,或概述整个应用结构。

拆解C++代码

我正在研究C++代码中的类。LabCD是QMainWindow的子类,负责初始化主窗口和各个界面元素。还用到一些自定义类和工具类。

考虑翻译细节

一个全而定制翻译需要实现许多自定义类。重点翻译主窗口LabCD,涉及菜单、工具栏和状态栏,提供自定义类的占位符。

Answer

将您提供的C++ Qt代码完整地转换为Python和PyQt涉及多个步骤,尤其是因为代码中引用了许多自定义类(如MultCanvasFileListLabelTable等)以及与OpenCV集成的部分。以下是一个全面的转换示例,包括主要窗口(LabCD类)、菜单栏、工具栏、状态栏以及部分核心功能。请注意,由于缺少自定义类的具体实现,部分功能将以占位符形式展示,您需要根据实际需求进一步实现这些类。

环境设置

首先,确保您已安装必要的Python库:

pip install PyQt5 opencv-python

项目结构

建议按照以下结构组织您的项目,以便于管理和扩展:

LabCD_Python/
├── main.py
├── labcd.py
├── utils/
│   ├── fileworker.py
│   └── imgpress.py
├── widgets/
│   ├── annotationview.py
│   ├── filelist.py
│   ├── labeltable.py
│   └── multcanvas.py
└── resources/
    ├── menu/
    │   └── resources/
    │       ├── Folder.png
    │       ├── Split.png
    │       ├── Merge.png
    │       ├── ClearMask.png
    │       ├── Convert.png
    │       ├── Github.png
    │       └── Help.png
    ├── tools/
    │   └── resources/
    │       ├── Save.png
    │       ├── Last.png
    │       ├── Next.png
    │       ├── Enlarge.png
    │       ├── Narrow.png
    │       ├── Full.png
    │       ├── DeletePolygon.png
    │       ├── DeleteAllPolygons.png
    │       ├── Color.png
    │       └── Reference.png
    └── main/
        └── resources/
            └── Icon.png

确保将相应的图标资源放置在resources文件夹中,并根据需要调整路径。

主应用程序 (main.py)

# main.py import sys from PyQt5.QtWidgets import QApplication from labcd import LabCD def main(): app = QApplication(sys.argv) window = LabCD() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()

主窗口实现 (labcd.py)

# labcd.py import sys from PyQt5.QtWidgets import ( QMainWindow, QLabel, QMenu, QAction, QFileDialog, QColorDialog, QMessageBox, QDockWidget, QToolBar, QStatusBar ) from PyQt5.QtGui import QIcon, QKeySequence, QPixmap, QImage from PyQt5.QtCore import Qt, QSettings, QUrl, pyqtSlot from PyQt5.Qt import QDesktopServices import cv2 from utils.fileworker import FileWorker from utils.imgpress import ImagePress from widgets.annotationview import AnnotationView from widgets.filelist import FileList from widgets.labeltable import LabelTable from widgets.multcanvas import MultCanvas from PyQt5.QtCore import QtConcurrent class LabCD(QMainWindow): def __init__(self, parent=None): super(LabCD, self).__init__(parent) self.setting = QSettings("./configs/setting.ini", QSettings.IniFormat) self.isCN = self.setting.value("language", "CN") != "EN" # 状态栏 self.status_bar = self.statusBar() self.message_state = QLabel("", self) self.message_local = QLabel("", self) self.status_bar.addWidget(self.message_state) self.status_bar.addPermanentWidget(self.message_local) # 菜单栏 self.init_menu_bar() # 工具栏 self.init_tool_bar() # 绘图界面 self.draw_canvas = MultCanvas(self) self.draw_canvas.t1Canva.aView.mousePosChanged.connect(self.update_mouse_position) self.draw_canvas.t2Canva.aView.mousePosChanged.connect(self.update_mouse_position) self.setCentralWidget(self.draw_canvas) # 图像文件列表 self.init_files_dock() # 标签列表 self.init_labels_dock() # 变化参考图 self.init_ref_dock() # 界面设置 self.resize(1200, 600) self.setWindowTitle(self.tr("LabCD - 遥感变化检测标注工具")) self.setWindowIcon(QIcon(":/main/resources/Icon.png")) def init_menu_bar(self): menu_bar = self.menuBar() # 文件菜单 file_menu = QMenu(self.tr("文件"), self) opens_act = QAction(QIcon(":/menu/resources/Folder.png"), self.tr("打开文件夹"), self) opens_act.setShortcut(QKeySequence("Ctrl+O")) opens_act.triggered.connect(self.open_dir) file_menu.addAction(opens_act) file_menu.addSeparator() split_act = QAction(QIcon(":/menu/resources/Split.png"), self.tr("切分大图"), self) split_act.setShortcut(QKeySequence("Ctrl+B")) split_act.triggered.connect(self.open_big_image_file) file_menu.addAction(split_act) merge_act = QAction(QIcon(":/menu/resources/Merge.png"), self.tr("合并大图"), self) merge_act.setShortcut(QKeySequence("Ctrl+M")) merge_act.triggered.connect(self.merge_big_image) file_menu.addAction(merge_act) clear_act = QAction(QIcon(":/menu/resources/ClearMask.png"), self.tr("清理空白标签"), self) clear_act.triggered.connect(self.clear_empty_mask) file_menu.addAction(clear_act) convert_act = QAction(QIcon(":/menu/resources/Convert.png"), self.tr("从标签建立标注"), self) convert_act.triggered.connect(self.convert_mask_to_json) file_menu.addAction(convert_act) menu_bar.addMenu(file_menu) # 关于菜单 about_menu = QMenu(self.tr("关于"), self) github_act = QAction(QIcon(":/menu/resources/Github.png"), self.tr("github主页"), self) github_act.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://github.com/geoyee/LabCD"))) about_menu.addAction(github_act) help_act = QAction(QIcon(":/menu/resources/Help.png"), self.tr("使用帮助"), self) help_act.setShortcut(QKeySequence("Ctrl+H")) help_act.triggered.connect(self.open_help) about_menu.addAction(help_act) menu_bar.addMenu(about_menu) # 语言菜单 lang_menu = QMenu(self.tr("语言"), self) if self.isCN: set_lang_act = QAction(QIcon(":/menu/resources/English.png"), self.tr("英文"), self) else: set_lang_act = QAction(QIcon(":/menu/resources/Chinese.png"), self.tr("中文"), self) set_lang_act.triggered.connect(self.toggle_language) lang_menu.addAction(set_lang_act) menu_bar.addMenu(lang_menu) def init_tool_bar(self): tool_bar = QToolBar(self) tool_bar.setMovable(False) # 保存 save_act = QAction(QIcon(":/tools/resources/Save.png"), self.tr("保存"), self) save_act.setShortcut(QKeySequence("Ctrl+S")) save_act.triggered.connect(self.save) tool_bar.addAction(save_act) tool_bar.addSeparator() # 上一张 last_act = QAction(QIcon(":/tools/resources/Last.png"), self.tr("上一张"), self) last_act.setShortcut(QKeySequence("S")) last_act.triggered.connect(self.goto_last_item) tool_bar.addAction(last_act) # 下一张 next_act = QAction(QIcon(":/tools/resources/Next.png"), self.tr("下一张"), self) next_act.setShortcut(QKeySequence("F")) next_act.triggered.connect(self.goto_next_item) tool_bar.addAction(next_act) tool_bar.addSeparator() # 放大 enlarge_act = QAction(QIcon(":/tools/resources/Enlarge.png"), self.tr("放大"), self) enlarge_act.triggered.connect(lambda: self.draw_canvas.zoom(1.1)) tool_bar.addAction(enlarge_act) # 缩小 narrow_act = QAction(QIcon(":/tools/resources/Narrow.png"), self.tr("缩小"), self) narrow_act.triggered.connect(lambda: self.draw_canvas.zoom(0.9)) tool_bar.addAction(narrow_act) # 全幅缩放 full_act = QAction(QIcon(":/tools/resources/Full.png"), self.tr("全幅缩放"), self) full_act.setShortcut(QKeySequence("Ctrl+F")) full_act.triggered.connect(self.reset_zoom) tool_bar.addAction(full_act) tool_bar.addSeparator() # 删除多边形 del_poly_act = QAction(QIcon(":/tools/resources/DeletePolygon.png"), self.tr("删除多边形"), self) del_poly_act.setShortcut(QKeySequence("Backspace")) del_poly_act.triggered.connect(self.delete_polygon) tool_bar.addAction(del_poly_act) # 删除所有多边形 del_all_polys_act = QAction(QIcon(":/tools/resources/DeleteAllPolygons.png"), self.tr("删除所有多边形"), self) del_all_polys_act.setShortcut(QKeySequence("Delete")) del_all_polys_act.triggered.connect(self.delete_all_polygons) tool_bar.addAction(del_all_polys_act) tool_bar.addSeparator() # 设置十字丝颜色 cross_color_act = QAction(QIcon(":/tools/resources/Color.png"), self.tr("设置十字丝颜色"), self) cross_color_act.triggered.connect(self.set_cross_pen_color) tool_bar.addAction(cross_color_act) tool_bar.addSeparator() # 打开变化参考图 self.isCVAAct = QAction(QIcon(":/tools/resources/Reference.png"), self.tr("打开变化参考图"), self, checkable=True) tool_bar.addAction(self.isCVAAct) self.addToolBar(Qt.LeftToolBarArea, tool_bar) def init_files_dock(self): files_dock = QDockWidget(self.tr("数据列表"), self) files_dock.setMinimumWidth(200) files_dock.setAllowedAreas(Qt.RightDockWidgetArea) self.fListWidget = FileList(self) # 连接信号 self.fListWidget.saveLastFileRequest.connect(self.save) self.fListWidget.FileClickRequest.connect(self.load_images) files_dock.setWidget(self.fListWidget) self.addDockWidget(Qt.RightDockWidgetArea, files_dock) def init_labels_dock(self): labels_dock = QDockWidget(self.tr("标签列表"), self) labels_dock.setMinimumWidth(200) labels_dock.setAllowedAreas(Qt.RightDockWidgetArea) self.labTableWidget = LabelTable(self) labels_dock.setWidget(self.labTableWidget) self.addDockWidget(Qt.RightDockWidgetArea, labels_dock) # 连接信号 self.labTableWidget.labelSelected.connect(self.on_label_selected) self.labTableWidget.colorChanged.connect(self.on_label_color_changed) self.draw_canvas.t1Canva.addimgDiff.connect(self.update_ref_image) def init_ref_dock(self): self.refDock = QDockWidget(self.tr("光谱变化向量强度参考图"), self) self.refDock.setAllowedAreas(Qt.NoDockWidgetArea) self.imgRef = QLabel(self) self.refDock.setFloating(True) self.refDock.hide() self.refDock.setWidget(self.imgRef) self.addDockWidget(Qt.NoDockWidgetArea, self.refDock) @pyqtSlot(float, float) def update_mouse_position(self, x, y): self.message_local.setText(f"{self.tr('当前坐标:')} {x}, {y}") def open_dir(self): t1List, t2List, GTList = [], [], [] if FileWorker.open_image_dir(t1List, t2List, None, self): # 新建保存目录 file_info = FileWorker.get_file_info(t1List[0]) self.savePath = FileWorker.get_save_path(file_info.path()) save_img_path = f"{self.savePath}/GT" FileWorker.create_folder(save_img_path) # 加载已有标签 json_path = f"{self.savePath}/label.json" if FileWorker.file_exists(json_path): self.labTableWidget.import_label_from_file(json_path) # 加载图像 self.fListWidget.add_file_names(t1List, t2List) self.fListWidget.goto_item(0) # 加载总进度 self.fListWidget.reset_progress() def open_big_image_file(self): file_name, _ = QFileDialog.getOpenFileName( self, self.tr("打开大图像"), "", self.tr("栅格图像文件 (*.tif *.tiff)") ) if not file_name: return save_dir = f"{FileWorker.get_file_info(file_name).absolutePath()}/split_output".replace("\\", "/") FileWorker.create_folder(save_dir) # 获取切分大小 block_size, ok = FileWorker.get_block_size(self) if not ok: return # 异步切分 QtConcurrent.run(self.split_image, file_name, save_dir, block_size, block_size) def split_image(self, file_name, save_dir, block_w, block_h): success = ImagePress.split_tiff(file_name, save_dir, block_w, block_h) if success: self.message_state.setText(f"{self.tr('切分完成,保存至:')} {save_dir}") else: self.message_state.setText(self.tr("切分失败,可能是不支持的类型或超出范围的切块大小")) def merge_big_image(self): dir_path = QFileDialog.getExistingDirectory( self, self.tr("打开图像文件夹"), "", QFileDialog.ShowDirsOnly ) if not dir_path: return # 异步合并 QtConcurrent.run(self.merge_image, dir_path) def merge_image(self, dir_path): success = ImagePress.merge_tiff(dir_path) if success: self.message_state.setText(f"{self.tr('合并完成,保存至:')} {dir_path}/merge.tif") else: self.message_state.setText(self.tr("合并失败")) def clear_empty_mask(self): t1List, t2List, GTList = [], [], [] if FileWorker.open_image_dir(t1List, t2List, GTList, self): QtConcurrent.run(self._clear_empty_mask, t1List, t2List, GTList) self.message_state.setText(self.tr("清理完成")) def _clear_empty_mask(self, t1List, t2List, GTList): FileWorker.clear_empty_mask(t1List, t2List, GTList) def convert_mask_to_json(self): dir_path = QFileDialog.getExistingDirectory( self, self.tr("打开标签文件夹"), "", QFileDialog.ShowDirsOnly ) if not dir_path: return QtConcurrent.run(self._convert_mask_to_json, dir_path) self.message_state.setText(self.tr("转换完成")) def _convert_mask_to_json(self, dir_path): ImagePress.save_polygon_from_mask(dir_path) def save(self): if self.draw_canvas.image_width != 0 and self.draw_canvas.image_height != 0: save_img_path = f"{self.savePath}/GT/{self.fileName}" lab_num = self.labTableWidget.get_length() self.draw_canvas.finished() ImagePress.save_result_from_polygon( save_img_path, lab_num, self.draw_canvas.image_height, self.draw_canvas.image_width, self.draw_canvas.t1Canva.aScene.polygon_items, self.draw_canvas.projs, self.draw_canvas.trans ) self.fListWidget.finished_current_item() self.message_state.setText(f"{self.tr('保存图像:')} {save_img_path}") self.fListWidget.progress_up_add() def set_cross_pen_color(self): current_color = self.draw_canvas.get_cross_pen_color() color = QColorDialog.getColor(current_color, self, self.tr("设置十字丝颜色"), QColorDialog.ShowAlphaChannel) if color.isValid(): self.setting.setValue("cross_color", color) self.draw_canvas.set_cross_pen_color(color) def update_ref_image(self, img_diff): if self.isCVAAct.isChecked(): ref_new_width = 300 # 可根据需要调整 ref_new_height = int(ref_new_width * img_diff.shape[0] / img_diff.shape[1]) img_diff_rgb = cv2.cvtColor(img_diff, cv2.COLOR_BGR2RGB) img_diff_resized = cv2.resize(img_diff_rgb, (ref_new_width, ref_new_height)) height, width, channel = img_diff_resized.shape bytes_per_line = 3 * width qimg = QImage(img_diff_resized.data, width, height, bytes_per_line, QImage.Format_RGB888) self.imgRef.setPixmap(QPixmap.fromImage(qimg)) self.refDock.setFixedSize(ref_new_width, ref_new_height) self.refDock.show() else: self.refDock.hide() def toggle_language(self): # 切换语言 lang_act = self.sender() if self.isCN: lang_act.setIcon(QIcon(":/menu/resources/Chinese.png")) lang_act.setText(self.tr("中文")) self.setting.setValue("language", "EN") else: lang_act.setIcon(QIcon(":/menu/resources/English.png")) lang_act.setText(self.tr("英文")) self.setting.setValue("language", "CN") self.isCN = not self.isCN QMessageBox.information( self, self.tr("提示"), self.tr("重启软件后更新语言设置。") ) def open_help(self): tutorial = "https://github.com/geoyee/LabCD/tree/develop/docs/Usage_tutorial.md" if not self.isCN: tutorial = "https://github.com/geoyee/LabCD/tree/develop/docs/Usage_tutorial_en.md" QDesktopServices.openUrl(QUrl(tutorial)) def goto_last_item(self): self.save() self.fListWidget.goto_last_item() def goto_next_item(self): self.save() self.fListWidget.goto_next_item() def reset_zoom(self): if self.draw_canvas.image_width != 0 and self.draw_canvas.image_height != 0: self.draw_canvas.reset_zoom(self.draw_canvas.image_width, self.draw_canvas.image_height) def delete_polygon(self): f1_index = self.draw_canvas.t1Canva.aScene.find_focus_polygon() f2_index = self.draw_canvas.t2Canva.aScene.find_focus_polygon() del_index = max(f1_index, f2_index) self.draw_canvas.t1Canva.aScene.del_poly(del_index) self.draw_canvas.t2Canva.aScene.del_poly(del_index) def delete_all_polygons(self): self.draw_canvas.t1Canva.aScene.remove_all_polygons() self.draw_canvas.t2Canva.aScene.remove_all_polygons() def load_images(self, t1_path, t2_path, json_path): self.draw_canvas.load_images(t1_path, t2_path, json_path) self.update_polys_color() file_info = FileWorker.get_file_info(t1_path) self.fileName = file_info.fileName() self.message_state.setText(f"{self.tr('加载图像:')} {t1_path}") def on_label_selected(self, now_label): self.draw_canvas.label_selected(now_label) self.message_state.setText(f"{self.tr('当前标签:[')}{now_label.get_index()}] {now_label.get_name()}") def on_label_color_changed(self, label_index, new_color): # 更新界面上的多边形颜色 for poly_item in self.draw_canvas.t1Canva.aScene.polygon_items: if poly_item.label_index == label_index: poly_item.set_color(new_color, new_color) self.draw_canvas.t2Canva.aScene.polygon_items[poly_item.index].set_color(new_color, new_color) self.draw_canvas.t1Canva.aScene.set_color(new_color, new_color) self.draw_canvas.t2Canva.aScene.set_color(new_color, new_color) def update_polys_color(self): for poly_item in self.draw_canvas.t1Canva.aScene.polygon_items: idx = poly_item.get_label_index() poly_color = poly_item.get_color() lab_color = self.labTableWidget.get_color_by_index(idx) if poly_color != lab_color: poly_item.set_color(lab_color, lab_color) self.draw_canvas.t2Canva.aScene.polygon_items[poly_item.index].set_color(lab_color, lab_color) def closeEvent(self, event): # 保存标签 if hasattr(self, 'savePath') and self.savePath: json_path = f"{self.savePath}/label.json" self.labTableWidget.export_label_to_file(json_path) # 保存界面 self.setting.setValue("layout_status", self.saveState()) super(LabCD, self).closeEvent(event)

工具类实现 (utils/fileworker.pyutils/imgpress.py)

由于具体实现细节未知,这里提供简化的占位符实现。您需要根据实际需求补充功能。

# utils/fileworker.py import os from PyQt5.QtWidgets import QMessageBox, QInputDialog class FileWorker: @staticmethod def open_image_dir(t1List, t2List, GTList, parent): # 实现打开文件夹并填充t1List、t2List、GTList # 返回True表示成功,False表示取消或失败 # 这里以简单示例返回False return False @staticmethod def get_file_info(path): from PyQt5.QtCore import QFileInfo return QFileInfo(path) @staticmethod def get_save_path(path): return os.path.dirname(path) @staticmethod def create_folder(path): if not os.path.exists(path): os.makedirs(path) @staticmethod def file_exists(path): return os.path.isfile(path) @staticmethod def get_block_size(parent): return QInputDialog.getInt( parent, "设置", "设置切块大小", 512, 1, 2048, 1 ) @staticmethod def clear_empty_mask(t1List, t2List, GTList): # 实现清理空白标签 pass
# utils/imgpress.py import cv2 import os class ImagePress: @staticmethod def split_tiff(file_name, save_dir, block_w, block_h): try: img = cv2.imread(file_name, cv2.IMREAD_UNCHANGED) if img is None: return False height, width = img.shape[:2] for y in range(0, height, block_h): for x in range(0, width, block_w): block = img[y:y+block_h, x:x+block_w] block_name = f"{os.path.splitext(os.path.basename(file_name))[0]}_{x}_{y}.tif" cv2.imwrite(os.path.join(save_dir, block_name), block) return True except Exception as e: print(e) return False @staticmethod def merge_tiff(dir_path): try: # 实现合并逻辑 return True except Exception as e: print(e) return False @staticmethod def save_polygon_from_mask(dir_path): # 实现从mask保存polygon的逻辑 pass @staticmethod def save_result_from_polygon(save_img_path, lab_num, img_h, img_w, polygon_items, projs, trans): # 实现保存结果的逻辑 pass @staticmethod def mask_is_empty(gt_path): # 判断mask是否为空 return False

自定义小部件实现 (widgets/)

以下是部分自定义小部件的简化实现。您需要根据实际需求补充功能和逻辑。

widgets/multcanvas.py

# widgets/multcanvas.py from PyQt5.QtWidgets import QWidget, QHBoxLayout from widgets.annotationview import AnnotationView from PyQt5.QtCore import pyqtSignal class MultCanvas(QWidget): addimgDiff = pyqtSignal(object) # 发送cv2 Mat def __init__(self, parent=None): super(MultCanvas, self).__init__(parent) layout = QHBoxLayout(self) self.t1Canva = AnnotationView(self) self.t2Canva = AnnotationView(self) layout.addWidget(self.t1Canva) layout.addWidget(self.t2Canva) self.setLayout(layout) def load_images(self, t1_path, t2_path, json_path): # 加载图像和JSON标注 pass def zoom(self, factor): self.t1Canva.aView.scale_zoom(factor) self.t2Canva.aView.scale_zoom(factor) def reset_zoom(self, width, height): self.t1Canva.aView.reset_zoom(width, height) self.t2Canva.aView.reset_zoom(width, height) def label_selected(self, label): # 处理标签选中 pass def finished(self): # 完成标注 pass def get_cross_pen_color(self): return self.t1Canva.aView.get_cross_pen_color() def set_cross_pen_color(self, color): self.t1Canva.aView.set_cross_pen_color(color) self.t2Canva.aView.set_cross_pen_color(color)

widgets/annotationview.py

# widgets/annotationview.py from PyQt5.QtWidgets import QGraphicsView from PyQt5.QtCore import pyqtSignal class AnnotationView(QGraphicsView): mousePosChanged = pyqtSignal(float, float) def __init__(self, parent=None): super(AnnotationView, self).__init__(parent) # 初始化QGraphicsScene等 # 连接鼠标移动事件 self.setMouseTracking(True) def mouseMoveEvent(self, event): pos = self.mapToScene(event.pos()) self.mousePosChanged.emit(pos.x(), pos.y()) super(AnnotationView, self).mouseMoveEvent(event) def scale_zoom(self, factor): self.scale(factor, factor) def reset_zoom(self, width, height): # 实现重置缩放 pass def get_cross_pen_color(self): # 返回当前十字丝颜色 return Qt.black def set_cross_pen_color(self, color): # 设置十字丝颜色 pass

widgets/filelist.py

# widgets/filelist.py from PyQt5.QtWidgets import QListWidget from PyQt5.QtCore import pyqtSignal class FileList(QListWidget): saveLastFileRequest = pyqtSignal() FileClickRequest = pyqtSignal(str, str, str) def __init__(self, parent=None): super(FileList, self).__init__(parent) # 初始化列表 def add_file_names(self, t1List, t2List): # 添加文件到列表 pass def goto_item(self, index): # 跳转到指定项 pass def goto_last_item(self): # 跳转到上一张 pass def goto_next_item(self): # 跳转到下一张 pass def finished_current_item(self): # 标记当前项为完成 pass def progress_up_add(self): # 更新进度 pass def reset_progress(self): # 重置进度 pass

widgets/labeltable.py

# widgets/labeltable.py from PyQt5.QtWidgets import QWidget class Label: def __init__(self, index, name): self.index = index self.name = name def get_index(self): return self.index def get_name(self): return self.name class LabelTable(QWidget): labelSelected = pyqtSignal(object) colorChanged = pyqtSignal(int, object) def __init__(self, parent=None): super(LabelTable, self).__init__(parent) # 初始化表格 def import_label_from_file(self, json_path): # 从文件导入标签 pass def export_label_to_file(self, json_path): # 导出标签到文件 pass def get_length(self): # 返回标签数量 return 0 def get_color_by_index(self, index): # 根据索引返回颜色 return Qt.black def change_label_duoto_add_poly_json(self, poly_json): # 更新标签 pass

资源文件处理

PyQt支持通过资源文件(.qrc)管理图标等资源。您可以使用pyrcc5工具将资源文件编译为Python模块。

创建资源文件 (resources.qrc)

创建一个resources.qrc文件,列出所有资源:

<!-- resources.qrc --> <RCC> <qresource prefix="/"> <file>menu/resources/Folder.png</file> <file>menu/resources/Split.png</file> <file>menu/resources/Merge.png</file> <file>menu/resources/ClearMask.png</file> <file>menu/resources/Convert.png</file> <file>menu/resources/Github.png</file> <file>menu/resources/Help.png</file> <file>tools/resources/Save.png</file> <file>tools/resources/Last.png</file> <file>tools/resources/Next.png</file> <file>tools/resources/Enlarge.png</file> <file>tools/resources/Narrow.png</file> <file>tools/resources/Full.png</file> <file>tools/resources/DeletePolygon.png</file> <file>tools/resources/DeleteAllPolygons.png</file> <file>tools/resources/Color.png</file> <file>tools/resources/Reference.png</file> <file>main/resources/Icon.png</file> </qresource> </RCC>

编译资源文件

使用pyrcc5编译资源文件:

pyrcc5 resources.qrc -o resources_rc.py

在您的Python代码中导入资源:

# 在labcd.py或main.py的开头 import resources_rc

总结

上述代码提供了一个全面的框架,将C++ Qt应用转换为Python PyQt应用。由于缺少具体的自定义类实现,部分功能以占位符形式提供。您需要根据实际需求完善这些类的具体实现,例如:

  • FileWorker:实现文件操作相关功能,如打开目录、获取文件信息、清理空白标签等。
  • ImagePress:实现图像处理相关功能,如切分和合并大图、从mask生成polygon等。
  • AnnotationView:实现图像标注视图,包括绘制多边形、处理鼠标事件等。
  • FileList:实现文件列表的管理和交互。
  • LabelTable:实现标签列表的管理和交互。
  • MultCanvas:实现包含多个AnnotationView的画布管理。

此外,确保处理多线程操作的线程安全性,尤其是在使用QtConcurrent进行异步任务时。

希望这个转换示例对您有所帮助!如果您在实现过程中遇到具体问题,请随时提问。

Share this Q&A