diff --git a/Modules/DataTypesExt/src/mitkCompressedImageContainer.cpp b/Modules/DataTypesExt/src/mitkCompressedImageContainer.cpp index e18427a4dc..4cf478a91e 100644 --- a/Modules/DataTypesExt/src/mitkCompressedImageContainer.cpp +++ b/Modules/DataTypesExt/src/mitkCompressedImageContainer.cpp @@ -1,181 +1,184 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkCompressedImageContainer.h" #include "mitkImageReadAccessor.h" #include "itk_zlib.h" #include mitk::CompressedImageContainer::CompressedImageContainer() - : m_PixelType(nullptr) + : m_PixelType(nullptr), + m_ImageGeometry (nullptr) { } mitk::CompressedImageContainer::~CompressedImageContainer() { for (auto iter = m_ByteBuffers.begin(); iter != m_ByteBuffers.end(); ++iter) { free( iter->first ); } + + delete m_PixelType; } void mitk::CompressedImageContainer::SetImage( Image* image ) { for (auto iter = m_ByteBuffers.begin(); iter != m_ByteBuffers.end(); ++iter) { free( iter->first ); } m_ByteBuffers.clear(); // Compress diff image using zlib (will be restored on demand) // determine memory size occupied by voxel data m_ImageDimension = image->GetDimension(); m_ImageDimensions.clear(); m_PixelType = new mitk::PixelType( image->GetPixelType()); m_OneTimeStepImageSizeInBytes = m_PixelType->GetSize(); // bits per element divided by 8 for (unsigned int i = 0; i < m_ImageDimension; ++i) { unsigned int currentImageDimension = image->GetDimension(i); m_ImageDimensions.push_back( currentImageDimension ); if (i < 3) { m_OneTimeStepImageSizeInBytes *= currentImageDimension; // only the 3D memory size } } m_ImageGeometry = image->GetGeometry(); m_NumberOfTimeSteps = 1; if (m_ImageDimension > 3) { m_NumberOfTimeSteps = image->GetDimension(3); } for (unsigned int timestep = 0; timestep < m_NumberOfTimeSteps; ++timestep) { // allocate a buffer as specified by zlib unsigned long bufferSize = m_OneTimeStepImageSizeInBytes + static_cast(m_OneTimeStepImageSizeInBytes * 0.2) + 12; unsigned char* byteBuffer = (unsigned char*) malloc(bufferSize); if (itk::Object::GetDebug()) { // compress image here into a buffer MITK_INFO << "Using ZLib version: '" << zlibVersion() << "'" << std::endl << "Attempting to compress " << m_OneTimeStepImageSizeInBytes << " image bytes into a buffer of size " << bufferSize << std::endl; } ImageReadAccessor imgAcc(image, image->GetVolumeData(timestep)); ::Bytef* dest(byteBuffer); ::uLongf destLen(bufferSize); ::Bytef* source( (unsigned char*) imgAcc.GetData() ); ::uLongf sourceLen( m_OneTimeStepImageSizeInBytes ); int zlibRetVal = ::compress(dest, &destLen, source, sourceLen); if (itk::Object::GetDebug()) { if (zlibRetVal == Z_OK) { MITK_INFO << "Success, using " << destLen << " bytes of the buffer (ratio " << ((double)destLen / (double)sourceLen) << ")" << std::endl; } else { switch ( zlibRetVal ) { case Z_MEM_ERROR: MITK_ERROR << "not enough memory" << std::endl; break; case Z_BUF_ERROR: MITK_ERROR << "output buffer too small" << std::endl; break; default: MITK_ERROR << "other, unspecified error" << std::endl; break; } } } // only use the neccessary amount of memory, realloc the buffer! byteBuffer = (unsigned char*) realloc( byteBuffer, destLen ); bufferSize = destLen; //MITK_INFO << "Using " << bufferSize << " bytes to store compressed image (" << destLen << " needed)" << std::endl; m_ByteBuffers.push_back( std::pair( byteBuffer, bufferSize ) ); } } mitk::Image::Pointer mitk::CompressedImageContainer::GetImage() { if (m_ByteBuffers.empty()) return nullptr; // uncompress image data, create an Image Image::Pointer image = Image::New(); unsigned int dims[20]; // more than 20 dimensions and bang for (unsigned int dim = 0; dim < m_ImageDimension; ++dim) dims[dim] = m_ImageDimensions[dim]; image->Initialize( *m_PixelType, m_ImageDimension, dims ); // this IS needed, right ?? But it does allocate memory -> does create one big lump of memory (also in windows) unsigned int timeStep(0); for (auto iter = m_ByteBuffers.begin(); iter != m_ByteBuffers.end(); ++iter, ++timeStep) { ImageReadAccessor imgAcc(image, image->GetVolumeData(timeStep)); ::Bytef* dest( (unsigned char*) imgAcc.GetData() ); ::uLongf destLen(m_OneTimeStepImageSizeInBytes); ::Bytef* source( iter->first ); ::uLongf sourceLen( iter->second ); int zlibRetVal = ::uncompress(dest, &destLen, source, sourceLen); if (itk::Object::GetDebug()) { if (zlibRetVal == Z_OK) { MITK_INFO << "Success, destLen now " << destLen << " bytes" << std::endl; } else { switch ( zlibRetVal ) { case Z_DATA_ERROR: MITK_ERROR << "compressed data corrupted" << std::endl; break; case Z_MEM_ERROR: MITK_ERROR << "not enough memory" << std::endl; break; case Z_BUF_ERROR: MITK_ERROR << "output buffer too small" << std::endl; break; default: MITK_ERROR << "other, unspecified error" << std::endl; break; } } } } image->SetGeometry( m_ImageGeometry ); image->Modified(); return image; } diff --git a/Modules/DicomUI/src/QmitkDicomLocalStorageWidget.cpp b/Modules/DicomUI/src/QmitkDicomLocalStorageWidget.cpp index 46c4d339cb..f8f53bf5e6 100644 --- a/Modules/DicomUI/src/QmitkDicomLocalStorageWidget.cpp +++ b/Modules/DicomUI/src/QmitkDicomLocalStorageWidget.cpp @@ -1,254 +1,254 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ // Qmitk #include "QmitkDicomLocalStorageWidget.h" //#include // Qt #include #include #include #include const std::string QmitkDicomLocalStorageWidget::Widget_ID = "org.mitk.Widgets.QmitkDicomLocalStorageWidget"; QmitkDicomLocalStorageWidget::QmitkDicomLocalStorageWidget(QWidget *parent) : QWidget(parent) - , m_Controls(nullptr) , m_LocalIndexer(new ctkDICOMIndexer(parent)) + , m_Controls(nullptr) { CreateQtPartControl(this); } QmitkDicomLocalStorageWidget::~QmitkDicomLocalStorageWidget() { m_LocalDatabase->closeDatabase(); } void QmitkDicomLocalStorageWidget::CreateQtPartControl( QWidget *parent ) { if ( !m_Controls ) { m_Controls = new Ui::QmitkDicomLocalStorageWidgetControls; m_Controls->setupUi( parent ); this->SetupProgressDialog(this); connect(m_Controls->deleteButton,SIGNAL(clicked()),this,SLOT(OnDeleteButtonClicked())); connect(m_Controls->viewInternalDataButton, SIGNAL(clicked()), this , SLOT(OnViewButtonClicked())); connect(m_Controls->ctkDicomBrowser, SIGNAL(seriesSelectionChanged(const QStringList&)), this, SLOT(OnSeriesSelectionChanged(const QStringList&))); connect(m_Controls->ctkDicomBrowser, SIGNAL(seriesSelectionChanged(const QStringList&)), this, SLOT(OnSeriesSelectionChanged(const QStringList&))); connect(m_Controls->ctkDicomBrowser, SIGNAL(seriesDoubleClicked(const QModelIndex&)), this, SLOT(OnViewButtonClicked())); connect(m_LocalIndexer, SIGNAL(indexingComplete()),this, SLOT(OnFinishedImport())); connect(m_LocalIndexer, SIGNAL(indexingComplete()),this, SIGNAL(SignalFinishedImport())); connect(m_LocalIndexer, SIGNAL(indexingComplete()),this, SLOT(OnFinishedImport())); connect(m_LocalIndexer, SIGNAL(indexingFilePath(const QString&)), m_ProgressDialogLabel, SLOT(setText(const QString&))); connect(m_LocalIndexer, SIGNAL(progress(int)), m_ProgressDialog, SLOT(setValue(int))); connect(m_ProgressDialog, SIGNAL(canceled()), m_LocalIndexer, SLOT(cancel())); m_Controls->ctkDicomBrowser->setTableOrientation(Qt::Vertical); } } void QmitkDicomLocalStorageWidget::OnStartDicomImport(const QString& dicomData) { if(m_LocalDatabase->isOpen()) { m_LocalIndexer->addDirectory(*m_LocalDatabase,dicomData,m_LocalDatabase->databaseDirectory()); } } void QmitkDicomLocalStorageWidget::OnStartDicomImport(const QStringList& dicomData) { if(m_LocalDatabase->isOpen()) { m_ProgressDialog->show(); m_LocalIndexer->addListOfFiles(*m_LocalDatabase,dicomData,m_LocalDatabase->databaseDirectory()); } } void QmitkDicomLocalStorageWidget::OnFinishedImport() { m_ProgressDialog->setValue(m_ProgressDialog->maximum()); } void QmitkDicomLocalStorageWidget::OnDeleteButtonClicked() { if (!this->DeletePatients()) { if (!this->DeleteStudies()) { this->DeleteSeries(); } } m_Controls->ctkDicomBrowser->updateTableViews(); } bool QmitkDicomLocalStorageWidget::DeletePatients() { auto selectedPatientUIDs = m_Controls->ctkDicomBrowser->currentPatientsSelection(); if (!selectedPatientUIDs.empty()) { QStringList studyUIDs; for (const auto& patientUID : selectedPatientUIDs) studyUIDs.append(m_LocalDatabase->studiesForPatient(patientUID)); QStringList seriesUIDs; for (const auto& studyUID : studyUIDs) seriesUIDs.append(m_LocalDatabase->seriesForStudy(studyUID)); auto answer = QMessageBox::question( nullptr, "Delete Patients", QString("Do you really want to delete %1 %2, containing %3 series in %4 %5?") .arg(selectedPatientUIDs.count()) .arg(selectedPatientUIDs.count() != 1 ? "patients" : "patient") .arg(seriesUIDs.count()) .arg(studyUIDs.count()) .arg(studyUIDs.count() != 1 ? "studies" : "study"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::Yes) { for (const auto& patientUID : selectedPatientUIDs) m_LocalDatabase->removePatient(patientUID); } return true; } return false; } bool QmitkDicomLocalStorageWidget::DeleteStudies() { auto selectedStudyUIDs = m_Controls->ctkDicomBrowser->currentStudiesSelection(); if (!selectedStudyUIDs.empty()) { QStringList seriesUIDs; for (const auto& studyUID : selectedStudyUIDs) seriesUIDs.append(m_LocalDatabase->seriesForStudy(studyUID)); auto answer = QMessageBox::question( nullptr, "Delete Studies", QString("Do you really want to delete %1 %2, containing %3 series?") .arg(selectedStudyUIDs.count()) .arg(selectedStudyUIDs.count() != 1 ? "studies" : "study") .arg(seriesUIDs.count()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::Yes) { for (const auto& studyUID : selectedStudyUIDs) m_LocalDatabase->removeStudy(studyUID); } return true; } return false; } bool QmitkDicomLocalStorageWidget::DeleteSeries() { auto selectedSeriesUIDs = m_Controls->ctkDicomBrowser->currentSeriesSelection(); if (!selectedSeriesUIDs.empty()) { auto answer = QMessageBox::question( nullptr, "Delete Series", QString("Do you really want to delete %1 series?") .arg(selectedSeriesUIDs.count()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::Yes) { for (const auto& seriesUID : selectedSeriesUIDs) m_LocalDatabase->removeSeries(seriesUID); } return true; } return false; } void QmitkDicomLocalStorageWidget::OnViewButtonClicked() { QStringList uids = m_Controls->ctkDicomBrowser->currentSeriesSelection(); QString uid; foreach (uid, uids) { QStringList filesForSeries = m_LocalDatabase->filesForSeries(uid); QHash eventProperty; eventProperty.insert("FilesForSeries", filesForSeries); if(!filesForSeries.isEmpty()) { QString modality = m_LocalDatabase->fileValue(filesForSeries.at(0),"0008,0060"); eventProperty.insert("Modality", modality); } emit SignalDicomToDataManager(eventProperty); } } void QmitkDicomLocalStorageWidget::SetDatabaseDirectory(QString newDatatbaseDirectory) { QDir databaseDirecory = QDir(newDatatbaseDirectory); if(!databaseDirecory.exists()) { databaseDirecory.mkpath(databaseDirecory.absolutePath()); } QString newDatatbaseFile = databaseDirecory.absolutePath() + QString("/ctkDICOM.sql"); this->SetDatabase(newDatatbaseFile); } void QmitkDicomLocalStorageWidget::SetDatabase(QString databaseFile) { m_LocalDatabase = new ctkDICOMDatabase(databaseFile); m_LocalDatabase->setParent(this); m_Controls->ctkDicomBrowser->setDICOMDatabase(m_LocalDatabase); } void QmitkDicomLocalStorageWidget::OnSeriesSelectionChanged(const QStringList &s) { m_Controls->viewInternalDataButton->setEnabled((s.size() != 0)); } void QmitkDicomLocalStorageWidget::SetupProgressDialog(QWidget* parent) { m_ProgressDialog = new QProgressDialog("DICOM Import", "Cancel", 0, 100, parent,Qt::WindowTitleHint | Qt::WindowSystemMenuHint); m_ProgressDialogLabel = new QLabel("Initialization...", m_ProgressDialog); m_ProgressDialog->setLabel(m_ProgressDialogLabel); m_ProgressDialog->setWindowModality(Qt::ApplicationModal); m_ProgressDialog->setMinimumDuration(0); } diff --git a/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.cpp b/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.cpp index d69b5f41a5..19e8a9fe47 100644 --- a/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.cpp +++ b/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.cpp @@ -1,104 +1,102 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDiffSliceOperation.h" +#include + #include mitk::DiffSliceOperation::DiffSliceOperation():Operation(1) { m_TimeStep = 0; - m_Slice = nullptr; + m_zlibSliceContainer = nullptr; m_Image = nullptr; m_WorldGeometry = nullptr; m_SliceGeometry = nullptr; m_ImageIsValid = false; } mitk::DiffSliceOperation::DiffSliceOperation(mitk::Image* imageVolume, - vtkImageData* slice, + Image *slice, SlicedGeometry3D* sliceGeometry, unsigned int timestep, BaseGeometry* currentWorldGeometry):Operation(1) { m_WorldGeometry = currentWorldGeometry->Clone(); /* Quick fix for bug 12338. Guard object - fix this when clone method of PlaneGeometry is cloning the reference geometry (see bug 13392)*/ //xxxx m_GuardReferenceGeometry = mitk::BaseGeometry::New(); m_GuardReferenceGeometry = dynamic_cast(m_WorldGeometry.GetPointer())->GetReferenceGeometry(); /*---------------------------------------------------------------------------------------------------*/ m_SliceGeometry = sliceGeometry->Clone(); m_TimeStep = timestep; - /*m_zlibSliceContainer = CompressedImageContainer::New(); - m_zlibSliceContainer->SetImage( slice );*/ - m_Slice = vtkSmartPointer::New(); - m_Slice->DeepCopy(slice); + m_zlibSliceContainer = CompressedImageContainer::New(); + m_zlibSliceContainer->SetImage( slice ); m_Image = imageVolume; if ( m_Image) { /*add an observer to listen to the delete event of the image, this is necessary because the operation is then invalid*/ itk::SimpleMemberCommand< DiffSliceOperation >::Pointer command = itk::SimpleMemberCommand< DiffSliceOperation >::New(); command->SetCallbackFunction( this, &DiffSliceOperation::OnImageDeleted ); //get the id of the observer, used to remove it later on m_DeleteObserverTag = imageVolume->AddObserver( itk::DeleteEvent(), command ); m_ImageIsValid = true; } else m_ImageIsValid = false; } mitk::DiffSliceOperation::~DiffSliceOperation() { - - m_Slice = nullptr; m_WorldGeometry = nullptr; - //m_zlibSliceContainer = NULL; + m_zlibSliceContainer = nullptr; if (m_ImageIsValid) { //if the image is still there, we have to remove the observer from it m_Image->RemoveObserver( m_DeleteObserverTag ); } m_Image = nullptr; } -vtkImageData* mitk::DiffSliceOperation::GetSlice() +mitk::Image::Pointer mitk::DiffSliceOperation::GetSlice() { - //Image::ConstPointer image = m_zlibSliceContainer->GetImage().GetPointer(); - return m_Slice; + Image::Pointer image = m_zlibSliceContainer->GetImage(); + return image; } bool mitk::DiffSliceOperation::IsValid() { - return m_ImageIsValid && (m_Slice.GetPointer() != nullptr) && (m_WorldGeometry.IsNotNull());//TODO improve + return m_ImageIsValid && m_zlibSliceContainer.IsNotNull() && (m_WorldGeometry.IsNotNull());//TODO improve } void mitk::DiffSliceOperation::OnImageDeleted() { //if our imageVolume is removed e.g. from the datastorage the operation is no lnger valid m_ImageIsValid = false; } diff --git a/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.h b/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.h index 953c3f7e7e..497fe1e56a 100644 --- a/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.h +++ b/Modules/Segmentation/Algorithms/mitkDiffSliceOperation.h @@ -1,140 +1,119 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDiffSliceOperation_h_Included #define mitkDiffSliceOperation_h_Included #include -#include "mitkCommon.h" +#include "mitkCompressedImageContainer.h" #include -//#include "mitkCompressedImageContainer.h" -#include #include -#include - -//DEPRECATED -#include namespace mitk { + +class Image; + /** \brief An Operation for applying an edited slice to the volume. \sa DiffSliceOperationApplier The information for the operation is specified by properties: imageVolume the volume where the slice was extracted from. slice the slice to be applied. timestep the timestep in an 4D image. currentWorldGeometry specifies the axis where the slice has to be applied in the volume. This Operation can be used to realize undo-redo functionality for e.g. segmentation purposes. */ class MITKSEGMENTATION_EXPORT DiffSliceOperation : public Operation { public: mitkClassMacro(DiffSliceOperation, OperationActor); //itkFactorylessNewMacro(Self) //itkCloneMacro(Self) //mitkNewMacro4Param(DiffSliceOperation,mitk::Image,mitk::Image,unsigned int, mitk::PlaneGeometry); /** \brief Creates an empty instance. Note that it is not valid yet. The properties of the object have to be set. */ DiffSliceOperation(); /** \brief */ - DiffSliceOperation( mitk::Image* imageVolume, vtkImageData* slice, SlicedGeometry3D* sliceGeometry, unsigned int timestep, BaseGeometry* currentWorldGeometry); - - /** \brief - * - * \deprecatedSince{2013_09} Please use TimeGeometry instead of TimeSlicedGeometry. For more information see http://www.mitk.org/Development/Refactoring%20of%20the%20Geometry%20Classes%20-%20Part%201 - */ - DEPRECATED(DiffSliceOperation( mitk::Image* imageVolume, vtkImageData* slice, TimeSlicedGeometry* sliceGeometry, unsigned int timestep, BaseGeometry* currentWorldGeometry)); + DiffSliceOperation( mitk::Image* imageVolume, mitk::Image* slice, SlicedGeometry3D* sliceGeometry, unsigned int timestep, BaseGeometry* currentWorldGeometry); /** \brief Check if it is a valid operation.*/ bool IsValid(); /** \brief Set the image volume.*/ void SetImage(mitk::Image* image){ this->m_Image = image;} /** \brief Get th image volume.*/ mitk::Image* GetImage(){return this->m_Image;} /** \brief Set thee slice to be applied.*/ void SetImage(vtkImageData* slice){ this->m_Slice = slice;} /** \brief Get the slice that is applied in the operation.*/ - vtkImageData* GetSlice(); + Image::Pointer GetSlice(); /** \brief Get timeStep.*/ void SetTimeStep(unsigned int timestep){this->m_TimeStep = timestep;} /** \brief Set timeStep*/ unsigned int GetTimeStep(){return this->m_TimeStep;} /** \brief Set the axis where the slice has to be applied in the volume.*/ void SetSliceGeometry(SlicedGeometry3D* sliceGeometry){this->m_SliceGeometry = sliceGeometry;} /** \brief Get the axis where the slice has to be applied in the volume.*/ SlicedGeometry3D* GetSliceGeometry(){return this->m_SliceGeometry;} - - /** \brief Set the axis where the slice has to be applied in the volume. - * \deprecatedSince{2013_09} Please use TimeGeometry instead of TimeSlicedGeometry. For more information see http://www.mitk.org/Development/Refactoring%20of%20the%20Geometry%20Classes%20-%20Part%201 - */ - void SetSliceGeometry(TimeSlicedGeometry* sliceGeometry); - /** \brief Set the axis where the slice has to be applied in the volume.*/ void SetCurrentWorldGeometry(BaseGeometry* worldGeometry){this->m_WorldGeometry = worldGeometry;} /** \brief Get the axis where the slice has to be applied in the volume.*/ BaseGeometry* GetWorldGeometry(){return this->m_WorldGeometry;} - - /** \brief Set the axis where the slice has to be applied in the volume. - * \deprecatedSince{2013_09} Please use TimeGeometry instead of TimeSlicedGeometry. For more information see http://www.mitk.org/Development/Refactoring%20of%20the%20Geometry%20Classes%20-%20Part%201 - */ - void SetCurrentWorldGeometry(TimeSlicedGeometry* worldGeometry); - protected: virtual ~DiffSliceOperation(); /** \brief Callback for image observer.*/ void OnImageDeleted(); - //CompressedImageContainer::Pointer m_zlibSliceContainer; + CompressedImageContainer::Pointer m_zlibSliceContainer; mitk::Image* m_Image; vtkSmartPointer m_Slice; SlicedGeometry3D::Pointer m_SliceGeometry; unsigned int m_TimeStep; BaseGeometry::Pointer m_WorldGeometry; bool m_ImageIsValid; unsigned long m_DeleteObserverTag; mitk::BaseGeometry::Pointer m_GuardReferenceGeometry; }; } #endif diff --git a/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.cpp b/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.cpp index 6466510fda..9dcbc5df67 100644 --- a/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.cpp +++ b/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.cpp @@ -1,93 +1,94 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkDiffSliceOperationApplier.h" + +#include "mitkDiffSliceOperation.h" +#include #include "mitkRenderingManager.h" #include "mitkSegTool2D.h" +#include // VTK #include mitk::DiffSliceOperationApplier::DiffSliceOperationApplier() { } mitk::DiffSliceOperationApplier::~DiffSliceOperationApplier() { } void mitk::DiffSliceOperationApplier::ExecuteOperation( Operation* operation ) { DiffSliceOperation* imageOperation = dynamic_cast( operation ); //as we only support DiffSliceOperation return if operation is not type of DiffSliceOperation if(!imageOperation) return; //chak if the operation is valid if(imageOperation->IsValid()) { //the actual overwrite filter (vtk) vtkSmartPointer reslice = vtkSmartPointer::New(); + mitk::Image::Pointer slice = imageOperation->GetSlice(); //Set the slice as 'input' - reslice->SetInputSlice(imageOperation->GetSlice()); + reslice->SetInputSlice(const_cast(slice->GetVtkImageData())); //set overwrite mode to true to write back to the image volume reslice->SetOverwriteMode(true); reslice->Modified(); //a wrapper for vtkImageOverwrite mitk::ExtractSliceFilter::Pointer extractor = mitk::ExtractSliceFilter::New(reslice); extractor->SetInput( imageOperation->GetImage() ); extractor->SetTimeStep( imageOperation->GetTimeStep() ); extractor->SetWorldGeometry( dynamic_cast(imageOperation->GetWorldGeometry()) ); extractor->SetVtkOutputRequest(true); extractor->SetResliceTransformByGeometry( imageOperation->GetImage()->GetGeometry( imageOperation->GetTimeStep() ) ); extractor->Modified(); extractor->Update(); //make sure the modification is rendered RenderingManager::GetInstance()->RequestUpdateAll(); imageOperation->GetImage()->Modified(); mitk::ExtractSliceFilter::Pointer extractor2 = mitk::ExtractSliceFilter::New(); extractor2->SetInput( imageOperation->GetImage() ); extractor2->SetTimeStep( imageOperation->GetTimeStep() ); extractor2->SetWorldGeometry( dynamic_cast(imageOperation->GetWorldGeometry()) ); extractor2->SetResliceTransformByGeometry( imageOperation->GetImage()->GetGeometry( imageOperation->GetTimeStep() ) ); extractor2->Modified(); extractor2->Update(); // TODO Move this code to SurfaceInterpolationController! - mitk::Image::Pointer slice = extractor2->GetOutput(); + mitk::Image::Pointer slice2 = extractor2->GetOutput(); mitk::PlaneGeometry::Pointer plane = dynamic_cast(imageOperation->GetWorldGeometry()); - slice->DisconnectPipeline(); - mitk::SegTool2D::UpdateSurfaceInterpolation(slice, imageOperation->GetImage(), plane, true); + slice2->DisconnectPipeline(); + mitk::SegTool2D::UpdateSurfaceInterpolation(slice2, imageOperation->GetImage(), plane, true); } } -//mitk::DiffSliceOperationApplier* mitk::DiffSliceOperationApplier::s_Instance = NULL; - mitk::DiffSliceOperationApplier* mitk::DiffSliceOperationApplier::GetInstance() { - //if(!s_Instance) static DiffSliceOperationApplier* s_Instance = new DiffSliceOperationApplier(); - return s_Instance; } diff --git a/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.h b/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.h index 85465dcd68..c4d7b3593a 100644 --- a/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.h +++ b/Modules/Segmentation/Algorithms/mitkDiffSliceOperationApplier.h @@ -1,64 +1,54 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkDiffSliceOpertationApplier_h_Included #define mitkDiffSliceOpertationApplier_h_Included #include #include "mitkCommon.h" #include -#include "mitkDiffSliceOperation.h" -#include -#include - namespace mitk { /** \brief Executes a DiffSliceOperation. \sa DiffSliceOperation */ class MITKSEGMENTATION_EXPORT DiffSliceOperationApplier : public OperationActor { public: mitkClassMacroNoParent(DiffSliceOperationApplier) - //itkFactorylessNewMacro(Self) - //itkCloneMacro(Self) - /** \brief Returns an instance of the class */ static DiffSliceOperationApplier* GetInstance(); /** \brief Executes a DiffSliceOperation. \sa DiffSliceOperation Note: Only DiffSliceOperation is supported. */ virtual void ExecuteOperation(Operation* op) override; protected: DiffSliceOperationApplier(); virtual ~DiffSliceOperationApplier(); - - //static DiffSliceOperationApplier* s_Instance; - }; } #endif diff --git a/Modules/Segmentation/Interactions/mitkSegTool2D.cpp b/Modules/Segmentation/Interactions/mitkSegTool2D.cpp index 01edecfd8f..ff8e3e4520 100644 --- a/Modules/Segmentation/Interactions/mitkSegTool2D.cpp +++ b/Modules/Segmentation/Interactions/mitkSegTool2D.cpp @@ -1,488 +1,488 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include "mitkSegTool2D.h" #include "mitkToolManager.h" #include "mitkDataStorage.h" #include "mitkBaseRenderer.h" #include "mitkPlaneGeometry.h" #include "mitkExtractImageFilter.h" #include "mitkExtractDirectedPlaneImageFilter.h" //Include of the new ImageExtractor #include "mitkExtractDirectedPlaneImageFilterNew.h" #include "mitkPlanarCircle.h" #include "mitkOverwriteSliceImageFilter.h" #include "mitkOverwriteDirectedPlaneImageFilter.h" #include "mitkMorphologicalOperations.h" #include "usGetModuleContext.h" //Includes for 3DSurfaceInterpolation #include "mitkImageToContourFilter.h" #include "mitkSurfaceInterpolationController.h" #include "mitkImageTimeSelector.h" //includes for resling and overwriting #include #include #include #include #include #include "mitkOperationEvent.h" #include "mitkUndoController.h" #include "mitkAbstractTransformGeometry.h" #define ROUND(a) ((a)>0 ? (int)((a)+0.5) : -(int)(0.5-(a))) bool mitk::SegTool2D::m_SurfaceInterpolationEnabled = true; mitk::SegTool2D::SegTool2D(const char* type) :Tool(type), m_LastEventSender(NULL), m_LastEventSlice(0), m_Contourmarkername ("Position"), m_ShowMarkerNodes (false) { } mitk::SegTool2D::~SegTool2D() { } bool mitk::SegTool2D::FilterEvents(InteractionEvent* interactionEvent, DataNode*dataNode) { const InteractionPositionEvent* positionEvent = dynamic_cast( interactionEvent ); bool isValidEvent = ( positionEvent && // Only events of type mitk::InteractionPositionEvent interactionEvent->GetSender()->GetMapperID() == BaseRenderer::Standard2D // Only events from the 2D renderwindows ); return isValidEvent; } bool mitk::SegTool2D::DetermineAffectedImageSlice( const Image* image, const PlaneGeometry* plane, int& affectedDimension, int& affectedSlice ) { assert(image); assert(plane); // compare normal of plane to the three axis vectors of the image Vector3D normal = plane->GetNormal(); Vector3D imageNormal0 = image->GetSlicedGeometry()->GetAxisVector(0); Vector3D imageNormal1 = image->GetSlicedGeometry()->GetAxisVector(1); Vector3D imageNormal2 = image->GetSlicedGeometry()->GetAxisVector(2); normal.Normalize(); imageNormal0.Normalize(); imageNormal1.Normalize(); imageNormal2.Normalize(); imageNormal0.SetVnlVector( vnl_cross_3d(normal.GetVnlVector(),imageNormal0.GetVnlVector()) ); imageNormal1.SetVnlVector( vnl_cross_3d(normal.GetVnlVector(),imageNormal1.GetVnlVector()) ); imageNormal2.SetVnlVector( vnl_cross_3d(normal.GetVnlVector(),imageNormal2.GetVnlVector()) ); double eps( 0.00001 ); // axial if ( imageNormal2.GetNorm() <= eps ) { affectedDimension = 2; } // sagittal else if ( imageNormal1.GetNorm() <= eps ) { affectedDimension = 1; } // frontal else if ( imageNormal0.GetNorm() <= eps ) { affectedDimension = 0; } else { affectedDimension = -1; // no idea return false; } // determine slice number in image BaseGeometry* imageGeometry = image->GetGeometry(0); Point3D testPoint = imageGeometry->GetCenter(); Point3D projectedPoint; plane->Project( testPoint, projectedPoint ); Point3D indexPoint; imageGeometry->WorldToIndex( projectedPoint, indexPoint ); affectedSlice = ROUND( indexPoint[affectedDimension] ); MITK_DEBUG << "indexPoint " << indexPoint << " affectedDimension " << affectedDimension << " affectedSlice " << affectedSlice; // check if this index is still within the image if ( affectedSlice < 0 || affectedSlice >= static_cast(image->GetDimension(affectedDimension)) ) return false; return true; } void mitk::SegTool2D::UpdateSurfaceInterpolation (const Image* slice, const Image* workingImage, const PlaneGeometry *plane, bool detectIntersection) { if (!m_SurfaceInterpolationEnabled) return; ImageToContourFilter::Pointer contourExtractor = ImageToContourFilter::New(); mitk::Surface::Pointer contour; if (detectIntersection) { // Test whether there is something to extract or whether the slice just contains intersections of others mitk::Image::Pointer slice2 = slice->Clone(); mitk::MorphologicalOperations::Erode(slice2, 2, mitk::MorphologicalOperations::Ball); contourExtractor->SetInput(slice2); contourExtractor->Update(); contour = contourExtractor->GetOutput(); if (contour->GetVtkPolyData()->GetNumberOfPoints() == 0) { // Remove contour! mitk::SurfaceInterpolationController::ContourPositionInformation contourInfo; contourInfo.contourNormal = plane->GetNormal(); contourInfo.contourPoint = plane->GetOrigin(); mitk::SurfaceInterpolationController::GetInstance()->RemoveContour(contourInfo); return; } } contourExtractor->SetInput(slice); contourExtractor->Update(); contour = contourExtractor->GetOutput(); mitk::ImageTimeSelector::Pointer timeSelector = mitk::ImageTimeSelector::New(); timeSelector->SetInput( workingImage ); timeSelector->SetTimeNr( 0 ); timeSelector->SetChannelNr( 0 ); timeSelector->Update(); Image::Pointer dimRefImg = timeSelector->GetOutput(); if (contour->GetVtkPolyData()->GetNumberOfPoints() != 0 && dimRefImg->GetDimension() == 3) { mitk::SurfaceInterpolationController::GetInstance()->AddNewContour( contour ); contour->DisconnectPipeline(); } else { // Remove contour! mitk::SurfaceInterpolationController::ContourPositionInformation contourInfo; contourInfo.contourNormal = plane->GetNormal(); contourInfo.contourPoint = plane->GetOrigin(); mitk::SurfaceInterpolationController::GetInstance()->RemoveContour(contourInfo); } } mitk::Image::Pointer mitk::SegTool2D::GetAffectedImageSliceAs2DImage(const InteractionPositionEvent* positionEvent, const Image* image) { if (!positionEvent) return NULL; assert( positionEvent->GetSender() ); // sure, right? unsigned int timeStep = positionEvent->GetSender()->GetTimeStep( image ); // get the timestep of the visible part (time-wise) of the image return this->GetAffectedImageSliceAs2DImage(positionEvent->GetSender()->GetCurrentWorldPlaneGeometry(), image, timeStep); } mitk::Image::Pointer mitk::SegTool2D::GetAffectedImageSliceAs2DImage(const PlaneGeometry* planeGeometry, const Image* image, unsigned int timeStep) { if ( !image || !planeGeometry ) return NULL; //Make sure that for reslicing and overwriting the same alogrithm is used. We can specify the mode of the vtk reslicer vtkSmartPointer reslice = vtkSmartPointer::New(); //set to false to extract a slice reslice->SetOverwriteMode(false); reslice->Modified(); //use ExtractSliceFilter with our specific vtkImageReslice for overwriting and extracting mitk::ExtractSliceFilter::Pointer extractor = mitk::ExtractSliceFilter::New(reslice); extractor->SetInput( image ); extractor->SetTimeStep( timeStep ); extractor->SetWorldGeometry( planeGeometry ); extractor->SetVtkOutputRequest(false); extractor->SetResliceTransformByGeometry( image->GetTimeGeometry()->GetGeometryForTimeStep( timeStep ) ); extractor->Modified(); extractor->Update(); Image::Pointer slice = extractor->GetOutput(); return slice; } mitk::Image::Pointer mitk::SegTool2D::GetAffectedWorkingSlice(const InteractionPositionEvent* positionEvent) { DataNode* workingNode( m_ToolManager->GetWorkingData(0) ); if ( !workingNode ) return NULL; Image* workingImage = dynamic_cast(workingNode->GetData()); if ( !workingImage ) return NULL; return GetAffectedImageSliceAs2DImage( positionEvent, workingImage ); } mitk::Image::Pointer mitk::SegTool2D::GetAffectedReferenceSlice(const InteractionPositionEvent* positionEvent) { DataNode* referenceNode( m_ToolManager->GetReferenceData(0) ); if ( !referenceNode ) return NULL; Image* referenceImage = dynamic_cast(referenceNode->GetData()); if ( !referenceImage ) return NULL; return GetAffectedImageSliceAs2DImage( positionEvent, referenceImage ); } void mitk::SegTool2D::WriteBackSegmentationResult (const InteractionPositionEvent* positionEvent, Image* slice) { if(!positionEvent) return; const PlaneGeometry* planeGeometry( dynamic_cast (positionEvent->GetSender()->GetCurrentWorldPlaneGeometry() ) ); const AbstractTransformGeometry* abstractTransformGeometry( dynamic_cast (positionEvent->GetSender()->GetCurrentWorldPlaneGeometry() ) ); if( planeGeometry && slice && !abstractTransformGeometry) { DataNode* workingNode( m_ToolManager->GetWorkingData(0) ); Image* image = dynamic_cast(workingNode->GetData()); unsigned int timeStep = positionEvent->GetSender()->GetTimeStep( image ); this->WriteBackSegmentationResult(planeGeometry, slice, timeStep); } } void mitk::SegTool2D::WriteBackSegmentationResult (const PlaneGeometry* planeGeometry, Image* slice, unsigned int timeStep) { if(!planeGeometry || !slice) return; SliceInformation sliceInfo (slice, const_cast(planeGeometry), timeStep); this->WriteSliceToVolume(sliceInfo); DataNode* workingNode( m_ToolManager->GetWorkingData(0) ); Image* image = dynamic_cast(workingNode->GetData()); this->UpdateSurfaceInterpolation(slice, image, planeGeometry, false); if (m_SurfaceInterpolationEnabled) this->AddContourmarker(); mitk::RenderingManager::GetInstance()->RequestUpdateAll(); } void mitk::SegTool2D::WriteBackSegmentationResult(std::vector sliceList, bool writeSliceToVolume) { std::vector contourList; contourList.reserve(sliceList.size()); ImageToContourFilter::Pointer contourExtractor = ImageToContourFilter::New(); DataNode* workingNode( m_ToolManager->GetWorkingData(0) ); Image* image = dynamic_cast(workingNode->GetData()); mitk::ImageTimeSelector::Pointer timeSelector = mitk::ImageTimeSelector::New(); timeSelector->SetInput( image ); timeSelector->SetTimeNr( 0 ); timeSelector->SetChannelNr( 0 ); timeSelector->Update(); Image::Pointer dimRefImg = timeSelector->GetOutput(); for (unsigned int i = 0; i < sliceList.size(); ++i) { SliceInformation currentSliceInfo = sliceList.at(i); if(writeSliceToVolume) this->WriteSliceToVolume(currentSliceInfo); if (m_SurfaceInterpolationEnabled && dimRefImg->GetDimension() == 3) { currentSliceInfo.slice->DisconnectPipeline(); contourExtractor->SetInput(currentSliceInfo.slice); contourExtractor->Update(); mitk::Surface::Pointer contour = contourExtractor->GetOutput(); contour->DisconnectPipeline(); contourList.push_back(contour); } } mitk::SurfaceInterpolationController::GetInstance()->AddNewContours(contourList); mitk::RenderingManager::GetInstance()->RequestUpdateAll(); } void mitk::SegTool2D::WriteSliceToVolume(mitk::SegTool2D::SliceInformation sliceInfo) { DataNode* workingNode( m_ToolManager->GetWorkingData(0) ); Image* image = dynamic_cast(workingNode->GetData()); /*============= BEGIN undo/redo feature block ========================*/ // Create undo operation by caching the not yet modified slices mitk::Image::Pointer originalSlice = GetAffectedImageSliceAs2DImage(sliceInfo.plane, image, sliceInfo.timestep); - DiffSliceOperation* undoOperation = new DiffSliceOperation(const_cast(image), originalSlice->GetVtkImageData(), dynamic_cast(originalSlice->GetGeometry()), sliceInfo.timestep, sliceInfo.plane); + DiffSliceOperation* undoOperation = new DiffSliceOperation(const_cast(image), originalSlice, dynamic_cast(originalSlice->GetGeometry()), sliceInfo.timestep, sliceInfo.plane); /*============= END undo/redo feature block ========================*/ //Make sure that for reslicing and overwriting the same alogrithm is used. We can specify the mode of the vtk reslicer vtkSmartPointer reslice = vtkSmartPointer::New(); //Set the slice as 'input' reslice->SetInputSlice(sliceInfo.slice->GetVtkImageData()); //set overwrite mode to true to write back to the image volume reslice->SetOverwriteMode(true); reslice->Modified(); mitk::ExtractSliceFilter::Pointer extractor = mitk::ExtractSliceFilter::New(reslice); extractor->SetInput( image ); extractor->SetTimeStep( sliceInfo.timestep ); extractor->SetWorldGeometry( sliceInfo.plane ); - extractor->SetVtkOutputRequest(true); + extractor->SetVtkOutputRequest(false); extractor->SetResliceTransformByGeometry( image->GetGeometry( sliceInfo.timestep ) ); extractor->Modified(); extractor->Update(); //the image was modified within the pipeline, but not marked so image->Modified(); image->GetVtkImageData()->Modified(); /*============= BEGIN undo/redo feature block ========================*/ //specify the undo operation with the edited slice - DiffSliceOperation* doOperation = new DiffSliceOperation(image, extractor->GetVtkOutput(),dynamic_cast(sliceInfo.slice->GetGeometry()), sliceInfo.timestep, sliceInfo.plane); + DiffSliceOperation* doOperation = new DiffSliceOperation(image, extractor->GetOutput(),dynamic_cast(sliceInfo.slice->GetGeometry()), sliceInfo.timestep, sliceInfo.plane); //create an operation event for the undo stack OperationEvent* undoStackItem = new OperationEvent( DiffSliceOperationApplier::GetInstance(), doOperation, undoOperation, "Segmentation" ); //add it to the undo controller UndoController::GetCurrentUndoModel()->SetOperationEvent( undoStackItem ); //clear the pointers as the operation are stored in the undocontroller and also deleted from there undoOperation = NULL; doOperation = NULL; /*============= END undo/redo feature block ========================*/ } void mitk::SegTool2D::SetShowMarkerNodes(bool status) { m_ShowMarkerNodes = status; } void mitk::SegTool2D::SetEnable3DInterpolation(bool enabled) { m_SurfaceInterpolationEnabled = enabled; } int mitk::SegTool2D::AddContourmarker() { if (m_LastEventSender == NULL) return -1; us::ServiceReference serviceRef = us::GetModuleContext()->GetServiceReference(); PlanePositionManagerService* service = us::GetModuleContext()->GetService(serviceRef); unsigned int slicePosition = m_LastEventSender->GetSliceNavigationController()->GetSlice()->GetPos(); // the first geometry is needed otherwise restoring the position is not working const mitk::PlaneGeometry* plane = dynamic_cast (dynamic_cast< const mitk::SlicedGeometry3D*>( m_LastEventSender->GetSliceNavigationController()->GetCurrentGeometry3D())->GetPlaneGeometry(0)); unsigned int size = service->GetNumberOfPlanePositions(); unsigned int id = service->AddNewPlanePosition(plane, slicePosition); mitk::PlanarCircle::Pointer contourMarker = mitk::PlanarCircle::New(); mitk::Point2D p1; plane->Map(plane->GetCenter(), p1); mitk::Point2D p2 = p1; p2[0] -= plane->GetSpacing()[0]; p2[1] -= plane->GetSpacing()[1]; contourMarker->PlaceFigure( p1 ); contourMarker->SetCurrentControlPoint( p1 ); contourMarker->SetPlaneGeometry( const_cast(plane)); std::stringstream markerStream; mitk::DataNode* workingNode (m_ToolManager->GetWorkingData(0)); markerStream << m_Contourmarkername ; markerStream << " "; markerStream << id+1; DataNode::Pointer rotatedContourNode = DataNode::New(); rotatedContourNode->SetData(contourMarker); rotatedContourNode->SetProperty( "name", StringProperty::New(markerStream.str()) ); rotatedContourNode->SetProperty( "isContourMarker", BoolProperty::New(true)); rotatedContourNode->SetBoolProperty( "PlanarFigureInitializedWindow", true, m_LastEventSender ); rotatedContourNode->SetProperty( "includeInBoundingBox", BoolProperty::New(false)); rotatedContourNode->SetProperty( "helper object", mitk::BoolProperty::New(!m_ShowMarkerNodes)); rotatedContourNode->SetProperty( "planarfigure.drawcontrolpoints", BoolProperty::New(false)); rotatedContourNode->SetProperty( "planarfigure.drawname", BoolProperty::New(false)); rotatedContourNode->SetProperty( "planarfigure.drawoutline", BoolProperty::New(false)); rotatedContourNode->SetProperty( "planarfigure.drawshadow", BoolProperty::New(false)); if (plane) { if ( id == size ) { m_ToolManager->GetDataStorage()->Add(rotatedContourNode, workingNode); } else { mitk::NodePredicateProperty::Pointer isMarker = mitk::NodePredicateProperty::New("isContourMarker", mitk::BoolProperty::New(true)); mitk::DataStorage::SetOfObjects::ConstPointer markers = m_ToolManager->GetDataStorage()->GetDerivations(workingNode,isMarker); for ( mitk::DataStorage::SetOfObjects::const_iterator iter = markers->begin(); iter != markers->end(); ++iter) { std::string nodeName = (*iter)->GetName(); unsigned int t = nodeName.find_last_of(" "); unsigned int markerId = atof(nodeName.substr(t+1).c_str())-1; if(id == markerId) { return id; } } m_ToolManager->GetDataStorage()->Add(rotatedContourNode, workingNode); } } return id; } void mitk::SegTool2D::InteractiveSegmentationBugMessage( const std::string& message ) { MITK_ERROR << "********************************************************************************" << std::endl << " " << message << std::endl << "********************************************************************************" << std::endl << " " << std::endl << " If your image is rotated or the 2D views don't really contain the patient image, try to press the button next to the image selection. " << std::endl << " " << std::endl << " Please file a BUG REPORT: " << std::endl << " http://bugs.mitk.org" << std::endl << " Contain the following information:" << std::endl << " - What image were you working on?" << std::endl << " - Which region of the image?" << std::endl << " - Which tool did you use?" << std::endl << " - What did you do?" << std::endl << " - What happened (not)? What did you expect?" << std::endl; }