/*========================================================================= medInria Copyright (c) INRIA 2013 - 2019. All rights reserved. See LICENSE.txt for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. =========================================================================*/ #include "polygonRoiToolBox.h" #include "urologyViewEvent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include const char *polygonRoiToolBox::generateBinaryImageButtonName = "generateBinaryImageButton"; polygonRoiToolBox::polygonRoiToolBox(QWidget *parent ) : medAbstractSelectableToolBox(parent), activeDataIndex() { medSettingsManager *manager = medSettingsManager::instance(); QString speciality = manager->value("startup", "default_segmentation_speciality", "Default").toString(); if (speciality=="Urology") { specialityPreference = 1; } else { specialityPreference = 0; } auto displayWidget = new QWidget(this); this->addWidget(displayWidget); auto layout = new QVBoxLayout(); displayWidget->setLayout(layout); activateTBButton = new QPushButton(tr("Activate Toolbox")); activateTBButton->setToolTip(tr("Activate closed polygon mode. You should only have one view.")); activateTBButton->setCheckable(true); activateTBButton->setObjectName("closedPolygonButton"); connect(activateTBButton,SIGNAL(toggled(bool)),this,SLOT(clickClosePolygon(bool))); interpolate = new QCheckBox(tr("Interpolate between contours")); interpolate->setToolTip("Interpolate between master ROIs"); interpolate->setObjectName("interpolateButton"); interpolate->setChecked(true); connect(interpolate,SIGNAL(clicked(bool)) ,this,SLOT(interpolateCurve(bool))); auto repulsorLayout = new QHBoxLayout(); auto repulsorLabel = new QLabel("Correct contours"); repulsorLayout->addWidget(repulsorLabel); repulsorTool = new QPushButton(tr("Repulsor")); repulsorTool->setToolTip(tr("Activate Repulsor")); repulsorTool->setObjectName("repulsorTool"); repulsorTool->setCheckable(true); connect(repulsorTool,SIGNAL(clicked(bool)),this,SLOT(activateRepulsor(bool))); repulsorLayout->addWidget(repulsorTool); auto activateTBLayout = new QHBoxLayout(); layout->addLayout(activateTBLayout ); activateTBLayout->addWidget(activateTBButton); // Add Contour Management Toolbox QString identifier = speciality.toLower() + QString("LabelToolBox"); pMedToolBox = medToolBoxFactory::instance()->createToolBox(identifier); pMedToolBox->header()->hide(); pMedToolBox->hide(); layout->addWidget(pMedToolBox); connect(activateTBButton, SIGNAL(toggled(bool)), pMedToolBox, SLOT(setEnabled(bool)), Qt::UniqueConnection); auto contoursActionLayout = new QVBoxLayout(); layout->addLayout(contoursActionLayout ); contoursActionLayout->addWidget(interpolate); contoursActionLayout->addLayout(repulsorLayout); auto saveLabel = new QLabel("Save segmentations as:"); auto saveButtonsLayout = new QHBoxLayout(); saveBinaryMaskButton = new QPushButton(tr("Mask(s)")); saveBinaryMaskButton->setToolTip("Import the current mask to the non persistent database"); saveBinaryMaskButton->setObjectName(generateBinaryImageButtonName); connect(saveBinaryMaskButton,SIGNAL(clicked()),this,SLOT(saveBinaryImage())); saveButtonsLayout->addWidget(saveBinaryMaskButton); saveContourButton = new QPushButton("Contour(s)"); saveContourButton->setToolTip("Export these contours as an .ctrb file loadable only in medInria."); saveContourButton->setMinimumSize(150, 20); saveContourButton->setMaximumSize(150, 20); saveContourButton->setObjectName("saveContoursButton"); connect(saveContourButton, SIGNAL(clicked()), this, SLOT(saveContours())); saveButtonsLayout->addWidget(saveContourButton); auto saveLayout = new QVBoxLayout(); layout->addLayout(saveLayout); saveLayout->addWidget(saveLabel); saveLayout->addLayout(saveButtonsLayout); // How to use auto helpLayout = new QVBoxLayout(); layout->addLayout(helpLayout); helpLayout->setContentsMargins(0, 10, 0, 0); dataDestButton = new QRadioButton("Save in source of original data",this); dataDestButton->setToolTip("Generated data can be saved in origin data source or default working source"); dataDestButton->setObjectName("dstButton"); dataDestButton->setChecked(true); helpButton = new QPushButton("Help"); helpButton->setToolTip("show help related to this toolbox."); helpButton->setFixedSize(150, 20); helpButton->setObjectName("helpButton"); connect(helpButton, SIGNAL(clicked()), this, SLOT(showHelp())); helpLayout->addWidget(dataDestButton); helpLayout->addWidget(helpButton); // buttons initialisation: view has no data disableButtons(); } polygonRoiToolBox::~polygonRoiToolBox() { clear(); pMedToolBox->deleteLater(); // If every view of the container has been closed, we need to check if the view needs to be clean medTabbedViewContainers *tabs = getWorkspace()->tabbedViewContainers(); QList containersInTabSelected = tabs->containersInTab(tabs->currentIndex()); if (containersInTabSelected.size() == 1) { auto view = containersInTabSelected.at(0)->view(); if (view && dynamic_cast(view)->layersCount() == 0) { containersInTabSelected.at(0)->checkIfStillDeserveToLiveContainer(); } } } bool polygonRoiToolBox::registered() { return medToolBoxFactory::instance()->registerToolBox(); } dtkPlugin* polygonRoiToolBox::plugin() { medPluginManager *pm = medPluginManager::instance(); dtkPlugin *plugin = pm->plugin ( "Polygon ROI" ); return plugin; } medAbstractData *polygonRoiToolBox::processOutput() { return nullptr; } void polygonRoiToolBox::updateView() { medTabbedViewContainers *tabs = this->getWorkspace()->tabbedViewContainers(); for (medViewContainer *container : tabs->containersInTab(tabs->currentIndex())) { QUuid uuid = container->uuid(); medAbstractView *view = container->view(); if (!view) { QString msg = "No view in selected container (" + uuid.toString() + ")"; displayMessageError(msg); return; } auto imageView = dynamic_cast(view); if (!imageView) { displayMessageError("[FATAL] Unable to get medAbstractImageView from medAbstractView"); return; } // work on image data we want to segment QList dataList = getITKImageDataInSelectedView(view); if (!dataList.empty()) { // attached view event with the first image data we found in view medAbstractData *data = dataList[0]; if (viewEventHash.contains(data->dataIndex())) { baseViewEvent *event = viewEventHash[data->dataIndex()]; // check if we have an event view attached to this data but in another container/view if (imageView != event->getCurrentView()) { connect(imageView, SIGNAL(layerRemoved(medAbstractData * )), this, SLOT(onLayerRemoveOnOrientedViews(medAbstractData * )), Qt::UniqueConnection); connect(imageView, SIGNAL(orientationChanged()), event, SLOT(showOnDifferentOrientation())); } } else { createAndConnectEventFilter(data, imageView); } // check if there is contour (.ctr/.ctrb) QList contoursList = getContoursInSelectedView(view); if (!contoursList.empty()) { for (medContours *contour : contoursList) { QVector &tagContoursSet = contour->getTagContoursSet(); qint32 speIndex = tagContoursSet[0].getSpecialityIndex(); if (speIndex!=specialityPreference) { displayMessageError("Unable to load contour. Mismatch speciality preference "); } else { for (baseViewEvent *event1 : viewEventHash.values()) { if (event1->getCurrentView()==view) { event1->loadContours(tagContoursSet); if (!activateTBButton->isChecked()) { emit activateTBButton->click(); } break; } } imageView->removeData(dynamic_cast(contour)); } } } } else { // Avoid loading of contours if there is no image data loaded before QList contoursList = getContoursInSelectedView(view); if (!contoursList.empty()) { for (medContours *contour : contoursList) { displayMessageError("[WARNING] - Drop image data in view before contours"); imageView->removeData(dynamic_cast(contour)); container->removeView(); } } } } } void polygonRoiToolBox::onLayerRemoveOnOrientedViews(medAbstractData *data) { auto view = dynamic_cast(sender()); if (viewEventHash.contains(data->dataIndex())) { baseViewEvent *event = viewEventHash[data->dataIndex()]; if (event->getCurrentView() != view) { event->removeViewFromList(view); } } } void polygonRoiToolBox::createAndConnectEventFilter(const medAbstractData *data, medAbstractImageView *imageView) { QString toolBoxName = data->metaDataValues(medMetaDataKeys::key("StudyDescription"))[0] + " - " + data->metaDataValues(medMetaDataKeys::key("SeriesDescription"))[0]; if (specialityPreference==1) { auto eventFilter = new urologyViewEvent(imageView, this); eventFilter->initialize(pMedToolBox, toolBoxName); viewEventHash[data->dataIndex()] = eventFilter; } else { auto eventFilter = new defaultViewEvent(imageView, this); eventFilter->initialize(pMedToolBox, toolBoxName); viewEventHash[data->dataIndex()] = eventFilter; } connect(imageView, SIGNAL(selectedRequest(bool)), this, SLOT(onDataIndexActivated()), Qt::UniqueConnection); connect(imageView, SIGNAL(selectedRequest(bool)), activateTBButton, SLOT(setEnabled(bool)), Qt::UniqueConnection); activateTBButton->setEnabled(true); connect(imageView, SIGNAL(layerRemoved(medAbstractData *)), this, SLOT(onLayerRemoved(medAbstractData *)), Qt::UniqueConnection); } void polygonRoiToolBox::onLayerRemoved(medAbstractData *data) { if (viewEventHash.contains(data->dataIndex())) { baseViewEvent *pViewEvent = viewEventHash[data->dataIndex()]; disconnect(pViewEvent->getCurrentView(), SIGNAL(selectedRequest(bool)), this, SLOT(onDataIndexActivated())); // check if this data is in another view. If yes, we have to disconnect signals medTabbedViewContainers *tabs = this->getWorkspace()->tabbedViewContainers(); for (medViewContainer *container : tabs->containersInTab(tabs->currentIndex())) { QUuid uuid = container->uuid(); medAbstractView *view = container->view(); if (!view) { QString msg = "No view in selected container (" + uuid.toString() + ")"; displayMessageError(msg); return; } QList dataList = getITKImageDataInSelectedView(view); auto imageView = dynamic_cast(view); if (dataList.contains(data) && imageView != pViewEvent->getCurrentView()) { disconnect(view, SIGNAL(orientationChanged()), pViewEvent, SLOT(showOnDifferentOrientation())); disconnect(view, SIGNAL(layerRemoved(medAbstractData * )), this, SLOT(onLayerRemoveOnOrientedViews(medAbstractData * ))); } } viewEventHash.remove(data->dataIndex()); if (activeDataIndex == data->dataIndex()) { activeDataIndex = medDataIndex(); } delete pViewEvent; if (viewEventHash.empty()) { clear(); } } } void polygonRoiToolBox::onDataIndexActivated() { if (activateTBButton->isChecked()) { auto view = dynamic_cast(sender()); for (medAbstractData *data : getITKImageDataInSelectedView(view)) { if (data->dataIndex() == activeDataIndex) { return; } } for (baseViewEvent *event : viewEventHash) { event->removeFromAllViews(); } for (medAbstractData *data : getITKImageDataInSelectedView(view)) { if (viewEventHash.contains(data->dataIndex())) { baseViewEvent *event = viewEventHash.value(data->dataIndex()); event->onSelectContainer(); event->activateRepulsor(repulsorTool->isChecked()); event->setEnableInterpolation(interpolate->isChecked()); activeDataIndex = data->dataIndex(); highLightContainer(event->getCurrentView()); return; } } } } void polygonRoiToolBox::clickClosePolygon(bool state) { if (!state) { pMedToolBox->hide(); for (baseViewEvent *event : viewEventHash.values()) { event->removeViewInteractor(); disconnect(event->getCurrentView(), SIGNAL(selectedRequest(bool)), this, SLOT(onDataIndexActivated())); disconnect(event->getCurrentView(), SIGNAL(layerRemoved(medAbstractData *)), this, SLOT(onLayerRemoved(medAbstractData *))); } } else { pMedToolBox->show(); for (baseViewEvent *event : viewEventHash.values()) { qDebug()<<"data index "<getCurrentView(); connect(event->getCurrentView(), SIGNAL(selectedRequest(bool)), this, SLOT(onDataIndexActivated()), Qt::UniqueConnection); connect(event->getCurrentView(), SIGNAL(layerRemoved(medAbstractData * )), this, SLOT(onLayerRemoved(medAbstractData *)), Qt::UniqueConnection); } if (activeDataIndex.isValid() && viewEventHash[activeDataIndex]) { viewEventHash[activeDataIndex]->onSelectContainer(); } } saveBinaryMaskButton->setEnabled(state); saveContourButton->setEnabled(state); dataDestButton->setEnabled(state); interpolate->setEnabled(state); repulsorTool->setEnabled(state); } void polygonRoiToolBox::activateRepulsor(bool state) { if (viewEventHash.contains(activeDataIndex)) { viewEventHash.value(activeDataIndex)->activateRepulsor(state); } } void polygonRoiToolBox::interpolateCurve(bool state) { if (viewEventHash.contains(activeDataIndex)) { viewEventHash.value(activeDataIndex)->setEnableInterpolation(state); } } void polygonRoiToolBox::saveBinaryImage() { for (baseViewEvent *event1 : viewEventHash.values()) { event1->saveMask(); } } void polygonRoiToolBox::disableButtons() { activateTBButton->setEnabled(false); activateTBButton->setChecked(false); repulsorTool->setEnabled(false); repulsorTool->setChecked(false); saveBinaryMaskButton->setEnabled(false); dataDestButton->setEnabled(false); saveContourButton->setEnabled(false); interpolate->setEnabled(false); interpolate->setChecked(true); } void polygonRoiToolBox::saveContours() { for (baseViewEvent *event : viewEventHash.values()) { event->saveAllContours(); } } void polygonRoiToolBox::clear() { disableButtons(); activateTBButton->setText("Activate Toolbox"); activeDataIndex = medDataIndex(); // Switching to a new toolbox, we can clean the main container behavior for (baseViewEvent *event : viewEventHash) { delete event; } viewEventHash.clear(); pMedToolBox->clear(); pMedToolBox->setEnabled(false); } QList polygonRoiToolBox::getITKImageDataInSelectedView(medAbstractView *view) { QList dataList; if (!view) { QString msg = "Unable to get data if the view is null"; displayMessageError(msg); qDebug() << msg; } else { auto layeredView = dynamic_cast(view); if (!layeredView) { QString msg = "Unable to get data if the layered view is null"; displayMessageError(msg); qDebug() << msg; } else { for (unsigned int layer=0; layerlayersCount(); layer++) { medAbstractData *data = layeredView->layerData(layer); if (!data || data->identifier().contains("vtkDataMesh")) { handleDisplayError(medAbstractProcessLegacy::NO_MESH); } else if (data->identifier().contains("itkDataImageVector")) { handleDisplayError(medAbstractProcessLegacy::UNDEFINED); } else if (data->identifier().contains("itkDataImage")) { dataList.append(layeredView->layerData(layer)); } } } } return dataList; } QList polygonRoiToolBox::getContoursInSelectedView(medAbstractView *view) { QList contoursList; if (!view) { QString msg = "Unable to get data if the view is null"; displayMessageError(msg); qDebug() << msg; } else { auto layeredView = dynamic_cast(view); if (!layeredView) { QString msg = "Unable to get data if the layered view is null"; displayMessageError(msg); qDebug() << msg; } else { for (unsigned int layer=0; layerlayersCount(); layer++) { medAbstractData *data = layeredView->layerData(layer); if (data && data->identifier().contains("medContours")) { contoursList.append(dynamic_cast(data)); } } } } return contoursList; } void polygonRoiToolBox::highLightContainer(medAbstractView *pView) { medTabbedViewContainers *tabs = this->getWorkspace()->tabbedViewContainers(); for (medViewContainer *container : tabs->containersInTab(tabs->currentIndex())) { auto iView = dynamic_cast(container->view()); container->setSelected(iView==pView); if (iView==pView) { container->highlight("red"); } } } void polygonRoiToolBox::drawCross(double *position) { for (auto event : viewEventHash) { event->drawCross(position); } } void polygonRoiToolBox::eraseCross() { for (auto event : viewEventHash) { event->eraseCross(); } } void polygonRoiToolBox::showHelp() const { QMessageBox msgBox; msgBox.setWindowTitle("Help"); msgBox.setIcon(QMessageBox::Information); QString underlineStyle = "• %1"; QString titleStyle = "
%1

"; QString main = QString(QString(titleStyle).arg("Main features") + QString(underlineStyle).arg("Draw Contour:") + " Activate the toolbox, then shift+click on the data set

" + QString(underlineStyle).arg("Define new Label:") + " Select a label then shift+click on the data set

" + QString(underlineStyle).arg("Add New Label:") + " Click on Plus Button

" + QString(underlineStyle).arg("Remove label:") + " Click on Minus Button

"); QString shortcut = QString(QString(titleStyle).arg("Shortcut") + QString(underlineStyle).arg("A:") + " Select contour (work only closed to a contour)

" + QString(underlineStyle).arg("C:") + " Copy contour (work only closed to a contour)

" + QString(underlineStyle).arg("V:") + " Paste contour(s)

" + QString(underlineStyle).arg("Up/Down:") + " Move to previous/next slice

" + QString(underlineStyle).arg("BackSpace:") + " Delete node (work only closed to a contour)

" + QString(underlineStyle).arg("Alt + Click:") + " Draw cross on mouse click 2D position in all views

" + QString(underlineStyle).arg("D:") + " Draw cross on current 2D position in all views

" + QString(underlineStyle).arg("E:") + " Erase cross in all views

" + QString(underlineStyle).arg("H:") + " show this help

"); if (specialityPreference==1) { shortcut.append(QString(underlineStyle).arg("S:") + " Switch color between target and score

"); } QString contextual = QString(QString(titleStyle).arg("Contextual menu") + QString(underlineStyle).arg("Remove node/contour/label

") + QString(underlineStyle).arg("Save segmentation contour/mask

") + QString(underlineStyle).arg("Change current label with another existing label

") + QString(underlineStyle).arg("Copy

")); if (specialityPreference==1) { contextual.append(QString(underlineStyle).arg("Attach a pirad score to a target

")); } const QString explanation = main + shortcut + contextual; msgBox.setText(explanation); msgBox.exec(); }