File indexing completed on 2024-04-21 03:50:56
0001 /************************************************************************* 0002 * Copyright (C) 2020 by Caio Jordão Carvalho <caiojcarvalho@gmail.com> * 0003 * * 0004 * This program is free software; you can redistribute it and/or * 0005 * modify it under the terms of the GNU General Public License as * 0006 * published by the Free Software Foundation; either version 3 of * 0007 * the License, or (at your option) any later version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, * 0010 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0012 * GNU General Public License for more details. * 0013 * * 0014 * You should have received a copy of the GNU General Public License * 0015 * along with this program. If not, see <http://www.gnu.org/licenses/>.* 0016 *************************************************************************/ 0017 0018 #include "image/imagepainter.h" 0019 #include "ui/container.h" 0020 #include "ui/mark.h" 0021 #include "ui/ui_mark.h" 0022 #include "util/fileutils.h" 0023 0024 #include <QAction> 0025 #include <QActionGroup> 0026 #include <QColorDialog> 0027 #include <QDir> 0028 #include <QFileDialog> 0029 #include <QFontMetrics> 0030 #include <QGraphicsItem> 0031 #include <QGraphicsPixmapItem> 0032 #include <QKeySequence> 0033 #include <QMessageBox> 0034 #include <QRandomGenerator> 0035 #include <QShortcut> 0036 #include <QToolButton> 0037 #include <QtGlobal> 0038 #include <QtConcurrent/QtConcurrentRun> 0039 0040 #include <KActionCollection> 0041 #include <QFutureWatcher> 0042 0043 static QDir markTempDirectory() 0044 { 0045 return QDir(QDir::tempPath().append("/mark")); 0046 } 0047 0048 marK::marK(QWidget *parent) : 0049 KXmlGuiWindow(parent), 0050 m_ui(new Ui::marK), 0051 m_watcher(new QFileSystemWatcher(this)), 0052 m_currentDirectory(QDir::currentPath()), 0053 m_filepath(""), 0054 m_autoSaveType(Serializer::OutputType::None) 0055 { 0056 m_ui->setupUi(this); 0057 m_ui->listLabel->setText(m_currentDirectory); 0058 m_ui->listLabel->setToolTip(m_currentDirectory); 0059 m_ui->containerWidget->setMinimumSize(860, 650); 0060 0061 setupActions(); 0062 setupConnections(); 0063 setupGUI(Default); 0064 0065 updateFiles(); 0066 addNewClass(); 0067 0068 if (!markTempDirectory().exists()) 0069 markTempDirectory().mkpath("."); 0070 } 0071 0072 void marK::setupActions() 0073 { 0074 QAction *openDirAction = new QAction(this); 0075 openDirAction->setText("Open Directory"); 0076 openDirAction->setIcon(QIcon::fromTheme("document-open")); 0077 actionCollection()->setDefaultShortcut(openDirAction, Qt::Modifier::CTRL + Qt::Key::Key_O); 0078 actionCollection()->addAction("openDirectory", openDirAction); 0079 connect(openDirAction, &QAction::triggered, this, &marK::changeDirectory); 0080 0081 QAction *importData = new QAction(this); 0082 importData->setText("Import"); 0083 actionCollection()->setDefaultShortcut(importData, Qt::Modifier::CTRL + Qt::Key::Key_I); 0084 actionCollection()->addAction("importData", importData); 0085 connect(importData, &QAction::triggered, this, &marK::importData); 0086 0087 QAction *toXML = new QAction(this); 0088 toXML->setText("XML"); 0089 actionCollection()->addAction("toXML", toXML); 0090 connect(toXML, &QAction::triggered, [&](){ saveObjects(Serializer::OutputType::XML); }); 0091 0092 QAction *toJson = new QAction(this); 0093 toJson->setText("JSON"); 0094 actionCollection()->addAction("toJSON", toJson); 0095 connect(toJson, &QAction::triggered, [&](){ saveObjects(Serializer::OutputType::JSON); }); 0096 0097 QAction *undoAction = new QAction(this); 0098 undoAction->setText("Undo"); 0099 actionCollection()->setDefaultShortcut(undoAction, Qt::Modifier::CTRL + Qt::Key::Key_Z); 0100 actionCollection()->addAction("undo", undoAction); 0101 connect(undoAction, &QAction::triggered, m_ui->containerWidget, &Container::undo); 0102 0103 QAction *deleteAction = new QAction(this); 0104 deleteAction->setText("Delete"); 0105 actionCollection()->setDefaultShortcut(deleteAction, Qt::Modifier::CTRL + Qt::Key::Key_D); 0106 actionCollection()->addAction("delete", deleteAction); 0107 connect(deleteAction, &QAction::triggered, m_ui->containerWidget, &Container::deleteObject); 0108 0109 QActionGroup *autoSaveActionGroup = new QActionGroup(this); 0110 0111 QAction *autoSaveJsonButton = new QAction(this); 0112 autoSaveJsonButton->setText("JSON"); 0113 autoSaveJsonButton->setCheckable(true); 0114 autoSaveJsonButton->setActionGroup(autoSaveActionGroup); 0115 actionCollection()->addAction("autosaveJSON", autoSaveJsonButton); 0116 connect(autoSaveJsonButton, &QAction::triggered, this, &marK::toggleAutoSave); 0117 0118 QAction *autoSaveXmlButton = new QAction(this); 0119 autoSaveXmlButton->setText("XML"); 0120 autoSaveXmlButton->setCheckable(true); 0121 autoSaveXmlButton->setActionGroup(autoSaveActionGroup); 0122 actionCollection()->addAction("autosaveXML", autoSaveXmlButton); 0123 connect(autoSaveXmlButton, &QAction::triggered, this, &marK::toggleAutoSave); 0124 0125 QAction *autoSaveDisabledButton = new QAction(this); 0126 autoSaveDisabledButton->setText("Disabled"); 0127 autoSaveDisabledButton->setCheckable(true); 0128 autoSaveDisabledButton->setChecked(true); 0129 autoSaveDisabledButton->setActionGroup(autoSaveActionGroup); 0130 actionCollection()->addAction("autosaveDisabled", autoSaveDisabledButton); 0131 connect(autoSaveDisabledButton, &QAction::triggered, this, &marK::toggleAutoSave); 0132 0133 QShortcut *nextItemShortcut = new QShortcut(this); 0134 nextItemShortcut->setKey(Qt::Key_Down); 0135 connect(nextItemShortcut, &QShortcut::activated, [&](){ changeIndex(1); }); 0136 0137 QShortcut *previousItemShortcut = new QShortcut(this); 0138 previousItemShortcut->setKey(Qt::Key_Up); 0139 connect(previousItemShortcut, &QShortcut::activated, [&]() { changeIndex(-1); }); 0140 0141 QShortcut *zoomInShortcut = new QShortcut(this); 0142 zoomInShortcut->setKey(Qt::Modifier::CTRL + Qt::Key::Key_Equal); 0143 connect(zoomInShortcut, &QShortcut::activated, [&]() { 0144 ImagePainter *painter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter()); 0145 if (painter != nullptr) 0146 painter->zoomIn(); 0147 }); 0148 0149 QShortcut *zoomOutShortcut = new QShortcut(this); 0150 zoomOutShortcut->setKey(Qt::Modifier::CTRL + Qt::Key::Key_Minus); 0151 connect(zoomOutShortcut, &QShortcut::activated, [&]() { 0152 ImagePainter *painter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter()); 0153 if (painter != nullptr) 0154 painter->zoomOut(); 0155 }); 0156 } 0157 0158 void marK::setupConnections() 0159 { 0160 m_ui->newClassButton->setEnabled(false); 0161 m_ui->undoButton->setEnabled(false); 0162 m_ui->deleteButton->setEnabled(false); 0163 m_ui->resetButton->setEnabled(false); 0164 m_ui->comboBox->setEnabled(false); 0165 m_ui->selectClassColorButton->setEnabled(false); 0166 m_ui->polygonButton->setEnabled(false); 0167 m_ui->rectButton->setEnabled(false); 0168 0169 KStandardAction::quit(qApp, SLOT(quit()), actionCollection()); 0170 0171 connect(m_ui->listWidget, &QListWidget::currentItemChanged, this, 0172 qOverload<QListWidgetItem*>(&marK::changeItem)); 0173 0174 connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &marK::updateFiles); 0175 0176 connect(m_ui->newClassButton, &QPushButton::clicked, this, qOverload<>(&marK::addNewClass)); 0177 0178 connect(m_ui->undoButton, &QPushButton::clicked, m_ui->containerWidget, &Container::undo); 0179 connect(m_ui->deleteButton, &QPushButton::clicked, m_ui->containerWidget, &Container::deleteObject); 0180 connect(m_ui->resetButton, &QPushButton::clicked, m_ui->containerWidget, &Container::reset); 0181 0182 connect(m_ui->comboBox, &QComboBox::editTextChanged, this, 0183 [&](const QString & text) { 0184 if (m_ui->comboBox->count() == 0) return; 0185 0186 m_ui->comboBox->setItemText(m_ui->comboBox->currentIndex(), text); 0187 m_objClasses[m_ui->comboBox->currentIndex()]->setName(text); 0188 } 0189 ); 0190 0191 connect(m_ui->comboBox, QOverload<int>::of(&QComboBox::activated), this, 0192 [&](int index) { 0193 m_ui->containerWidget->setObjClass(m_objClasses[index]); 0194 } 0195 ); 0196 0197 connect(m_ui->selectClassColorButton, &QPushButton::clicked, this, &marK::selectClassColor); 0198 0199 connect(m_ui->polygonButton, &QPushButton::clicked, this, 0200 [&](bool checked) { 0201 // probably temporary, made this so Shape can be in imagecontainer 0202 auto polygonShape = ImagePainter::Shape::Polygon; 0203 ImagePainter* imgPainter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter()); 0204 if (imgPainter != nullptr) 0205 imgPainter->setShape(polygonShape); 0206 } 0207 ); 0208 0209 connect(m_ui->rectButton, &QPushButton::clicked, this, 0210 [&](bool checked) { 0211 auto rectangleShape = ImagePainter::Shape::Rectangle; 0212 ImagePainter* imgPainter = dynamic_cast<ImagePainter*>(m_ui->containerWidget->painter()); 0213 if (imgPainter != nullptr) 0214 imgPainter->setShape(rectangleShape); 0215 } 0216 ); 0217 0218 connect(m_ui->containerWidget, &Container::painterChanged, this, 0219 [&](Container::PainterType painterType) { 0220 m_objClasses.clear(); 0221 m_ui->comboBox->clear(); 0222 addNewClass(); 0223 bool isFileLoaded = painterType != Container::PainterType::None; 0224 m_ui->newClassButton->setEnabled(isFileLoaded); 0225 m_ui->undoButton->setEnabled(isFileLoaded); 0226 m_ui->resetButton->setEnabled(isFileLoaded); 0227 m_ui->comboBox->setEnabled(isFileLoaded); 0228 m_ui->selectClassColorButton->setEnabled(isFileLoaded); 0229 m_ui->polygonButton->setEnabled(isFileLoaded); 0230 m_ui->rectButton->setEnabled(isFileLoaded); 0231 m_ui->deleteButton->setEnabled(isFileLoaded); 0232 if (painterType == Container::PainterType::Image || !isFileLoaded) 0233 m_ui->groupBox_2->setVisible(true); 0234 else 0235 m_ui->groupBox_2->setVisible(false); 0236 } 0237 ); 0238 0239 connect(m_ui->containerWidget, &Container::savedObjectsChanged, this, &marK::autoSave); 0240 } 0241 0242 void marK::updateFiles() 0243 { 0244 int index = m_ui->listWidget->currentRow(); 0245 m_ui->listWidget->clear(); 0246 0247 QDir resDirectory(m_currentDirectory); 0248 QStringList items = resDirectory.entryList(QDir::Files); 0249 // removing json and xml files that are resulted from autosave 0250 for (const QString &item : qAsConst(items)) { 0251 items.removeOne(FileUtils::placeSuffix(item, Serializer::OutputType::JSON)); 0252 items.removeOne(FileUtils::placeSuffix(item, Serializer::OutputType::XML)); 0253 } 0254 0255 for (const QString &item : qAsConst(items)) { 0256 if (FileUtils::isTextFile(item) || FileUtils::isImageFile(item)) { 0257 QPixmap item_pix = (FileUtils::isTextFile(item)) ? 0258 QIcon::fromTheme("document-edit-sign").pixmap(20, 20) : 0259 QPixmap(resDirectory.filePath(item)); 0260 0261 item_pix = item_pix.scaledToWidth(20); 0262 0263 QListWidgetItem *itemW = new QListWidgetItem(item_pix, item); 0264 itemW->setToolTip(item); 0265 0266 m_ui->listWidget->addItem(itemW); 0267 } 0268 } 0269 0270 m_ui->listWidget->setGridSize(QSize( 0271 m_ui->listWidget->sizeHintForColumn(0), 0272 m_ui->listWidget->sizeHintForRow(0))); 0273 0274 m_ui->listWidget->setCurrentRow(index); 0275 } 0276 0277 void marK::changeIndex(const int count) 0278 { 0279 int newIndex = m_ui->listWidget->currentRow() + count; 0280 if (newIndex >= m_ui->listWidget->count()) 0281 newIndex = 0; 0282 0283 else if (newIndex < 0) 0284 newIndex = m_ui->listWidget->count() - 1; 0285 0286 m_ui->listWidget->setCurrentRow(newIndex); 0287 QListWidgetItem *currentItem = m_ui->listWidget->item(newIndex); 0288 changeItem(currentItem); 0289 } 0290 0291 void marK::changeItem(QListWidgetItem *item) 0292 { 0293 if (item != nullptr) { 0294 QString itemPath = QDir(m_currentDirectory).filePath(item->text()); 0295 0296 if (itemPath != m_filepath) { 0297 makeTempFile(); 0298 m_filepath = itemPath; 0299 m_ui->containerWidget->changeItem(itemPath); 0300 retrieveTempFile(); 0301 } 0302 } 0303 } 0304 0305 void marK::changeDirectory() 0306 { 0307 QString newDirectoryPath = QFileDialog::getExistingDirectory(this, "Select Directory", QDir::homePath(), 0308 QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); 0309 0310 if (m_currentDirectory == newDirectoryPath) 0311 return; 0312 0313 if (!newDirectoryPath.isEmpty()) { 0314 if (m_currentDirectory != "") 0315 m_watcher->removePath(m_currentDirectory); 0316 0317 m_currentDirectory = newDirectoryPath; 0318 m_watcher->addPath(m_currentDirectory); 0319 m_ui->listWidget->clear(); 0320 m_ui->containerWidget->clear(); 0321 m_filepath.clear(); 0322 updateFiles(); 0323 0324 QFontMetrics metrics(m_ui->listLabel->font()); 0325 QString elidedText = metrics.elidedText(m_currentDirectory, Qt::ElideMiddle, 0326 m_ui->listLabel->maximumWidth() - int(m_ui->listLabel->maximumWidth() * 0.05)); 0327 0328 m_ui->listLabel->setText(elidedText); 0329 m_ui->listLabel->setToolTip(m_currentDirectory); 0330 } 0331 } 0332 0333 void marK::addNewClass() 0334 { 0335 auto classSize = QString::number(m_objClasses.size() + 1); 0336 MarkedClass* newClass = new MarkedClass(classSize); 0337 m_objClasses << newClass; 0338 0339 QPixmap colorPix(70, 45); 0340 colorPix.fill(newClass->color()); 0341 0342 m_ui->comboBox->addItem(QIcon(colorPix), newClass->name()); 0343 m_ui->comboBox->setCurrentIndex(m_objClasses.size() - 1); 0344 0345 m_ui->containerWidget->setObjClass(newClass); 0346 } 0347 0348 void marK::updateComboBox() 0349 { 0350 m_ui->comboBox->clear(); 0351 0352 for (const auto& markedClass : m_objClasses) { 0353 QPixmap colorPix(70, 45); 0354 colorPix.fill(markedClass->color()); 0355 m_ui->comboBox->addItem(QIcon(colorPix), markedClass->name()); 0356 } 0357 0358 m_ui->comboBox->setCurrentIndex(m_objClasses.size() - 1); 0359 MarkedClass* currentClass = m_objClasses[m_ui->comboBox->currentIndex()]; 0360 m_ui->containerWidget->setObjClass(currentClass); 0361 } 0362 0363 void marK::selectClassColor() 0364 { 0365 auto rand = QRandomGenerator().global(); 0366 QColorDialog colorDialog(QColor(rand->bounded(0, 256), rand->bounded(0, 256), rand->bounded(0, 256)), this); 0367 0368 if (colorDialog.exec() == QDialog::DialogCode::Accepted) { 0369 QPixmap colorPix(70, 45); 0370 colorPix.fill(colorDialog.selectedColor()); 0371 m_ui->comboBox->setItemIcon(m_ui->comboBox->currentIndex(), QIcon(colorPix)); 0372 0373 m_objClasses[m_ui->comboBox->currentIndex()]->setColor(colorDialog.selectedColor()); 0374 } 0375 0376 m_ui->containerWidget->repaint(); 0377 } 0378 0379 void marK::saveObjects(Serializer::OutputType outputType) 0380 { 0381 QString filepath = QFileDialog::getSaveFileName(this, tr("Save File"), 0382 m_currentDirectory, 0383 tr(FileUtils::filterString(outputType))); 0384 0385 if (filepath.isEmpty()) 0386 return; 0387 0388 bool success = Serializer::write(filepath, m_ui->containerWidget->savedObjects(), outputType); 0389 0390 if (!success) { 0391 QMessageBox msgBox; 0392 msgBox.setText("failed to save annotation"); 0393 msgBox.setIcon(QMessageBox::Warning); 0394 msgBox.exec(); 0395 } 0396 } 0397 0398 void marK::importData() 0399 { 0400 if (m_filepath.isEmpty()) return; //exiting because this is no image loaded 0401 0402 QString filepath = QFileDialog::getOpenFileName(this, "Select File", m_currentDirectory, 0403 "JSON and XML files (*.json *.xml)"); 0404 0405 if (filepath.isEmpty()) 0406 return; 0407 0408 QVector<MarkedObject*> objects = Serializer::read(filepath); 0409 0410 bool success = m_ui->containerWidget->importObjects(objects); 0411 0412 if (!success) { 0413 QMessageBox msgBox; 0414 msgBox.setText("failed to load annotation"); 0415 msgBox.setIcon(QMessageBox::Warning); 0416 msgBox.exec(); 0417 return; 0418 } 0419 0420 m_objClasses.clear(); 0421 for (MarkedObject *obj : objects) { 0422 if (!m_objClasses.contains(obj->objClass())) 0423 m_objClasses << obj->objClass(); 0424 } 0425 updateComboBox(); 0426 } 0427 0428 void marK::retrieveTempFile() 0429 { 0430 QString tempFilePath = markTempDirectory().filePath(QString(m_filepath).replace("/", "_")); 0431 tempFilePath = FileUtils::placeSuffix(tempFilePath, Serializer::OutputType::JSON); 0432 0433 QVector<MarkedObject*> objects = Serializer::read(tempFilePath); 0434 0435 bool success = m_ui->containerWidget->importObjects(objects); 0436 0437 if (!success) 0438 return; 0439 0440 m_objClasses.clear(); 0441 for (MarkedObject *obj : objects) { 0442 if (!m_objClasses.contains(obj->objClass())) 0443 m_objClasses << obj->objClass(); 0444 } 0445 0446 updateComboBox(); 0447 0448 m_ui->containerWidget->repaint(); 0449 } 0450 0451 void marK::makeTempFile() 0452 { 0453 QString tempFilePath = markTempDirectory().filePath(QString(m_filepath).replace("/", "_")); 0454 0455 Serializer::write(tempFilePath, m_ui->containerWidget->savedObjects(), Serializer::OutputType::JSON); 0456 } 0457 0458 void marK::toggleAutoSave() 0459 { 0460 QAction *button = qobject_cast<QAction*>(sender()); 0461 QString type = button->text(); 0462 0463 if (type == "&Disabled") 0464 m_autoSaveType = Serializer::OutputType::None; 0465 0466 else if (type == "&XML") 0467 m_autoSaveType = Serializer::OutputType::XML; 0468 0469 else if (type == "&JSON") 0470 m_autoSaveType = Serializer::OutputType::JSON; 0471 } 0472 0473 void marK::autoSave() 0474 { 0475 if (m_autoSaveType == Serializer::OutputType::None) 0476 return; 0477 0478 auto *watcher = new QFutureWatcher<bool>(); 0479 connect(watcher, &QFutureWatcher<bool>::finished, this, [watcher]() { 0480 bool success = watcher->result(); 0481 delete watcher; 0482 0483 if (!success) { 0484 QMessageBox msgBox; 0485 msgBox.setText("Failed to auto-save annotation."); 0486 msgBox.setIcon(QMessageBox::Warning); 0487 msgBox.exec(); 0488 } 0489 }); 0490 0491 QFuture<bool> future = QtConcurrent::run( 0492 Serializer::write, m_filepath, m_ui->containerWidget->savedObjects(), m_autoSaveType 0493 ); 0494 watcher->setFuture(future); 0495 } 0496 0497 marK::~marK() 0498 { 0499 markTempDirectory().removeRecursively(); 0500 } 0501 0502 #include "moc_mark.cpp"