diff --git a/Modules/Core/files.cmake b/Modules/Core/files.cmake index f1bd7b5ec4..ea75df2801 100644 --- a/Modules/Core/files.cmake +++ b/Modules/Core/files.cmake @@ -1,330 +1,331 @@ file(GLOB_RECURSE H_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include/*") set(CPP_FILES mitkCoreActivator.cpp mitkCoreObjectFactoryBase.cpp mitkCoreObjectFactory.cpp mitkCoreServices.cpp mitkException.cpp Algorithms/mitkBaseDataSource.cpp Algorithms/mitkClippedSurfaceBoundsCalculator.cpp Algorithms/mitkCompareImageDataFilter.cpp Algorithms/mitkCompositePixelValueToString.cpp Algorithms/mitkConvert2Dto3DImageFilter.cpp Algorithms/mitkDataNodeSource.cpp Algorithms/mitkExtractSliceFilter.cpp Algorithms/mitkExtractSliceFilter2.cpp Algorithms/mitkHistogramGenerator.cpp Algorithms/mitkImageChannelSelector.cpp Algorithms/mitkImageSliceSelector.cpp Algorithms/mitkImageSource.cpp Algorithms/mitkImageTimeSelector.cpp Algorithms/mitkImageToImageFilter.cpp Algorithms/mitkImageToSurfaceFilter.cpp Algorithms/mitkMultiComponentImageDataComparisonFilter.cpp Algorithms/mitkPlaneGeometryDataToSurfaceFilter.cpp Algorithms/mitkPointSetSource.cpp Algorithms/mitkPointSetToPointSetFilter.cpp Algorithms/mitkRGBToRGBACastImageFilter.cpp Algorithms/mitkSubImageSelector.cpp Algorithms/mitkSurfaceSource.cpp Algorithms/mitkSurfaceToImageFilter.cpp Algorithms/mitkSurfaceToSurfaceFilter.cpp Algorithms/mitkUIDGenerator.cpp Algorithms/mitkVolumeCalculator.cpp Algorithms/mitkTemporalJoinImagesFilter.cpp Controllers/mitkBaseController.cpp Controllers/mitkCallbackFromGUIThread.cpp Controllers/mitkCameraController.cpp Controllers/mitkCameraRotationController.cpp Controllers/mitkCrosshairManager.cpp Controllers/mitkLimitedLinearUndo.cpp Controllers/mitkOperationEvent.cpp Controllers/mitkPlanePositionManager.cpp Controllers/mitkProgressBar.cpp Controllers/mitkRenderingManager.cpp Controllers/mitkSliceNavigationController.cpp Controllers/mitkSliceNavigationHelper.cpp Controllers/mitkStatusBar.cpp Controllers/mitkStepper.cpp Controllers/mitkTestManager.cpp Controllers/mitkTimeNavigationController.cpp Controllers/mitkUndoController.cpp Controllers/mitkVerboseLimitedLinearUndo.cpp Controllers/mitkVtkLayerController.cpp DataManagement/mitkAffineTransform3D.cpp DataManagement/mitkAnatomicalStructureColorPresets.cpp DataManagement/mitkArbitraryTimeGeometry.cpp DataManagement/mitkAbstractTransformGeometry.cpp DataManagement/mitkAnnotationProperty.cpp DataManagement/mitkApplicationCursor.cpp DataManagement/mitkApplyTransformMatrixOperation.cpp DataManagement/mitkBaseData.cpp DataManagement/mitkBaseGeometry.cpp DataManagement/mitkBaseProperty.cpp DataManagement/mitkChannelDescriptor.cpp DataManagement/mitkClippingProperty.cpp DataManagement/mitkColorProperty.cpp DataManagement/mitkCrosshairData.cpp DataManagement/mitkDataNode.cpp DataManagement/mitkDataStorage.cpp DataManagement/mitkEnumerationProperty.cpp DataManagement/mitkFloatPropertyExtension.cpp DataManagement/mitkGeometry3D.cpp DataManagement/mitkGeometryData.cpp DataManagement/mitkGeometryTransformHolder.cpp DataManagement/mitkGroupTagProperty.cpp DataManagement/mitkGenericIDRelationRule.cpp DataManagement/mitkIdentifiable.cpp DataManagement/mitkImageAccessorBase.cpp DataManagement/mitkImageCaster.cpp DataManagement/mitkImageCastPart1.cpp DataManagement/mitkImageCastPart2.cpp DataManagement/mitkImageCastPart3.cpp DataManagement/mitkImageCastPart4.cpp DataManagement/mitkImage.cpp DataManagement/mitkImageDataItem.cpp DataManagement/mitkImageDescriptor.cpp DataManagement/mitkImageReadAccessor.cpp DataManagement/mitkImageStatisticsHolder.cpp DataManagement/mitkImageVtkAccessor.cpp DataManagement/mitkImageVtkReadAccessor.cpp DataManagement/mitkImageVtkWriteAccessor.cpp DataManagement/mitkImageWriteAccessor.cpp DataManagement/mitkIntPropertyExtension.cpp DataManagement/mitkIPersistenceService.cpp DataManagement/mitkIPropertyAliases.cpp DataManagement/mitkIPropertyDescriptions.cpp DataManagement/mitkIPropertyDeserialization.cpp DataManagement/mitkIPropertyExtensions.cpp DataManagement/mitkIPropertyFilters.cpp DataManagement/mitkIPropertyOwner.cpp DataManagement/mitkIPropertyPersistence.cpp DataManagement/mitkIPropertyProvider.cpp DataManagement/mitkITKEventObserverGuard.cpp DataManagement/mitkLandmarkProjectorBasedCurvedGeometry.cpp DataManagement/mitkLandmarkProjector.cpp DataManagement/mitkLevelWindow.cpp DataManagement/mitkLevelWindowManager.cpp DataManagement/mitkLevelWindowPreset.cpp DataManagement/mitkLevelWindowProperty.cpp DataManagement/mitkLookupTable.cpp DataManagement/mitkLookupTableProperty.cpp DataManagement/mitkLookupTables.cpp DataManagement/mitkMaterial.cpp DataManagement/mitkMemoryUtilities.cpp DataManagement/mitkModalityProperty.cpp DataManagement/mitkModifiedLock.cpp DataManagement/mitkNodePredicateAnd.cpp DataManagement/mitkNodePredicateBase.cpp DataManagement/mitkNodePredicateCompositeBase.cpp DataManagement/mitkNodePredicateData.cpp DataManagement/mitkNodePredicateDataType.cpp DataManagement/mitkNodePredicateDataUID.cpp DataManagement/mitkNodePredicateDimension.cpp DataManagement/mitkNodePredicateFunction.cpp DataManagement/mitkNodePredicateGeometry.cpp DataManagement/mitkNodePredicateNot.cpp DataManagement/mitkNodePredicateOr.cpp DataManagement/mitkNodePredicateProperty.cpp DataManagement/mitkNodePredicateDataProperty.cpp DataManagement/mitkNodePredicateSubGeometry.cpp DataManagement/mitkNumericConstants.cpp DataManagement/mitkPlaneGeometry.cpp DataManagement/mitkPlaneGeometryData.cpp DataManagement/mitkPlaneOperation.cpp DataManagement/mitkPlaneOrientationProperty.cpp DataManagement/mitkPointOperation.cpp DataManagement/mitkPointSet.cpp DataManagement/mitkPointSetShapeProperty.cpp DataManagement/mitkProperties.cpp DataManagement/mitkPropertyAliases.cpp DataManagement/mitkPropertyDescriptions.cpp DataManagement/mitkPropertyDeserialization.cpp DataManagement/mitkPropertyExtension.cpp DataManagement/mitkPropertyExtensions.cpp DataManagement/mitkPropertyFilter.cpp DataManagement/mitkPropertyFilters.cpp DataManagement/mitkPropertyKeyPath.cpp DataManagement/mitkPropertyList.cpp DataManagement/mitkPropertyListReplacedObserver.cpp DataManagement/mitkPropertyNameHelper.cpp DataManagement/mitkPropertyObserver.cpp DataManagement/mitkPropertyPersistence.cpp DataManagement/mitkPropertyPersistenceInfo.cpp DataManagement/mitkPropertyRelationRuleBase.cpp DataManagement/mitkProportionalTimeGeometry.cpp DataManagement/mitkRenderingModeProperty.cpp DataManagement/mitkResliceMethodProperty.cpp DataManagement/mitkRestorePlanePositionOperation.cpp DataManagement/mitkRotationOperation.cpp DataManagement/mitkScaleOperation.cpp DataManagement/mitkSlicedData.cpp DataManagement/mitkSlicedGeometry3D.cpp DataManagement/mitkSmartPointerProperty.cpp DataManagement/mitkStandaloneDataStorage.cpp DataManagement/mitkStringProperty.cpp DataManagement/mitkSurface.cpp DataManagement/mitkSurfaceOperation.cpp DataManagement/mitkSourceImageRelationRule.cpp DataManagement/mitkThinPlateSplineCurvedGeometry.cpp DataManagement/mitkTimeGeometry.cpp DataManagement/mitkTransferFunction.cpp DataManagement/mitkTransferFunctionInitializer.cpp DataManagement/mitkTransferFunctionProperty.cpp DataManagement/mitkTemporoSpatialStringProperty.cpp DataManagement/mitkUIDManipulator.cpp DataManagement/mitkVectorProperty.cpp DataManagement/mitkVtkInterpolationProperty.cpp DataManagement/mitkVtkRepresentationProperty.cpp DataManagement/mitkVtkResliceInterpolationProperty.cpp DataManagement/mitkVtkScalarModeProperty.cpp DataManagement/mitkWeakPointerProperty.cpp DataManagement/mitkIPropertyRelations.cpp DataManagement/mitkPropertyRelations.cpp Interactions/mitkAction.cpp Interactions/mitkBindDispatcherInteractor.cpp Interactions/mitkDataInteractor.cpp Interactions/mitkDispatcher.cpp Interactions/mitkDisplayActionEventBroadcast.cpp Interactions/mitkDisplayActionEventFunctions.cpp Interactions/mitkDisplayActionEventHandler.cpp Interactions/mitkDisplayActionEventHandlerDesynchronized.cpp Interactions/mitkDisplayActionEventHandlerStd.cpp Interactions/mitkDisplayActionEventHandlerSynchronized.cpp Interactions/mitkDisplayCoordinateOperation.cpp Interactions/mitkEventConfig.cpp Interactions/mitkEventFactory.cpp Interactions/mitkEventRecorder.cpp Interactions/mitkEventStateMachine.cpp Interactions/mitkInteractionEventConst.cpp Interactions/mitkInteractionEvent.cpp Interactions/mitkInteractionEventHandler.cpp Interactions/mitkInteractionEventObserver.cpp Interactions/mitkInteractionKeyEvent.cpp Interactions/mitkInteractionPositionEvent.cpp Interactions/mitkInteractionSchemeSwitcher.cpp Interactions/mitkInternalEvent.cpp Interactions/mitkMouseDoubleClickEvent.cpp Interactions/mitkMouseMoveEvent.cpp Interactions/mitkMousePressEvent.cpp Interactions/mitkMouseReleaseEvent.cpp Interactions/mitkMouseWheelEvent.cpp Interactions/mitkPointSetDataInteractor.cpp Interactions/mitkSinglePointDataInteractor.cpp Interactions/mitkStateMachineAction.cpp Interactions/mitkStateMachineCondition.cpp Interactions/mitkStateMachineContainer.cpp Interactions/mitkStateMachineState.cpp Interactions/mitkStateMachineTransition.cpp Interactions/mitkVtkEventAdapter.cpp Interactions/mitkVtkInteractorStyle.cxx Interactions/mitkXML2EventParser.cpp IO/mitkAbstractFileIO.cpp IO/mitkAbstractFileReader.cpp IO/mitkAbstractFileWriter.cpp IO/mitkCustomMimeType.cpp IO/mitkFileReader.cpp IO/mitkFileReaderRegistry.cpp IO/mitkFileReaderSelector.cpp IO/mitkFileReaderWriterBase.cpp IO/mitkFileWriter.cpp IO/mitkFileWriterRegistry.cpp IO/mitkFileWriterSelector.cpp IO/mitkGeometry3DToXML.cpp IO/mitkIFileIO.cpp IO/mitkIFileReader.cpp IO/mitkIFileWriter.cpp IO/mitkGeometryDataReaderService.cpp IO/mitkGeometryDataWriterService.cpp IO/mitkImageVtkLegacyIO.cpp IO/mitkImageVtkXmlIO.cpp IO/mitkIMimeTypeProvider.cpp IO/mitkIOConstants.cpp IO/mitkIOMimeTypes.cpp IO/mitkIOUtil.cpp + IO/mitkIOVolumeSplitReason.cpp IO/mitkItkImageIO.cpp IO/mitkItkLoggingAdapter.cpp IO/mitkLegacyFileReaderService.cpp IO/mitkLegacyFileWriterService.cpp IO/mitkLocaleSwitch.cpp IO/mitkLogBackend.cpp IO/mitkMimeType.cpp IO/mitkMimeTypeProvider.cpp IO/mitkOperation.cpp IO/mitkPixelType.cpp IO/mitkPointSetReaderService.cpp IO/mitkPointSetWriterService.cpp IO/mitkProportionalTimeGeometryToXML.cpp IO/mitkRawImageFileReader.cpp IO/mitkStandardFileLocations.cpp IO/mitkSurfaceStlIO.cpp IO/mitkSurfaceVtkIO.cpp IO/mitkSurfaceVtkLegacyIO.cpp IO/mitkSurfaceVtkXmlIO.cpp IO/mitkUtf8Util.cpp IO/mitkVtkLoggingAdapter.cpp IO/mitkPreferenceListReaderOptionsFunctor.cpp IO/mitkIOMetaInformationPropertyConstants.cpp IO/mitkIPreferences.cpp IO/mitkPreferences.cpp IO/mitkIPreferencesService.cpp IO/mitkPreferencesService.cpp IO/mitkIPreferencesStorage.cpp IO/mitkXMLPreferencesStorage.cpp Rendering/mitkAbstractAnnotationRenderer.cpp Rendering/mitkAnnotationUtils.cpp Rendering/mitkBaseRenderer.cpp Rendering/mitkBaseRendererHelper.cpp Rendering/mitkCrosshairVtkMapper2D.cpp Rendering/mitkGradientBackground.cpp Rendering/mitkImageVtkMapper2D.cpp Rendering/mitkMapper.cpp Rendering/mitkAnnotation.cpp Rendering/mitkPlaneGeometryDataMapper2D.cpp Rendering/mitkPlaneGeometryDataVtkMapper3D.cpp Rendering/mitkPointSetVtkMapper2D.cpp Rendering/mitkPointSetVtkMapper3D.cpp Rendering/mitkRenderWindowBase.cpp Rendering/mitkRenderWindow.cpp Rendering/mitkRenderWindowFrame.cpp Rendering/mitkSurfaceVtkMapper2D.cpp Rendering/mitkSurfaceVtkMapper3D.cpp Rendering/mitkVideoRecorder.cpp Rendering/mitkVtkEventProvider.cpp Rendering/mitkVtkMapper.cpp Rendering/mitkVtkPropRenderer.cpp Rendering/mitkVtkWidgetRendering.cpp Rendering/vtkMitkLevelWindowFilter.cpp Rendering/vtkMitkRectangleProp.cpp Rendering/vtkMitkRenderProp.cpp Rendering/vtkMitkThickSlicesFilter.cpp Rendering/vtkNeverTranslucentTexture.cpp ) set(RESOURCE_FILES Interactions/globalConfig.xml Interactions/DisplayInteraction.xml Interactions/DisplayConfigMITKBase.xml Interactions/DisplayConfigPACSBase.xml Interactions/DisplayConfigCrosshair.xml Interactions/DisplayConfigRotation.xml Interactions/DisplayConfigActivateCoupling.xml Interactions/DisplayConfigSwivel.xml Interactions/DisplayConfigPACSPan.xml Interactions/DisplayConfigPACSScroll.xml Interactions/DisplayConfigPACSZoom.xml Interactions/DisplayConfigPACSLevelWindow.xml Interactions/DisplayConfigBlockLMB.xml Interactions/PointSet.xml Interactions/PointSetConfig.xml mitkLevelWindowPresets.xml mitkAnatomicalStructureColorPresets.xml ) diff --git a/Modules/Core/include/mitkIOMetaInformationPropertyConstants.h b/Modules/Core/include/mitkIOMetaInformationPropertyConstants.h index 854d6cd673..41991a3f02 100644 --- a/Modules/Core/include/mitkIOMetaInformationPropertyConstants.h +++ b/Modules/Core/include/mitkIOMetaInformationPropertyConstants.h @@ -1,44 +1,46 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkIOMetaInformationPropertyConstants_h #define mitkIOMetaInformationPropertyConstants_h #include #include "mitkPropertyKeyPath.h" namespace mitk { /** * @ingroup IO * @brief The IOMetaInformationPropertyConstants struct */ struct MITKCORE_EXPORT IOMetaInformationPropertyConstants { //Path to the property containing the name of the reader used static PropertyKeyPath READER_DESCRIPTION(); //Path to the property containing the version of mitk used to read the data static PropertyKeyPath READER_VERSION(); //Path to the property containing the mine name detected used to read the data static PropertyKeyPath READER_MIME_NAME(); //Path to the property containing the mime category detected to read the data static PropertyKeyPath READER_MIME_CATEGORY(); //Path to the property containing the input location if loaded by file used to read the data static PropertyKeyPath READER_INPUTLOCATION(); + //Path to the property containing split reason information for the read volume + static PropertyKeyPath VOLUME_SPLIT_REASON(); //Path to the properties containing the reader options used to read the data static PropertyKeyPath READER_OPTION_ROOT(); static PropertyKeyPath READER_OPTIONS_ANY(); }; } #endif diff --git a/Modules/Core/include/mitkIOVolumeSplitReason.h b/Modules/Core/include/mitkIOVolumeSplitReason.h new file mode 100644 index 0000000000..b27a9e96ef --- /dev/null +++ b/Modules/Core/include/mitkIOVolumeSplitReason.h @@ -0,0 +1,105 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#ifndef mitkIOVolumeSplitReason_h +#define mitkIOVolumeSplitReason_h + +#include +#include +#include + +#include "MitkCoreExports.h" + +namespace mitk +{ + + class MITKCORE_EXPORT IOVolumeSplitReason + { + public: + using Self = IOVolumeSplitReason; + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + enum class ReasonType + { + Unknown = 0, + ValueSplitDifference, //*< split due to different values in splitting relevant dicom tags + ValueSortDistance, //*< split due value distance of sort criterion too large for relevant dicom tag(s) + ImagePostionMissing, //*< split because image position tag was missing in one of the compared files + OverlappingSlices, //*< split because at least two input files are overlapping in world coordinate space + GantryTiltDifference, //*< split because the gantry tilts of at least two input files were different + SliceDistanceInconsistency, //*< split because the distance between slices were inconsistent. + // This can either be evoked by volumes with heterogeneous z spacing or by missing slices. + // Details for this reason will contain the detected slice distance inconsistency + MissingSlices //*< Indicates that is a split was done due to missing slices. (It is a sub class of SliceDistanceInconsistency + // as all SliceDistanceInconsistency with a positive distance inconsistency greater then one times the slice + // thickness are deemed also missing slices as split reason. This sub class was introduced to make it easier + // for parsing applications to react on this important split reason. + // Details for this reason will contain the assumed/detected number of missing slices + }; + + void AddReason(ReasonType type, const std::string& detail = ""); + void RemoveReason(ReasonType type); + + bool HasReasons() const; + bool HasReason(ReasonType type) const; + std::string GetReasonDetails(ReasonType type) const; + + /** This methods generates a clone of this instances and extends the clone by the reason types and details + provided by another IOVolumeSplitReason instance. + @remark If the other instance contains a reason type that is already existing it will be ignored. + Therefor only new types and details will be added to the extension. + @pre otherReason must point to a valid instance. + @return Pointer to the cloned and extended instance.*/ + Pointer ExtendReason(ConstPointer otherReason) const; + + static nlohmann::json ToJSON(ConstPointer); + static Pointer FromJSON(const nlohmann::json& j); + + static std::string TypeToString(ReasonType reasonType); + static IOVolumeSplitReason::ReasonType StringToType(const std::string& reasonStr); + + Pointer Clone() const; + static Pointer New(); + + protected: + using ReasonMapType = std::map; + ReasonMapType m_ReasonMap; + }; + + template + inline void to_json(BasicJsonType& j, const IOVolumeSplitReason::ReasonType& e) + { + static_assert(std::is_enum::value, + "IOVolumeSplitReason::ReasonType" + " must be an enum!"); + j = IOVolumeSplitReason::TypeToString(e); + } + + template + inline void from_json(const BasicJsonType& j, IOVolumeSplitReason::ReasonType& e) + { + static_assert(std::is_enum::value, + "IOVolumeSplitReason::ReasonType" + " must be an enum!"); + e = IOVolumeSplitReason::StringToType(j.template get()); + } + + + MITKCORE_EXPORT void to_json(nlohmann::json& j, IOVolumeSplitReason::ConstPointer reason); + MITKCORE_EXPORT void to_json(nlohmann::json& j, IOVolumeSplitReason::Pointer reason); + + MITKCORE_EXPORT void from_json(const nlohmann::json& j, IOVolumeSplitReason& reason); + +} + +#endif diff --git a/Modules/Core/src/IO/mitkIOMetaInformationPropertyConstants.cpp b/Modules/Core/src/IO/mitkIOMetaInformationPropertyConstants.cpp index a57c640107..dd0b2e1c04 100644 --- a/Modules/Core/src/IO/mitkIOMetaInformationPropertyConstants.cpp +++ b/Modules/Core/src/IO/mitkIOMetaInformationPropertyConstants.cpp @@ -1,52 +1,57 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkIOMetaInformationPropertyConstants.h" namespace mitk { PropertyKeyPath IOMetaInformationPropertyConstants::READER_DESCRIPTION() { return PropertyKeyPath({ "MITK", "IO", "reader", "description" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_VERSION() { return PropertyKeyPath({ "MITK", "IO", "reader", "version" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_MIME_NAME() { return PropertyKeyPath({ "MITK", "IO", "reader", "mime", "name" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_MIME_CATEGORY() { return PropertyKeyPath({ "MITK", "IO", "reader", "mime", "category" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_INPUTLOCATION() { return PropertyKeyPath({ "MITK", "IO", "reader", "inputlocation" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_OPTION_ROOT() { return PropertyKeyPath({ "MITK", "IO", "reader", "option" }); } PropertyKeyPath IOMetaInformationPropertyConstants::READER_OPTIONS_ANY() { return READER_OPTION_ROOT().AddAnyElement(); } + + PropertyKeyPath IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON() + { + return PropertyKeyPath({ "MITK", "IO", "reader", "VolumeSplitReason" }); + } } diff --git a/Modules/Core/src/IO/mitkIOMimeTypes.cpp b/Modules/Core/src/IO/mitkIOMimeTypes.cpp index 01a3d23c15..17b007a6f1 100644 --- a/Modules/Core/src/IO/mitkIOMimeTypes.cpp +++ b/Modules/Core/src/IO/mitkIOMimeTypes.cpp @@ -1,365 +1,380 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkIOMimeTypes.h" #include "mitkCustomMimeType.h" #include "mitkLog.h" #include #include "itkGDCMImageIO.h" #include "itkMetaDataObject.h" #include #include namespace mitk { IOMimeTypes::BaseDicomMimeType::BaseDicomMimeType(const std::string& name) : CustomMimeType(name) { this->AddExtension("gdcm"); this->AddExtension("dcm"); this->AddExtension("DCM"); this->AddExtension("dc3"); this->AddExtension("DC3"); this->AddExtension("ima"); this->AddExtension("img"); this->SetCategory(CATEGORY_IMAGES()); this->SetComment("DICOM"); } + bool IOMimeTypes::BaseDicomMimeType::AppliesTo(const std::string &path) const { // check whether directory or file // if directory try to find first file within it instead bool pathIsDirectory = itksys::SystemTools::FileIsDirectory(Utf8Util::Local8BitToUtf8(path)); std::string filepath = path; + itk::GDCMImageIO::Pointer gdcmIO = itk::GDCMImageIO::New(); + if (pathIsDirectory) { itksys::Directory input; input.Load(path.c_str()); - std::vector files; + bool dcmFound = false; for (unsigned long idx = 0; idxMatchesExtension(filename)) + std::string fullpath = path + "/" + filename; + + if (!itksys::SystemTools::FileIsDirectory(fullpath)) { - std::string fullpath = path + "/" + std::string(input.GetFile(idx)); - files.push_back(fullpath.c_str()); + + gdcmIO->SetFileName(fullpath); + try + { + gdcmIO->ReadImageInformation(); + dcmFound = true; + filepath = fullpath; + break; + } + catch (const itk::ExceptionObject& /*err*/) + { + } } } - if (!files.empty()) - { - filepath = files.front(); + if (!dcmFound) return false; + } + else + { + // Ask the GDCM ImageIO class directly + gdcmIO->SetFileName(filepath); + try { + gdcmIO->ReadImageInformation(); + } + catch (const itk::ExceptionObject& /*err*/) { + return false; } } - // Ask the GDCM ImageIO class directly - itk::GDCMImageIO::Pointer gdcmIO = itk::GDCMImageIO::New(); - gdcmIO->SetFileName(filepath); - try { - gdcmIO->ReadImageInformation(); - } - catch (const itk::ExceptionObject & /*err*/) { - return false; - } + //If we reached that point, there is at least one file (stored under filepath) that is a DICOM //DICOMRT modalities have specific reader, don't read with normal DICOM readers std::string modality; itk::MetaDataDictionary& dict = gdcmIO->GetMetaDataDictionary(); itk::ExposeMetaData(dict, "0008|0060", modality); MITK_DEBUG << "DICOM Modality detected by MimeType "<< this->GetName() << " is " << modality; if (modality == "RTSTRUCT" || modality == "RTDOSE" || modality == "RTPLAN") { return false; } else { return gdcmIO->CanReadFile(filepath.c_str()); } } IOMimeTypes::BaseDicomMimeType*IOMimeTypes::BaseDicomMimeType::Clone() const { return new BaseDicomMimeType(*this); } IOMimeTypes::DicomMimeType::DicomMimeType() : BaseDicomMimeType(DICOM_MIMETYPE_NAME()) { } IOMimeTypes::DicomMimeType* IOMimeTypes::DicomMimeType::Clone() const { return new DicomMimeType(*this); } std::vector IOMimeTypes::Get() { std::vector mimeTypes; // order matters here (descending rank for mime types) mimeTypes.push_back(NRRD_MIMETYPE().Clone()); mimeTypes.push_back(NIFTI_MIMETYPE().Clone()); mimeTypes.push_back(VTK_IMAGE_MIMETYPE().Clone()); mimeTypes.push_back(VTK_PARALLEL_IMAGE_MIMETYPE().Clone()); mimeTypes.push_back(VTK_IMAGE_LEGACY_MIMETYPE().Clone()); mimeTypes.push_back(DICOM_MIMETYPE().Clone()); mimeTypes.push_back(VTK_POLYDATA_MIMETYPE().Clone()); mimeTypes.push_back(VTK_PARALLEL_POLYDATA_MIMETYPE().Clone()); mimeTypes.push_back(VTK_POLYDATA_LEGACY_MIMETYPE().Clone()); mimeTypes.push_back(STEREOLITHOGRAPHY_MIMETYPE().Clone()); mimeTypes.push_back(WAVEFRONT_OBJ_MIMETYPE().Clone()); mimeTypes.push_back(STANFORD_PLY_MIMETYPE().Clone()); mimeTypes.push_back(RAW_MIMETYPE().Clone()); mimeTypes.push_back(POINTSET_MIMETYPE().Clone()); return mimeTypes; } CustomMimeType IOMimeTypes::VTK_IMAGE_MIMETYPE() { CustomMimeType mimeType(VTK_IMAGE_NAME()); mimeType.AddExtension("vti"); mimeType.SetCategory(CATEGORY_IMAGES()); mimeType.SetComment("VTK Image"); return mimeType; } CustomMimeType IOMimeTypes::VTK_IMAGE_LEGACY_MIMETYPE() { CustomMimeType mimeType(VTK_IMAGE_LEGACY_NAME()); mimeType.AddExtension("vtk"); mimeType.SetCategory(CATEGORY_IMAGES()); mimeType.SetComment("VTK Legacy Image"); return mimeType; } CustomMimeType IOMimeTypes::VTK_PARALLEL_IMAGE_MIMETYPE() { CustomMimeType mimeType(VTK_PARALLEL_IMAGE_NAME()); mimeType.AddExtension("pvti"); mimeType.SetCategory(CATEGORY_IMAGES()); mimeType.SetComment("VTK Parallel Image"); return mimeType; } CustomMimeType IOMimeTypes::VTK_POLYDATA_MIMETYPE() { CustomMimeType mimeType(VTK_POLYDATA_NAME()); mimeType.AddExtension("vtp"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("VTK PolyData"); return mimeType; } CustomMimeType IOMimeTypes::VTK_POLYDATA_LEGACY_MIMETYPE() { CustomMimeType mimeType(VTK_POLYDATA_LEGACY_NAME()); mimeType.AddExtension("vtk"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("VTK Legacy PolyData"); return mimeType; } CustomMimeType IOMimeTypes::VTK_PARALLEL_POLYDATA_MIMETYPE() { CustomMimeType mimeType(VTK_PARALLEL_POLYDATA_NAME()); mimeType.AddExtension("pvtp"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("VTK Parallel PolyData"); return mimeType; } CustomMimeType IOMimeTypes::STEREOLITHOGRAPHY_MIMETYPE() { CustomMimeType mimeType(STEREOLITHOGRAPHY_NAME()); mimeType.AddExtension("stl"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("Stereolithography"); return mimeType; } CustomMimeType IOMimeTypes::WAVEFRONT_OBJ_MIMETYPE() { CustomMimeType mimeType(WAVEFRONT_OBJ_NAME()); mimeType.AddExtension("obj"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("Wavefront OBJ"); return mimeType; } CustomMimeType IOMimeTypes::STANFORD_PLY_MIMETYPE() { CustomMimeType mimeType(STANFORD_PLY_NAME()); mimeType.AddExtension("ply"); mimeType.SetCategory(CATEGORY_SURFACES()); mimeType.SetComment("Stanford PLY"); return mimeType; } std::string IOMimeTypes::STEREOLITHOGRAPHY_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".stl"; return name; } std::string IOMimeTypes::WAVEFRONT_OBJ_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".obj"; return name; } std::string IOMimeTypes::STANFORD_PLY_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".ply"; return name; } std::string IOMimeTypes::DEFAULT_BASE_NAME() { static std::string name = "application/vnd.mitk"; return name; } std::string IOMimeTypes::CATEGORY_IMAGES() { static std::string cat = "Images"; return cat; } std::string IOMimeTypes::CATEGORY_SURFACES() { static std::string cat = "Surfaces"; return cat; } std::string IOMimeTypes::VTK_IMAGE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.image"; return name; } std::string IOMimeTypes::VTK_IMAGE_LEGACY_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.image.legacy"; return name; } std::string IOMimeTypes::VTK_PARALLEL_IMAGE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.parallel.image"; return name; } std::string IOMimeTypes::VTK_POLYDATA_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.polydata"; return name; } std::string IOMimeTypes::VTK_POLYDATA_LEGACY_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.polydata.legacy"; return name; } std::string IOMimeTypes::VTK_PARALLEL_POLYDATA_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".vtk.parallel.polydata"; return name; } CustomMimeType IOMimeTypes::NRRD_MIMETYPE() { CustomMimeType mimeType(NRRD_MIMETYPE_NAME()); mimeType.AddExtension("nrrd"); mimeType.AddExtension("nhdr"); mimeType.SetCategory("Images"); mimeType.SetComment("NRRD"); return mimeType; } CustomMimeType IOMimeTypes::NIFTI_MIMETYPE() { CustomMimeType mimeType(NIFTI_MIMETYPE_NAME()); mimeType.AddExtension("nii"); mimeType.AddExtension("nii.gz"); mimeType.AddExtension("hdr"); mimeType.AddExtension("hdr.gz"); mimeType.AddExtension("img"); mimeType.AddExtension("img.gz"); mimeType.AddExtension("nia"); mimeType.SetCategory("Images"); mimeType.SetComment("Nifti"); return mimeType; } CustomMimeType IOMimeTypes::RAW_MIMETYPE() { CustomMimeType mimeType(RAW_MIMETYPE_NAME()); mimeType.AddExtension("raw"); mimeType.SetCategory("Images"); mimeType.SetComment("Raw data"); return mimeType; } IOMimeTypes::DicomMimeType IOMimeTypes::DICOM_MIMETYPE() { return DicomMimeType(); } std::string IOMimeTypes::NRRD_MIMETYPE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".image.nrrd"; return name; } std::string IOMimeTypes::NIFTI_MIMETYPE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".image.nifti"; return name; } std::string IOMimeTypes::RAW_MIMETYPE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".image.raw"; return name; } std::string IOMimeTypes::DICOM_MIMETYPE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".image.dicom"; return name; } CustomMimeType IOMimeTypes::POINTSET_MIMETYPE() { CustomMimeType mimeType(POINTSET_MIMETYPE_NAME()); mimeType.AddExtension("mps"); mimeType.SetCategory("Point Sets"); mimeType.SetComment("MITK Point Set"); return mimeType; } std::string IOMimeTypes::POINTSET_MIMETYPE_NAME() { static std::string name = DEFAULT_BASE_NAME() + ".pointset"; return name; } CustomMimeType IOMimeTypes::GEOMETRY_DATA_MIMETYPE() { mitk::CustomMimeType mimeType(DEFAULT_BASE_NAME() + ".geometrydata"); mimeType.AddExtension("mitkgeometry"); mimeType.SetCategory("Geometries"); mimeType.SetComment("GeometryData object"); return mimeType; } } diff --git a/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp b/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp new file mode 100644 index 0000000000..0f7c5dea40 --- /dev/null +++ b/Modules/Core/src/IO/mitkIOVolumeSplitReason.cpp @@ -0,0 +1,169 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include + +#include + +void mitk::IOVolumeSplitReason::AddReason(ReasonType type, const std::string& detail) +{ + m_ReasonMap[type] = detail; +} + +void mitk::IOVolumeSplitReason::RemoveReason(ReasonType type) +{ + m_ReasonMap.erase(type); +} + +bool mitk::IOVolumeSplitReason::HasReasons() const +{ + return !m_ReasonMap.empty(); +} + +bool mitk::IOVolumeSplitReason::HasReason(ReasonType type) const +{ + return m_ReasonMap.cend() != m_ReasonMap.find(type); +} + +std::string mitk::IOVolumeSplitReason::GetReasonDetails(ReasonType type) const +{ + auto finding = m_ReasonMap.find(type); + if (m_ReasonMap.cend() == finding) + mitkThrow() << "Cannot get details for inexistent type."; + + return finding->second; +}; + +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::ExtendReason(ConstPointer otherReason) const +{ + if (nullptr == otherReason) + mitkThrow() << "Cannot extend reason. Passed other reason is in valid."; + + Pointer result = this->Clone(); + + result->m_ReasonMap.insert(otherReason->m_ReasonMap.cbegin(), otherReason->m_ReasonMap.cend()); + + return result; +} + +std::string mitk::IOVolumeSplitReason::TypeToString(ReasonType reasonType) +{ + switch (reasonType) + { + case ReasonType::GantryTiltDifference: + return "gantry_tilt_difference"; + case ReasonType::ImagePostionMissing: + return "image_position_missing"; + case ReasonType::OverlappingSlices: + return "overlapping_slices"; + case ReasonType::SliceDistanceInconsistency: + return "slice_distance_inconsistency"; + case ReasonType::ValueSortDistance: + return "value_sort_distance"; + case ReasonType::ValueSplitDifference: + return "value_split_difference"; + case ReasonType::MissingSlices: + return "missing_slices"; + default: return "unknown"; + } +} + +mitk::IOVolumeSplitReason::ReasonType mitk::IOVolumeSplitReason::StringToType(const std::string& reasonStr) +{ + if (reasonStr == "gantry_tilt_difference") + return ReasonType::GantryTiltDifference; + else if (reasonStr == "image_position_missing") + return ReasonType::ImagePostionMissing; + else if (reasonStr == "overlapping_slices") + return ReasonType::OverlappingSlices; + else if (reasonStr == "slice_distance_inconsistency") + return ReasonType::SliceDistanceInconsistency; + else if (reasonStr == "value_sort_distance") + return ReasonType::ValueSortDistance; + else if (reasonStr == "value_split_difference") + return ReasonType::ValueSplitDifference; + else if (reasonStr == "missing_slices") + return ReasonType::MissingSlices; + + return ReasonType::Unknown; +} + +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::Clone() const +{ + return std::make_shared(*this); +} + +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::New() +{ + return std::make_shared(); +} + + +nlohmann::json mitk::IOVolumeSplitReason::ToJSON(ConstPointer reason) +{ + if (nullptr == reason) + mitkThrow() << "Cannot extend reason. Passed other reason is in valid."; + + auto data = nlohmann::json::array(); + + for (const auto& [type, detail] : reason->m_ReasonMap) + { + auto details = nlohmann::json::array(); + + details.push_back(type); + if (!detail.empty()) + { + details.push_back(detail); + } + data.push_back(details); + } + + return data; +} + +mitk::IOVolumeSplitReason::Pointer mitk::IOVolumeSplitReason::FromJSON(const nlohmann::json& j) +{ + auto reason = std::make_shared(); + + for (const auto& jItem : j) + { + if (!jItem.empty()) + { + ReasonType reasonType = jItem.at(0).get(); + + std::string detail; + if (jItem.size() > 1) + { + detail = jItem.at(1).get(); + } + reason->AddReason(reasonType, detail); + } + } + + return reason; +} + + +void mitk::to_json(nlohmann::json& j, IOVolumeSplitReason::ConstPointer reason) +{ + j = IOVolumeSplitReason::ToJSON(reason); +} + +void mitk::to_json(nlohmann::json& j, IOVolumeSplitReason::Pointer reason) +{ + j = IOVolumeSplitReason::ToJSON(reason); +} + +void mitk::from_json(const nlohmann::json& j, IOVolumeSplitReason& e) +{ + e = *IOVolumeSplitReason::FromJSON(j); +} diff --git a/Modules/Core/src/mitkCoreActivator.cpp b/Modules/Core/src/mitkCoreActivator.cpp index a5c00479ad..00cc3e159b 100644 --- a/Modules/Core/src/mitkCoreActivator.cpp +++ b/Modules/Core/src/mitkCoreActivator.cpp @@ -1,437 +1,438 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkCoreActivator.h" #include #include // File IO #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mitkLegacyFileWriterService.h" #include #include #include // PropertyRelationRules #include // Micro Services #include #include #include #include #include #include #include #include #include #include // Properties #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ITK "injects" static initialization code for IO factories // via the itkImageIOFactoryRegisterManager.h header (which // is generated in the application library build directory). // To ensure that the code is called *before* the CppMicroServices // static initialization code (which triggers the Activator::Start // method), we include the ITK header here. #include namespace { void HandleMicroServicesMessages(us::MsgType type, const char* msg) { switch (type) { case us::DebugMsg: MITK_DEBUG << msg; break; case us::InfoMsg: MITK_INFO << msg; break; case us::WarningMsg: MITK_WARN << msg; break; case us::ErrorMsg: MITK_ERROR << msg; break; } } void AddMitkAutoLoadPaths(const std::string& programPath) { us::ModuleSettings::AddAutoLoadPath(programPath); #ifdef __APPLE__ // Walk up three directories since that is where the .dylib files are located // for build trees. std::string additionalPath = programPath; bool addPath = true; for (int i = 0; i < 3; ++i) { std::size_t index = additionalPath.find_last_of('/'); if (index != std::string::npos) { additionalPath = additionalPath.substr(0, index); } else { addPath = false; break; } } if (addPath) { us::ModuleSettings::AddAutoLoadPath(additionalPath); } #endif } void AddPropertyPersistence(const mitk::PropertyKeyPath& propPath) { mitk::CoreServicePointer persistenceService(mitk::CoreServices::GetPropertyPersistence()); auto info = mitk::PropertyPersistenceInfo::New(); if (propPath.IsExplicit()) { std::string name = mitk::PropertyKeyPathToPropertyName(propPath); std::string key = name; std::replace(key.begin(), key.end(), '.', '_'); info->SetNameAndKey(name, key); } else { std::string key = mitk::PropertyKeyPathToPersistenceKeyRegEx(propPath); std::string keyTemplate = mitk::PropertyKeyPathToPersistenceKeyTemplate(propPath); std::string propRegEx = mitk::PropertyKeyPathToPropertyRegEx(propPath); std::string propTemplate = mitk::PropertyKeyPathToPersistenceNameTemplate(propPath); info->UseRegEx(propRegEx, propTemplate, key, keyTemplate); } persistenceService->AddInfo(info); } void RegisterProperties() { mitk::CoreServicePointer service(mitk::CoreServices::GetPropertyDeserialization()); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); service->RegisterProperty(); } } class FixedNiftiImageIO : public itk::NiftiImageIO { public: /** Standard class typedefs. */ typedef FixedNiftiImageIO Self; typedef itk::NiftiImageIO Superclass; typedef itk::SmartPointer Pointer; /** Method for creation through the object factory. */ itkNewMacro(Self) /** Run-time type information (and related methods). */ itkTypeMacro(FixedNiftiImageIO, Superclass) bool SupportsDimension(unsigned long dim) override { return dim > 1 && dim < 5; } }; void MitkCoreActivator::Load(us::ModuleContext *context) { // Handle messages from CppMicroServices us::installMsgHandler(HandleMicroServicesMessages); this->m_Context = context; // Add the current application directory to the auto-load paths. // This is useful for third-party executables. std::string programPath = mitk::IOUtil::GetProgramPath(); if (programPath.empty()) { MITK_WARN << "Could not get the program path."; } else { AddMitkAutoLoadPaths(programPath); } // m_RenderingManager = mitk::RenderingManager::New(); // context->RegisterService(renderingManager.GetPointer()); m_PlanePositionManager.reset(new mitk::PlanePositionManagerService); context->RegisterService(m_PlanePositionManager.get()); m_PropertyAliases.reset(new mitk::PropertyAliases); context->RegisterService(m_PropertyAliases.get()); m_PropertyDescriptions.reset(new mitk::PropertyDescriptions); context->RegisterService(m_PropertyDescriptions.get()); m_PropertyDeserialization.reset(new mitk::PropertyDeserialization); context->RegisterService(m_PropertyDeserialization.get()); m_PropertyExtensions.reset(new mitk::PropertyExtensions); context->RegisterService(m_PropertyExtensions.get()); m_PropertyFilters.reset(new mitk::PropertyFilters); context->RegisterService(m_PropertyFilters.get()); m_PropertyPersistence.reset(new mitk::PropertyPersistence); context->RegisterService(m_PropertyPersistence.get()); m_PropertyRelations.reset(new mitk::PropertyRelations); context->RegisterService(m_PropertyRelations.get()); m_PreferencesService.reset(new mitk::PreferencesService); context->RegisterService(m_PreferencesService.get()); m_MimeTypeProvider.reset(new mitk::MimeTypeProvider); m_MimeTypeProvider->Start(); m_MimeTypeProviderReg = context->RegisterService(m_MimeTypeProvider.get()); this->RegisterDefaultMimeTypes(); this->RegisterItkReaderWriter(); this->RegisterVtkReaderWriter(); // Add custom Reader / Writer Services m_FileReaders.push_back(new mitk::PointSetReaderService()); m_FileWriters.push_back(new mitk::PointSetWriterService()); m_FileReaders.push_back(new mitk::GeometryDataReaderService()); m_FileWriters.push_back(new mitk::GeometryDataWriterService()); m_FileReaders.push_back(new mitk::RawImageFileReaderService()); //add properties that should be persistent (if possible/supported by the writer) AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_DESCRIPTION()); AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_INPUTLOCATION()); AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_MIME_CATEGORY()); AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_MIME_NAME()); AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_VERSION()); AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::READER_OPTIONS_ANY()); + AddPropertyPersistence(mitk::IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()); AddPropertyPersistence(mitk::PropertyRelationRuleBase::GetRIIDestinationUIDPropertyKeyPath()); AddPropertyPersistence(mitk::PropertyRelationRuleBase::GetRIIRelationUIDPropertyKeyPath()); AddPropertyPersistence(mitk::PropertyRelationRuleBase::GetRIIRuleIDPropertyKeyPath()); AddPropertyPersistence(mitk::PropertyRelationRuleBase::GetRIIPropertyKeyPath("","").AddAnyElement()); RegisterProperties(); /* There IS an option to exchange ALL vtkTexture instances against vtkNeverTranslucentTextureFactory. This code is left here as a reminder, just in case we might need to do that some time. vtkNeverTranslucentTextureFactory* textureFactory = vtkNeverTranslucentTextureFactory::New(); vtkObjectFactory::RegisterFactory( textureFactory ); textureFactory->Delete(); */ this->RegisterLegacyWriter(); } void MitkCoreActivator::Unload(us::ModuleContext *) { for (auto &elem : m_FileReaders) { delete elem; } for (auto &elem : m_FileWriters) { delete elem; } for (auto &elem : m_FileIOs) { delete elem; } for (auto &elem : m_LegacyWriters) { delete elem; } // The mitk::ModuleContext* argument of the Unload() method // will always be 0 for the Mitk library. It makes no sense // to use it at this stage anyway, since all libraries which // know about the module system have already been unloaded. // we need to close the internal service tracker of the // MimeTypeProvider class here. Otherwise it // would hold on to the ModuleContext longer than it is // actually valid. m_MimeTypeProviderReg.Unregister(); m_MimeTypeProvider->Stop(); for (std::vector::const_iterator mimeTypeIter = m_DefaultMimeTypes.begin(), iterEnd = m_DefaultMimeTypes.end(); mimeTypeIter != iterEnd; ++mimeTypeIter) { delete *mimeTypeIter; } } void MitkCoreActivator::RegisterDefaultMimeTypes() { // Register some default mime-types std::vector mimeTypes = mitk::IOMimeTypes::Get(); for (std::vector::const_iterator mimeTypeIter = mimeTypes.begin(), iterEnd = mimeTypes.end(); mimeTypeIter != iterEnd; ++mimeTypeIter) { m_DefaultMimeTypes.push_back(*mimeTypeIter); m_Context->RegisterService(m_DefaultMimeTypes.back()); } } void MitkCoreActivator::RegisterItkReaderWriter() { std::list allobjects = itk::ObjectFactoryBase::CreateAllInstance("itkImageIOBase"); for (auto &allobject : allobjects) { auto *io = dynamic_cast(allobject.GetPointer()); // NiftiImageIO does not provide a correct "SupportsDimension()" methods // and the supported read/write extensions are not ordered correctly if (dynamic_cast(io)) continue; // Use a custom mime-type for GDCMImageIO below if (dynamic_cast(allobject.GetPointer())) { // MITK provides its own DICOM reader (which internally uses GDCMImageIO). continue; } if (io) { m_FileIOs.push_back(new mitk::ItkImageIO(io)); } else { MITK_WARN << "Error ImageIO factory did not return an ImageIOBase: " << (allobject)->GetNameOfClass(); } } FixedNiftiImageIO::Pointer itkNiftiIO = FixedNiftiImageIO::New(); mitk::ItkImageIO *niftiIO = new mitk::ItkImageIO(mitk::IOMimeTypes::NIFTI_MIMETYPE(), itkNiftiIO.GetPointer(), 0); m_FileIOs.push_back(niftiIO); } void MitkCoreActivator::RegisterVtkReaderWriter() { m_FileIOs.push_back(new mitk::SurfaceVtkXmlIO()); m_FileIOs.push_back(new mitk::SurfaceStlIO()); m_FileIOs.push_back(new mitk::SurfaceVtkLegacyIO()); m_FileIOs.push_back(new mitk::ImageVtkXmlIO()); m_FileIOs.push_back(new mitk::ImageVtkLegacyIO()); } void MitkCoreActivator::RegisterLegacyWriter() { std::list allobjects = itk::ObjectFactoryBase::CreateAllInstance("IOWriter"); for (auto i = allobjects.begin(); i != allobjects.end(); ++i) { mitk::FileWriter::Pointer io = dynamic_cast(i->GetPointer()); if (io) { std::string description = std::string("Legacy ") + io->GetNameOfClass() + " Writer"; mitk::IFileWriter *writer = new mitk::LegacyFileWriterService(io, description); m_LegacyWriters.push_back(writer); } else { MITK_ERROR << "Error IOWriter override is not of type mitk::FileWriter: " << (*i)->GetNameOfClass() << std::endl; } } } US_EXPORT_MODULE_ACTIVATOR(MitkCoreActivator) // Call CppMicroservices initialization code at the end of the file. // This especially ensures that VTK object factories have already // been registered (VTK initialization code is injected by implicitly // include VTK header files at the top of this file). US_INITIALIZE_MODULE diff --git a/Modules/Core/test/files.cmake b/Modules/Core/test/files.cmake index b4b926c612..5d53b55f50 100644 --- a/Modules/Core/test/files.cmake +++ b/Modules/Core/test/files.cmake @@ -1,199 +1,200 @@ # tests with no extra command line parameter set(MODULE_TESTS # IMPORTANT: If you plan to deactivate / comment out a test please write a bug number to the commented out line of code. # # Example: #mitkMyTest #this test is commented out because of bug 12345 # # It is important that the bug is open and that the test will be activated again before the bug is closed. This assures that # no test is forgotten after it was commented out. If there is no bug for your current problem, please add a new one and # mark it as critical. ################## DISABLED TESTS ################################################# #mitkAbstractTransformGeometryTest.cpp #seems as tested class mitkExternAbstractTransformGeometry doesn't exist any more #mitkStateMachineContainerTest.cpp #rewrite test, indirect since no longer exported Bug 14529 #mitkRegistrationBaseTest.cpp #tested class mitkRegistrationBase doesn't exist any more #mitkSegmentationInterpolationTest.cpp #file doesn't exist! #mitkPipelineSmartPointerCorrectnessTest.cpp #file doesn't exist! #mitkITKThreadingTest.cpp #test outdated because itk::Semaphore was removed from ITK #mitkAbstractTransformPlaneGeometryTest.cpp #mitkVtkAbstractTransformPlaneGeometry doesn't exist any more #mitkTestUtilSharedLibrary.cpp #Linker problem with this test... #mitkTextOverlay2DSymbolsRenderingTest.cpp #Implementation of the tested feature is not finished yet. Ask Christoph or see bug 15104 for details. ################# RUNNING TESTS ################################################### mitkAccessByItkTest.cpp mitkCoreObjectFactoryTest.cpp mitkDataNodeTest.cpp mitkMaterialTest.cpp mitkActionTest.cpp mitkDispatcherTest.cpp mitkEnumerationPropertyTest.cpp mitkFileReaderRegistryTest.cpp #mitkFileWriterRegistryTest.cpp mitkFloatToStringTest.cpp mitkGenericPropertyTest.cpp mitkGeometry3DTest.cpp mitkGeometry3DEqualTest.cpp mitkGeometryDataIOTest.cpp mitkGeometryDataToSurfaceFilterTest.cpp mitkImageCastTest.cpp mitkImageDataItemTest.cpp mitkImageGeneratorTest.cpp mitkIOUtilTest.cpp mitkITKEventObserverGuardTest.cpp mitkBaseDataTest.cpp mitkImportItkImageTest.cpp mitkGrabItkImageMemoryTest.cpp mitkInstantiateAccessFunctionTest.cpp mitkLevelWindowTest.cpp mitkMessageTest.cpp mitkPixelTypeTest.cpp mitkPlaneGeometryTest.cpp mitkPointSetTest.cpp mitkPointSetEqualTest.cpp mitkPointSetFileIOTest.cpp mitkPointSetOnEmptyTest.cpp mitkPointSetLocaleTest.cpp mitkPointSetWriterTest.cpp mitkPointSetPointOperationsTest.cpp mitkProgressBarTest.cpp mitkPropertyTest.cpp mitkPropertyListTest.cpp mitkPropertyPersistenceTest.cpp mitkPropertyPersistenceInfoTest.cpp mitkPropertyRelationRuleBaseTest.cpp mitkPropertyRelationsTest.cpp mitkSlicedGeometry3DTest.cpp mitkSliceNavigationControllerTest.cpp mitkSurfaceTest.cpp mitkSurfaceEqualTest.cpp mitkSurfaceToSurfaceFilterTest.cpp mitkTimeGeometryTest.cpp mitkProportionalTimeGeometryTest.cpp mitkUndoControllerTest.cpp mitkVtkWidgetRenderingTest.cpp mitkVerboseLimitedLinearUndoTest.cpp mitkWeakPointerTest.cpp mitkTransferFunctionTest.cpp mitkStepperTest.cpp mitkRenderingManagerTest.cpp mitkCompositePixelValueToStringTest.cpp vtkMitkThickSlicesFilterTest.cpp mitkNodePredicateDataPropertyTest.cpp mitkNodePredicateFunctionTest.cpp mitkVectorTest.cpp mitkClippedSurfaceBoundsCalculatorTest.cpp mitkExceptionTest.cpp mitkExtractSliceFilterTest.cpp mitkLogTest.cpp mitkImageDimensionConverterTest.cpp mitkLoggingAdapterTest.cpp mitkUIDGeneratorTest.cpp mitkPlanePositionManagerTest.cpp mitkAffineTransformBaseTest.cpp mitkPropertyAliasesTest.cpp mitkPropertyDescriptionsTest.cpp mitkPropertyExtensionsTest.cpp mitkPropertyFiltersTest.cpp mitkPropertyKeyPathTest.cpp mitkTinyXMLTest.cpp mitkRawImageFileReaderTest.cpp mitkInteractionEventTest.cpp mitkLookupTableTest.cpp mitkSTLFileReaderTest.cpp mitkPointTypeConversionTest.cpp mitkVectorTypeConversionTest.cpp mitkMatrixTypeConversionTest.cpp mitkArrayTypeConversionTest.cpp mitkSurfaceToImageFilterTest.cpp mitkBaseGeometryTest.cpp mitkImageToSurfaceFilterTest.cpp mitkEqualTest.cpp mitkLineTest.cpp mitkArbitraryTimeGeometryTest.cpp mitkItkImageIOTest.cpp mitkLevelWindowManagerTest.cpp mitkVectorPropertyTest.cpp mitkTemporoSpatialStringPropertyTest.cpp mitkPropertyNameHelperTest.cpp mitkNodePredicateGeometryTest.cpp mitkNodePredicateSubGeometryTest.cpp mitkPreferenceListReaderOptionsFunctorTest.cpp mitkGenericIDRelationRuleTest.cpp mitkSourceImageRelationRuleTest.cpp mitkTemporalJoinImagesFilterTest.cpp mitkPreferencesTest.cpp + mitkIOVolumeSplitReasonTest.cpp ) set(MODULE_RENDERING_TESTS mitkPointSetDataInteractorTest.cpp mitkSurfaceVtkMapper2DTest.cpp mitkSurfaceVtkMapper2D3DTest.cpp ) # test with image filename as an extra command line parameter set(MODULE_IMAGE_TESTS mitkImageTimeSelectorTest.cpp #only runs on images mitkImageAccessorTest.cpp #only runs on images ) set(MODULE_SURFACE_TESTS mitkSurfaceVtkWriterTest.cpp #only runs on surfaces ) # list of images for which the tests are run set(MODULE_TESTIMAGE US4DCyl.nrrd Pic3D.nrrd Pic2DplusT.nrrd BallBinary30x30x30.nrrd Png2D-bw.png ) set(MODULE_TESTSURFACE binary.stl ball.stl ) set(MODULE_CUSTOM_TESTS mitkDataStorageTest.cpp mitkDataNodeTest.cpp mitkEventConfigTest.cpp mitkPointSetLocaleTest.cpp mitkImageTest.cpp mitkImageVtkMapper2DTest.cpp mitkImageVtkMapper2DLevelWindowTest.cpp mitkImageVtkMapper2DOpacityTest.cpp mitkImageVtkMapper2DResliceInterpolationPropertyTest.cpp mitkImageVtkMapper2DColorTest.cpp mitkImageVtkMapper2DSwivelTest.cpp mitkImageVtkMapper2DTransferFunctionTest.cpp mitkImageVtkMapper2DOpacityTransferFunctionTest.cpp mitkImageVtkMapper2DLookupTableTest.cpp mitkSurfaceVtkMapper3DTest.cpp mitkVolumeCalculatorTest.cpp mitkLevelWindowManagerTest.cpp mitkPointSetVtkMapper2DTest.cpp mitkPointSetVtkMapper2DImageTest.cpp mitkPointSetVtkMapper2DGlyphTypeTest.cpp mitkPointSetVtkMapper2DTransformedPointsTest.cpp mitkVTKRenderWindowSizeTest.cpp mitkMultiComponentImageDataComparisonFilterTest.cpp mitkImageToItkTest.cpp mitkImageSliceSelectorTest.cpp mitkPointSetReaderTest.cpp mitkImageEqualTest.cpp mitkRotatedSlice4DTest.cpp mitkPlaneGeometryDataMapper2DTest.cpp ) # Currently not working on windows because of a rendering timing issue # see bug 18083 for details if(NOT WIN32) set(MODULE_CUSTOM_TESTS ${MODULE_CUSTOM_TESTS} mitkSurfaceDepthSortingTest.cpp) endif() set(RESOURCE_FILES Interactions/AddAndRemovePoints.xml Interactions/globalConfig.xml Interactions/StatemachineTest.xml Interactions/StatemachineConfigTest.xml ) diff --git a/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp b/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp new file mode 100644 index 0000000000..a16d37af78 --- /dev/null +++ b/Modules/Core/test/mitkIOVolumeSplitReasonTest.cpp @@ -0,0 +1,180 @@ +/*============================================================================ + +The Medical Imaging Interaction Toolkit (MITK) + +Copyright (c) German Cancer Research Center (DKFZ) +All rights reserved. + +Use of this source code is governed by a 3-clause BSD license that can be +found in the LICENSE file. + +============================================================================*/ + +#include "mitkTestingMacros.h" +#include +#include + +#include +#include + +#include + +class mitkIOVolumeSplitReasonTestSuite : public mitk::TestFixture +{ + CPPUNIT_TEST_SUITE(mitkIOVolumeSplitReasonTestSuite); + MITK_TEST(TestAddNRemoveMethods); + MITK_TEST(TestSerializeReasonType); + MITK_TEST(TestSerializeReason); + MITK_TEST(TestExtendReason); + CPPUNIT_TEST_SUITE_END(); + +private: + std::string m_ReasonStr; + mitk::IOVolumeSplitReason::Pointer m_Reason1; + mitk::IOVolumeSplitReason::Pointer m_Reason2; + mitk::IOVolumeSplitReason::Pointer m_EmptyReason; + +public: + void setUp() override + { + m_Reason1 = mitk::IOVolumeSplitReason::New(); + m_Reason2 = mitk::IOVolumeSplitReason::New(); + m_EmptyReason = mitk::IOVolumeSplitReason::New(); + + m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices); + m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices,"1"); + m_Reason2->AddReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing); + m_Reason2->AddReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices, "2"); + + m_ReasonStr = "[[\"value_sort_distance\",\"detail\"],[\"overlapping_slices\"],[\"missing_slices\",\"3\"]]"; + } + + void TestAddNRemoveMethods() + { + CPPUNIT_ASSERT(!m_EmptyReason->HasReasons()); + CPPUNIT_ASSERT(m_Reason1->HasReasons()); + CPPUNIT_ASSERT(m_Reason2->HasReasons()); + + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(!m_Reason2->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + + m_Reason1->RemoveReason(mitk::IOVolumeSplitReason::ReasonType::Unknown); + + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + + m_Reason1->RemoveReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices); + + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + + m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance); + m_Reason1->AddReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference); + + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(m_Reason1->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + } + + static bool CheckType(mitk::IOVolumeSplitReason::ReasonType reasontype, const std::string& reasonStr) + { + if (mitk::IOVolumeSplitReason::TypeToString(reasontype) != reasonStr) + return false; + if (mitk::IOVolumeSplitReason::StringToType(reasonStr) != reasontype) + return false; + + return true; + } + + static bool CheckReason(const std::string& refStr, const nlohmann::json& j) + { + auto refJ = nlohmann::json::parse(refStr); + + return refJ == j; + } + + + void TestSerializeReasonType() + { + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference,"gantry_tilt_difference")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing, "image_position_missing")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::MissingSlices, "missing_slices")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices, "overlapping_slices")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency, "slice_distance_inconsistency")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::Unknown, "unknown")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance, "value_sort_distance")); + CPPUNIT_ASSERT(CheckType(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference, "value_split_difference")); + } + + void TestSerializeReason() + { + auto serializedReason1 = mitk::IOVolumeSplitReason::ToJSON(m_Reason1); + auto serializedReason2 = mitk::IOVolumeSplitReason::ToJSON(m_Reason2); + auto serializedReasonEmpty = mitk::IOVolumeSplitReason::ToJSON(m_EmptyReason); + + CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", serializedReason1)); + CPPUNIT_ASSERT(CheckReason("[[\"image_position_missing\"],[\"missing_slices\",\"2\"]]", serializedReason2)); + CPPUNIT_ASSERT(CheckReason("[]", serializedReasonEmpty)); + + auto newReason = mitk::IOVolumeSplitReason::FromJSON(nlohmann::json::parse(m_ReasonStr)); + CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::GantryTiltDifference)); + CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ImagePostionMissing)); + CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices)); + CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency)); + CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::Unknown)); + CPPUNIT_ASSERT(newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance)); + CPPUNIT_ASSERT(!newReason->HasReason(mitk::IOVolumeSplitReason::ReasonType::ValueSplitDifference)); + + CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::ValueSortDistance) == "detail"); + CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::MissingSlices) == "3"); + CPPUNIT_ASSERT(newReason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::OverlappingSlices) == ""); + } + + void TestExtendReason() + { + auto extendedReason = m_Reason1->ExtendReason(m_EmptyReason); + CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); + + extendedReason = m_EmptyReason->ExtendReason(m_Reason1); + CPPUNIT_ASSERT(CheckReason("[[\"overlapping_slices\"],[\"missing_slices\",\"1\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); + + extendedReason = m_Reason2->ExtendReason(m_Reason1); + CPPUNIT_ASSERT(CheckReason("[[\"image_position_missing\"],[\"overlapping_slices\"],[\"missing_slices\",\"2\"]]", mitk::IOVolumeSplitReason::ToJSON(extendedReason))); + } + +}; + +MITK_TEST_SUITE_REGISTRATION(mitkIOVolumeSplitReason) diff --git a/Modules/CoreCmdApps/FileConverter.cpp b/Modules/CoreCmdApps/FileConverter.cpp index 197fa9fa67..f9f52f936d 100644 --- a/Modules/CoreCmdApps/FileConverter.cpp +++ b/Modules/CoreCmdApps/FileConverter.cpp @@ -1,116 +1,152 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkProperties.h" #include "mitkCommandLineParser.h" #include "mitkIOUtil.h" #include #include "mitkPreferenceListReaderOptionsFunctor.h" +#include "mitkIOMetaInformationPropertyConstants.h" +#include "mitkIOVolumeSplitReason.h" +#include "mitkPropertyKeyPath.h" int main(int argc, char* argv[]) { mitkCommandLineParser parser; parser.setTitle("File Converter"); parser.setCategory("Basic Image Processing"); parser.setDescription(""); parser.setContributor("German Cancer Research Center (DKFZ)"); parser.setArgumentPrefix("--","-"); // Add command line argument names parser.addArgument("help", "h",mitkCommandLineParser::Bool, "Help:", "Show this help text"); - parser.addArgument("input", "i", mitkCommandLineParser::File, "Input file:", "Input File",us::Any(),false, false, false, mitkCommandLineParser::Input); - parser.addArgument("output", "o", mitkCommandLineParser::File, "Output file:", "Output file", us::Any(), false, false, false, mitkCommandLineParser::Output); - parser.addArgument("reader", "r", mitkCommandLineParser::String, "Reader Name", "Reader Name", us::Any()); - parser.addArgument("list-readers", "lr", mitkCommandLineParser::Bool, "Reader Name", "Reader Name", us::Any()); + parser.addArgument("input", "i", mitkCommandLineParser::File, "Input file:", "Input path that should be loaded.",us::Any(),false, false, false, mitkCommandLineParser::Input); + parser.addArgument("output", "o", mitkCommandLineParser::File, "Output file:", "Output path where the result should be stored. If the input generates multiple outputs the index will be added for all but the first output (before the extension; starting with 0).", us::Any(), false, false, false, mitkCommandLineParser::Output); + parser.addArgument("reader", "r", mitkCommandLineParser::String, "Reader Name", "Enforce a certain reader to be used for loading the input.", us::Any()); + parser.addArgument("list-readers", "lr", mitkCommandLineParser::Bool, "List reader names", "Print names of all available readers.", us::Any()); std::map parsedArgs = parser.parseArguments(argc, argv); if (parsedArgs.size()==0) return EXIT_FAILURE; // Show a help message if ( parsedArgs.count("help") || parsedArgs.count("h")) { std::cout << parser.helpText(); return EXIT_SUCCESS; } std::string inputFilename = us::any_cast(parsedArgs["input"]); std::string outputFilename = us::any_cast(parsedArgs["output"]); mitk::PreferenceListReaderOptionsFunctor::ListType preference = {}; if (parsedArgs.count("reader")) { preference.push_back(us::any_cast(parsedArgs["reader"])); } if (parsedArgs.count("list-readers")) { mitk::IOUtil::LoadInfo loadInfo(inputFilename); auto readers = loadInfo.m_ReaderSelector.Get(); std::string errMsg; if (readers.empty()) { if (!itksys::SystemTools::FileExists(loadInfo.m_Path.c_str())) { errMsg += "File '" + loadInfo.m_Path + "' does not exist\n"; } else { errMsg += "No reader available for '" + loadInfo.m_Path + "'\n"; } MITK_ERROR << errMsg; return 0; } std::cout << "Available Readers: "< 0) { writeName = path + filename + "_" + std::to_string(count) + extension; } mitk::IOUtil::Save(node, writeName); ++count; + + try + { + auto splitReasonProperty = node->GetProperty(mitk::PropertyKeyPathToPropertyName(mitk::IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()).c_str()); + if (splitReasonProperty.IsNotNull()) + { + auto reasonStr = splitReasonProperty->GetValueAsString(); + auto reason = mitk::IOVolumeSplitReason::FromJSON(reasonStr); + if (nullptr != reason && reason->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)) + { + missingSlicesDetected += std::stoi(reason->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + } + } + } + catch (const std::exception& e) + { + std::cerr << "Error while checking for existing split reasons in volume #" << count << "." << std::endl; + std::cerr << "Error details:" << e.what() << std::endl; + } + catch (...) + { + std::cerr << "Unknown error while checking for existing split reasons in volume #" << count << "." << std::endl; + } + } + + if (missingSlicesDetected > 0) + { + std::cout << std::endl; + std::cout << "\n!!! WARNING: MISSING SLICES !!!\n" + "Details: Reader indicated volume splitting due to missing slices. Converted data might be invalid/incomplete.\n" + "Estimated number of missing slices: " << missingSlicesDetected << std::endl; } return EXIT_SUCCESS; } diff --git a/Modules/DICOM/cmdapps/DICOMVolumeDiagnostics.cpp b/Modules/DICOM/cmdapps/DICOMVolumeDiagnostics.cpp index dc50c4c610..fa9ec00f29 100644 --- a/Modules/DICOM/cmdapps/DICOMVolumeDiagnostics.cpp +++ b/Modules/DICOM/cmdapps/DICOMVolumeDiagnostics.cpp @@ -1,199 +1,230 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include #include #include #include #include #include #include void InitializeCommandLineParser(mitkCommandLineParser& parser) { parser.setTitle("DICOM Volume Diagnostics"); parser.setCategory("DICOM"); parser.setDescription("Gives insights how MITK readers would convert a set of DICOM files into image volumes (e.g. number of volumes and the sorting of the files)"); parser.setContributor("German Cancer Research Center (DKFZ)"); parser.setArgumentPrefix("--", "-"); parser.addArgument("help", "h", mitkCommandLineParser::Bool, "Help:", "Show this help text"); parser.addArgument("only-own-series", "s", mitkCommandLineParser::Bool, "Only own series", "Analyze only files in the same directory that have the same DICOM Series UID, if a file is provided as input.", us::Any()); parser.addArgument("check-3d", "d", mitkCommandLineParser::Bool, "Check 3D configs", "Analyze the input by using all known 3D configurations. If flag is not set all configurations (3D and 3D+t) will be used.", us::Any()); parser.addArgument("check-3d+t", "t", mitkCommandLineParser::Bool, "Check 3D+t configs", "Analyze the input by using all known 3D+t configurations (thus dynamic image configurations). If flag is not set all configurations (3D and 3D+t) will be used.", us::Any()); parser.addArgument("input", "i", mitkCommandLineParser::File, "Input file or path", "Input contour(s)", us::Any(), false, false, false, mitkCommandLineParser::Input); parser.addArgument("output", "o", mitkCommandLineParser::File, "Output file", "Output file where the diagnostics results are stored as json.", us::Any()); } int main(int argc, char* argv[]) { int returnValue = EXIT_SUCCESS; mitkCommandLineParser parser; InitializeCommandLineParser(parser); auto args = parser.parseArguments(argc, argv); if (args.empty()) { std::cout << parser.helpText(); return EXIT_FAILURE; } nlohmann::json diagnosticsResult; try { + int missingSlicesDetected = 0; + auto inputFilename = us::any_cast(args["input"]); auto outputFilename = args.count("output")==0 ? std::string() : us::any_cast(args["output"]); bool onlyOwnSeries = args.count("only-own-series"); bool check3D = args.count("check-3d"); bool check3DPlusT = args.count("check-3d+t"); if (!check3D && !check3DPlusT) { //if no check option is selected all are activated by default. check3D = true; check3DPlusT = true; } diagnosticsResult["input"] = inputFilename; diagnosticsResult["only-own-series"] = onlyOwnSeries; diagnosticsResult["check-3d"] = check3D; diagnosticsResult["check-3d+t"] = check3DPlusT; mitk::StringList relevantFiles = mitk::GetDICOMFilesInSameDirectory(inputFilename); if (relevantFiles.empty()) { mitkThrow() << "DICOM Volume Diagnostics found no relevant files in specified location. No data is loaded. Location: " << inputFilename; } else { bool pathIsDirectory = fs::is_directory(inputFilename); if (!pathIsDirectory && onlyOwnSeries) { relevantFiles = mitk::FilterDICOMFilesForSameSeries(inputFilename, relevantFiles); } diagnosticsResult["analyzed_files"] = relevantFiles; auto selector = mitk::DICOMFileReaderSelector::New(); if (check3D) selector->LoadBuiltIn3DConfigs(); if (check3DPlusT) selector->LoadBuiltIn3DnTConfigs(); nlohmann::json readerInfos; for (const auto& reader : selector->GetAllConfiguredReaders()) { nlohmann::json readerInfo; readerInfo["class_name"] = reader->GetNameOfClass(); readerInfo["configuration_label"] = reader->GetConfigurationLabel(); readerInfo["configuration_description"] = reader->GetConfigurationDescription(); readerInfos.push_back(readerInfo); } diagnosticsResult["checked_readers"] = readerInfos; selector->SetInputFiles(relevantFiles); auto reader = selector->GetFirstReaderWithMinimumNumberOfOutputImages(); if (reader.IsNull()) { mitkThrow() << "DICOM Volume Diagnostics service found no suitable reader configuration for relevant files."; } else { nlohmann::json readerInfo; readerInfo["class_name"] = reader->GetNameOfClass(); readerInfo["configuration_label"] = reader->GetConfigurationLabel(); readerInfo["configuration_description"] = reader->GetConfigurationDescription(); readerInfo["configuration_description"] = reader->GetConfigurationDescription(); std::stringstream config; reader->PrintConfiguration(config); readerInfo["config_details"] = config.str(); diagnosticsResult["selected_reader"] = readerInfo; nlohmann::json outputInfos; unsigned int relevantOutputCount = 0; const auto nrOfOutputs = reader->GetNumberOfOutputs(); for (std::remove_const_t outputIndex = 0; outputIndex < nrOfOutputs; ++outputIndex) { bool isRelevantOutput = true; if (!pathIsDirectory) { const auto frameList = reader->GetOutput(outputIndex).GetImageFrameList(); auto finding = std::find_if(frameList.begin(), frameList.end(), [&](const mitk::DICOMImageFrameInfo::Pointer& frame) { fs::path framePath(frame->Filename); fs::path inputPath(inputFilename); return framePath == inputPath; }); isRelevantOutput = finding != frameList.end(); } if (isRelevantOutput) { ++relevantOutputCount; nlohmann::json outputInfo; const auto output = reader->GetOutput(outputIndex); const auto frameList = output.GetImageFrameList(); mitk::DICOMFilePathList outputFiles; outputFiles.resize(frameList.size()); std::transform(frameList.begin(), frameList.end(), outputFiles.begin(), [](const mitk::DICOMImageFrameInfo::Pointer& frame) { return frame->Filename; }); outputInfo["files"] = outputFiles; outputInfo["timesteps"] = output.GetNumberOfTimeSteps(); outputInfo["frames_per_timesteps"] = output.GetNumberOfFramesPerTimeStep(); + if (output.GetSplitReason()!=nullptr && output.GetSplitReason()->HasReasons()) + { + outputInfo["volume_split_reason"] = mitk::IOVolumeSplitReason::ToJSON(output.GetSplitReason()); + + try + { + if (output.GetSplitReason()->HasReason(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)) + { + missingSlicesDetected += std::stoi(output.GetSplitReason()->GetReasonDetails(mitk::IOVolumeSplitReason::ReasonType::MissingSlices)); + } + } + catch (const std::exception& e) + { + std::cerr << "Error while checking for missing slices split reasons in volume #" << relevantOutputCount << "." << std::endl; + std::cerr << "Error details:" << e.what() << std::endl; + } + catch (...) + { + std::cerr << "Unknown error while checking for missing slices split reasons in volume #" << relevantOutputCount << "." << std::endl; + } + } outputInfos.push_back(outputInfo); } } diagnosticsResult["volume_count"] = relevantOutputCount; diagnosticsResult["volumes"] = outputInfos; } } std::cout << "\n### DIAGNOSTICS REPORT ###\n" << std::endl; std::cout << std::setw(2) << diagnosticsResult << std::endl; + if (missingSlicesDetected > 0) + { + std::cout << std::endl; + std::cout << "\n!!! WARNING: MISSING SLICES !!!\n" + "Details: Reader indicated volume splitting due to missing slices. Converted data might be invalid/incomplete.\n" + "Estimated number of missing slices: " << missingSlicesDetected << std::endl; + } + if (!outputFilename.empty()) { std::ofstream fileout(outputFilename); fileout << diagnosticsResult; fileout.close(); } } catch (const mitk::Exception& e) { MITK_ERROR << e.GetDescription(); return EXIT_FAILURE; } catch (const std::exception& e) { MITK_ERROR << e.what(); return EXIT_FAILURE; } catch (...) { MITK_ERROR << "An unknown error occurred!"; return EXIT_FAILURE; } return returnValue; } diff --git a/Modules/DICOM/include/mitkClassicDICOMSeriesReader.h b/Modules/DICOM/include/mitkClassicDICOMSeriesReader.h index 7e00602a37..be66ab02f1 100644 --- a/Modules/DICOM/include/mitkClassicDICOMSeriesReader.h +++ b/Modules/DICOM/include/mitkClassicDICOMSeriesReader.h @@ -1,78 +1,78 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkClassicDICOMSeriesReader_h #define mitkClassicDICOMSeriesReader_h #include "mitkThreeDnTDICOMSeriesReader.h" #include "MitkDICOMExports.h" namespace mitk { /** \ingroup DICOMModule \brief Sorting and grouping like mitk::DicomSeriesReader until 2013. This class implements the same functionality as the legacy class DicomSeriesReader, except that it is 75 lines instead of 2500 lines. \warning Since the old class is known to have problems with some series, it is advised to use a good configuration of DICOMITKSeriesGDCMReader, which can be obtained by using DICOMFileReaderSelector. The following text documents the actual sorting logic of this reader. The class groups datasets that have different values in any of the following tags: - (0028,0010) Number of Rows - (0028,0011) Number of Columns - (0028,0030) Pixel Spacing - (0018,1164) Imager Pixel Spacing - (0020,0037) %Image Orientation (Patient) - (0018,0050) Slice Thickness - (0028,0008) Number of Frames - (0020,000e) Series Instance UID Within each of the groups, datasets are sorted by the value of the following tags (primary sorting first): - (0020,0032) %Image Position (Patient) (distance from zero along normal of (0020,0037) %Image Orientation (Patient)) - (0020,0012) Acquisition Number - (0008,0032) Acquisition Time - (0018,1060) Trigger Time - (0008,0018) SOP Instance UID (last resort, not really meaningful but decides clearly) If the series was acquired using a tilted gantry, this will be "fixed" by applying a shear transformation. If multiple images occupy the same position in space, it is assumed that this indicated a 3D+t image. */ class MITKDICOM_EXPORT ClassicDICOMSeriesReader : public ThreeDnTDICOMSeriesReader { public: mitkClassMacro( ClassicDICOMSeriesReader, DICOMITKSeriesGDCMReader ); - mitkCloneMacro( ClassicDICOMSeriesReader ); itkNewMacro( ClassicDICOMSeriesReader ); bool operator==(const DICOMFileReader& other) const override; protected: + mitkCloneMacro(ClassicDICOMSeriesReader); ClassicDICOMSeriesReader(); ~ClassicDICOMSeriesReader() override; ClassicDICOMSeriesReader(const ClassicDICOMSeriesReader& other); ClassicDICOMSeriesReader& operator=(const ClassicDICOMSeriesReader& other); }; } #endif diff --git a/Modules/DICOM/include/mitkDICOMDatasetSorter.h b/Modules/DICOM/include/mitkDICOMDatasetSorter.h index c2b8ad4066..84898d35dd 100644 --- a/Modules/DICOM/include/mitkDICOMDatasetSorter.h +++ b/Modules/DICOM/include/mitkDICOMDatasetSorter.h @@ -1,91 +1,95 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkDICOMDatasetSorter_h #define mitkDICOMDatasetSorter_h #include "itkObjectFactory.h" #include "mitkCommon.h" #include "mitkDICOMDatasetAccess.h" +#include "mitkIOVolumeSplitReason.h" namespace mitk { /** \ingroup DICOMModule \brief The sorting/splitting building-block of DICOMITKSeriesGDCMReader. This class describes the interface of the sorting/splitting process described as part of DICOMITKSeriesGDCMReader::AnalyzeInputFiles() (see \ref DICOMITKSeriesGDCMReader_LoadingStrategy). The procedure is simple: - take a list of input datasets (DICOMDatasetAccess) - sort them (to be defined by sub-classes, based on specific tags) - return the sorting result as outputs (the single input might be distributed into multiple outputs) The simplest and most generic form of sorting is implemented in sub-class DICOMTagBasedSorter. */ class MITKDICOM_EXPORT DICOMDatasetSorter : public itk::LightObject { public: mitkClassMacroItkParent( DICOMDatasetSorter, itk::LightObject ); /** \brief Return the tags of interest (to facilitate scanning) */ virtual DICOMTagList GetTagsOfInterest() = 0; /// \brief Input for sorting void SetInput(DICOMDatasetList filenames); /// \brief Input for sorting const DICOMDatasetList& GetInput() const; /// \brief Sort input datasets into one or multiple outputs. virtual void Sort() = 0; /// \brief Output of the sorting process. unsigned int GetNumberOfOutputs() const; /// \brief Output of the sorting process. const DICOMDatasetList& GetOutput(unsigned int index) const; /// \brief Output of the sorting process. DICOMDatasetList& GetOutput(unsigned int index); + IOVolumeSplitReason::ConstPointer GetSplitReason(unsigned int index) const; + /// \brief Print configuration details into stream. virtual void PrintConfiguration(std::ostream& os, const std::string& indent = "") const = 0; virtual bool operator==(const DICOMDatasetSorter& other) const = 0; protected: DICOMDatasetSorter(); ~DICOMDatasetSorter() override; DICOMDatasetSorter(const DICOMDatasetSorter& other); DICOMDatasetSorter& operator=(const DICOMDatasetSorter& other); void ClearOutputs(); void SetNumberOfOutputs(unsigned int numberOfOutputs); - void SetOutput(unsigned int index, const DICOMDatasetList& output); + void SetOutput(unsigned int index, const DICOMDatasetList& output, IOVolumeSplitReason::ConstPointer splitReason = nullptr); private: DICOMDatasetList m_Input; std::vector< DICOMDatasetList > m_Outputs; + std::vector< IOVolumeSplitReason::Pointer > m_SplitReasons; }; } #endif diff --git a/Modules/DICOM/include/mitkDICOMITKSeriesGDCMReader.h b/Modules/DICOM/include/mitkDICOMITKSeriesGDCMReader.h index 62b242f8d3..e6c44f2ab5 100644 --- a/Modules/DICOM/include/mitkDICOMITKSeriesGDCMReader.h +++ b/Modules/DICOM/include/mitkDICOMITKSeriesGDCMReader.h @@ -1,377 +1,378 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkDICOMITKSeriesGDCMReader_h #define mitkDICOMITKSeriesGDCMReader_h #include #include #include "mitkDICOMFileReader.h" #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMGDCMImageFrameInfo.h" #include "mitkEquiDistantBlocksSorter.h" #include "mitkNormalDirectionConsistencySorter.h" #include "MitkDICOMExports.h" namespace itk { class TimeProbesCollectorBase; } namespace mitk { /** \ingroup DICOMModule \brief Flexible reader based on itk::ImageSeriesReader and GDCM, for single-slice modalities like CT, MR, PET, CR, etc. Implements the loading processed as structured by DICOMFileReader offers configuration of its loading strategy. Documentation sections: - \ref DICOMITKSeriesGDCMReader_LoadingStrategy - \ref DICOMITKSeriesGDCMReader_ForcedConfiguration - \ref DICOMITKSeriesGDCMReader_UserConfiguration - \ref DICOMITKSeriesGDCMReader_GantryTilt - \ref DICOMITKSeriesGDCMReader_Testing - \ref DICOMITKSeriesGDCMReader_Internals - \ref DICOMITKSeriesGDCMReader_RelatedClasses - \ref DICOMITKSeriesGDCMReader_TiltInternals - \ref DICOMITKSeriesGDCMReader_Condensing \section DICOMITKSeriesGDCMReader_LoadingStrategy Loading strategy The set of input files is processed by a number of DICOMDatasetSorter objects which may do two sort of things: 1. split a list of input frames into multiple lists, based on DICOM tags such as "Rows", "Columns", which cannot be mixed within a single mitk::Image 2. sort the frames within the input lists, based on the values of DICOM tags such as "Image Position Patient" When the DICOMITKSeriesGDCMReader is configured with DICOMDatasetSorter%s, the list of input files is processed as follows: 1. build an initial set of output groups, simply by grouping all input files. 2. for each configured DICOMDatasetSorter, process: - for each output group: 1. set this group's files as input to the sorter 2. let the sorter sort (and split) 3. integrate the sorter's output groups with our own output groups \section DICOMITKSeriesGDCMReader_ForcedConfiguration Forced Configuration In all cases, the reader will add two DICOMDatasetSorter objects that are required to load mitk::Images properly via itk::ImageSeriesReader: 1. As a \b first step, the input files will be split into groups that are not compatible because they differ in essential aspects: - (0028,0010) Number of Rows - (0028,0011) Number of Columns - (0028,0030) Pixel Spacing - (0018,1164) Imager Pixel Spacing - (0020,0037) %Image Orientation (Patient) - (0018,0050) Slice Thickness - (0028,0008) Number of Frames 2. As are two forced \b last steps: 1. There will always be an instance of EquiDistantBlocksSorter, which ensures that there is an equal distance between all the frames of an Image. This is required to achieve correct geometrical positions in the mitk::Image, i.e. it is essential to be able to make measurements in images. - whether or not the distance is required to be orthogonal to the image planes is configured by SetFixTiltByShearing(). - during this check, we need to tolerate some minor errors in documented vs. calculated image origins. The amount of tolerance can be adjusted by SetToleratedOriginOffset() and SetToleratedOriginOffsetToAdaptive(). Please see EquiDistantBlocksSorter for more details. The default should be good for most cases. 2. There is always an instance of NormalDirectionConsistencySorter, which makes the order of images go along the image normals (see NormalDirectionConsistencySorter) \section DICOMITKSeriesGDCMReader_UserConfiguration User Configuration The user of this class can add more sorting steps (similar to the one described in above section) by calling AddSortingElement(). Usually, an application will add sorting by "Image Position Patient", by "Instance Number", and by other relevant tags here. \section DICOMITKSeriesGDCMReader_GantryTilt Gantry tilt handling When CT gantry tilt is used, the gantry plane (= X-Ray source and detector ring) and the vertical plane do not align anymore. This scanner feature is used for example to reduce metal artifacts (e.g. Lee C , Evaluation of Using CT Gantry Tilt Scan on Head and Neck Cancer Patients with Dental Structure: Scans Show Less Metal Artifacts. Presented at: Radiological Society of North America 2011 Scientific Assembly and Annual Meeting; November 27- December 2, 2011 Chicago IL.). The acquired planes of such CT series do not match the expectations of a orthogonal geometry in mitk::Image: if you stack the slices, they show a small shift along the Y axis: \verbatim without tilt with tilt |||||| ////// |||||| ////// -- |||||| --------- ////// -------- table orientation |||||| ////// |||||| ////// Stacked slices: without tilt with tilt -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- -------------- \endverbatim As such gemetries do not "work" in conjunction with mitk::Image, DICOMITKSeriesGDCMReader is able to perform a correction for such series. Whether or not such correction should be attempted is controlled by SetFixTiltByShearing(), the default being correction. For details, see "Internals" below. \section DICOMITKSeriesGDCMReader_Testing Testing A number of tests is implemented in module DICOMTesting, which is documented at \ref DICOMTesting. \section DICOMITKSeriesGDCMReader_Internals Class internals Internally, the class is based on GDCM and it depends heavily on the gdcm::Scanner class. Since the sorting elements (see DICOMDatasetSorter and DICOMSortCriterion) can access tags only via the DICOMDatasetAccess interface, BUT DICOMITKSeriesGDCMReader holds a list of more specific classes DICOMGDCMImageFrameInfo, we must convert between the two types sometimes. This explains the methods ToDICOMDatasetList(), FromDICOMDatasetList(). The intermediate result of all the sorting efforts is held in m_SortingResultInProgress, which is modified through InternalExecuteSortingStep(). \subsection DICOMITKSeriesGDCMReader_RelatedClasses Overview of related classes The following diagram gives an overview of the related classes: \image html implementeditkseriesgdcmreader.jpg \subsection DICOMITKSeriesGDCMReader_TiltInternals Details about the tilt correction The gantry tilt "correction" algorithm fixes two errors introduced by ITK's ImageSeriesReader: - the plane shift that is ignored by ITK's reader is recreated by applying a shearing transformation using itk::ResampleFilter. - the spacing is corrected (it is calculated by ITK's reader from the distance between two origins, which is NOT the slice distance in this special case) Both errors are introduced in itkImageSeriesReader.txx (ImageSeriesReader::GenerateOutputInformation(void)), lines 176 to 245 (as of ITK 3.20) For the correction, we examine two consecutive slices of a series, both described as a pair (origin/orientation): - we calculate if the first origin is on a line along the normal of the second slice - if this is not the case, the geometry will not fit a normal mitk::Image/mitk::%Geometry3D - we then project the second origin into the first slice's coordinate system to quantify the shift - both is done in class GantryTiltInformation with quite some comments. The geometry of image stacks with tilted geometries is illustrated below: - green: the DICOM images as described by their tags: origin as a point with the line indicating the orientation - red: the output of ITK ImageSeriesReader: wrong, larger spacing, no tilt - blue: how much a shear must correct \image html Modules/DICOM/doc/Doxygen/tilt-correction.jpg \subsection DICOMITKSeriesGDCMReader_Condensing Sub-classes can condense multiple blocks into a single larger block The sorting/splitting process described above is helpful for at least two more DICOM readers, which either try to load 3D+t images or which load diffusion data. In both cases, a single pixel of the mitk::Image is made up of multiple values, in one case values over time, in the other case multiple measurements of a single point. The specialized readers for these cases (e.g. ThreeDnTDICOMSeriesReader) can reuse most of the methods in DICOMITKSeriesGDCMReader, except that they need an extra step after the usual sorting, in which they can merge already grouped 3D blocks. What blocks are merged depends on the specialized reader's understanding of these images. To allow for such merging, a method Condense3DBlocks() is called as an absolute last step of AnalyzeInputFiles(). Given this, a sub-class could implement only LoadImages() and Condense3DBlocks() instead repeating most of AnalyzeInputFiles(). */ class MITKDICOM_EXPORT DICOMITKSeriesGDCMReader : public DICOMFileReader { public: mitkClassMacro( DICOMITKSeriesGDCMReader, DICOMFileReader ); mitkCloneMacro( DICOMITKSeriesGDCMReader ); itkFactorylessNewMacro( DICOMITKSeriesGDCMReader ); mitkNewMacro1Param( DICOMITKSeriesGDCMReader, unsigned int ); mitkNewMacro2Param( DICOMITKSeriesGDCMReader, unsigned int, bool ); /** \brief Runs the sorting / splitting process described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. Method required by DICOMFileReader. */ void AnalyzeInputFiles() override; // void AllocateOutputImages(); /** \brief Loads images using itk::ImageSeriesReader, potentially applies shearing to correct gantry tilt. */ bool LoadImages() override; // re-implemented from super-class bool CanHandleFile(const std::string& filename) override; /** \brief Add an element to the sorting procedure described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. */ virtual void AddSortingElement(DICOMDatasetSorter* sorter, bool atFront = false); typedef const std::list ConstSorterList; ConstSorterList GetFreelyConfiguredSortingElements() const; /** \brief Controls whether to "fix" tilted acquisitions by shearing the output (see \ref DICOMITKSeriesGDCMReader_GantryTilt). */ void SetFixTiltByShearing(bool on); bool GetFixTiltByShearing() const; /** \brief Controls whether groups of only two images are accepted when ensuring consecutive slices via EquiDistantBlocksSorter. */ void SetAcceptTwoSlicesGroups(bool accept) const; bool GetAcceptTwoSlicesGroups() const; /** \brief See \ref DICOMITKSeriesGDCMReader_ForcedConfiguration. */ void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct = 0.3) const; /** \brief See \ref DICOMITKSeriesGDCMReader_ForcedConfiguration. */ void SetToleratedOriginOffset(double millimeters = 0.005) const; /** \brief Ignore all dicom tags that are non-essential for simple 3D volume import. */ void SetSimpleVolumeReading(bool read) { m_SimpleVolumeReading = read; }; /** \brief Ignore all dicom tags that are non-essential for simple 3D volume import. */ bool GetSimpleVolumeReading() { return m_SimpleVolumeReading; }; double GetToleratedOriginError() const; bool IsToleratedOriginOffsetAbsolute() const; double GetDecimalPlacesForOrientation() const; bool operator==(const DICOMFileReader& other) const override; DICOMTagPathList GetTagsOfInterest() const override; static int GetDefaultDecimalPlacesForOrientation() { return m_DefaultDecimalPlacesForOrientation; } static bool GetDefaultSimpleVolumeImport() { return m_DefaultSimpleVolumeImport; } static bool GetDefaultFixTiltByShearing() { return m_DefaultFixTiltByShearing; } protected: void InternalPrintConfiguration(std::ostream& os) const override; /// \brief Return active C locale - static std::string GetActiveLocale(); + static std::string GetActiveLocale(); /** \brief Remember current locale on stack, activate "C" locale. "C" locale is required for correct parsing of numbers by itk::ImageSeriesReader */ void PushLocale() const; /** \brief Activate last remembered locale from locale stack "C" locale is required for correct parsing of numbers by itk::ImageSeriesReader */ void PopLocale() const; const static int m_DefaultDecimalPlacesForOrientation = 5; const static bool m_DefaultSimpleVolumeImport = false; const static bool m_DefaultFixTiltByShearing = true; DICOMITKSeriesGDCMReader(unsigned int decimalPlacesForOrientation = m_DefaultDecimalPlacesForOrientation, bool simpleVolumeImport = m_DefaultSimpleVolumeImport); ~DICOMITKSeriesGDCMReader() override; DICOMITKSeriesGDCMReader(const DICOMITKSeriesGDCMReader& other); DICOMITKSeriesGDCMReader& operator=(const DICOMITKSeriesGDCMReader& other); - typedef std::vector SortingBlockList; + using SortingBlockListItemType = std::pair; + using SortingBlockList = std::vector ; /** \brief "Hook" for sub-classes, see \ref DICOMITKSeriesGDCMReader_Condensing \return REMAINING blocks */ virtual SortingBlockList Condense3DBlocks(SortingBlockList& resultOf3DGrouping); virtual DICOMTagCache::Pointer GetTagCache() const; void SetTagCache( const DICOMTagCache::Pointer& ) override; /// \brief Sorting step as described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy - static SortingBlockList InternalExecuteSortingStep( + static SortingBlockList InternalExecuteSortingStep( unsigned int sortingStepIndex, const DICOMDatasetSorter::Pointer& sorter, const SortingBlockList& input); /// \brief Loads the mitk::Image by means of an itk::ImageSeriesReader virtual bool LoadMitkImageForOutput(unsigned int o); virtual bool LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const; /// \brief Describe this reader's confidence for given SOP class UID - static ReaderImplementationLevel GetReaderImplementationLevel(const std::string sopClassUID); + static ReaderImplementationLevel GetReaderImplementationLevel(const std::string sopClassUID); private: /// \brief Creates the required sorting steps described in \ref DICOMITKSeriesGDCMReader_ForcedConfiguration void EnsureMandatorySortersArePresent(unsigned int decimalPlacesForOrientation, bool simpleVolumeImport = false); protected: // NOT nice, made available to ThreeDnTDICOMSeriesReader due to lack of time bool m_FixTiltByShearing; // could be removed by ITKDICOMSeriesReader NOT flagging tilt unless requested to fix it! bool m_SimpleVolumeReading; private: SortingBlockList m_SortingResultInProgress; typedef std::list SorterList; SorterList m_Sorter; protected: // NOT nice, made available to ThreeDnTDICOMSeriesReader and ClassicDICOMSeriesReader due to lack of time mitk::EquiDistantBlocksSorter::Pointer m_EquiDistantBlocksSorter; mitk::NormalDirectionConsistencySorter::Pointer m_NormalDirectionConsistencySorter; private: static std::mutex s_LocaleMutex; mutable std::stack m_ReplacedCLocales; mutable std::stack m_ReplacedCinLocales; double m_DecimalPlacesForOrientation; DICOMTagCache::Pointer m_TagCache; bool m_ExternalCache; }; } #endif diff --git a/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h b/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h index db2a634911..1434cda24f 100644 --- a/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h +++ b/Modules/DICOM/include/mitkDICOMImageBlockDescriptor.h @@ -1,237 +1,247 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkDICOMImageBlockDescriptor_h #define mitkDICOMImageBlockDescriptor_h #include "mitkDICOMEnums.h" #include "mitkDICOMImageFrameInfo.h" #include "mitkDICOMTag.h" #include "mitkDICOMTagCache.h" +#include "mitkIOVolumeSplitReason.h" #include "mitkImage.h" #include "mitkProperties.h" #include "mitkWeakPointer.h" #include "mitkIPropertyProvider.h" #include "mitkGantryTiltInformation.h" #include namespace mitk { struct DICOMCachedValueInfo { unsigned int TimePoint; unsigned int SliceInTimePoint; std::string Value; }; class DICOMCachedValueLookupTable : public GenericLookupTable< DICOMCachedValueInfo > { public: typedef DICOMCachedValueLookupTable Self; typedef GenericLookupTable< DICOMCachedValueInfo > Superclass; const char *GetNameOfClass() const override { return "DICOMCachedValueLookupTable"; } DICOMCachedValueLookupTable() {} Superclass& operator=(const Superclass& other) override { return Superclass::operator=(other); } ~DICOMCachedValueLookupTable() override {} }; /** \ingroup DICOMModule \brief Output descriptor for DICOMFileReader. As a result of analysis by a mitk::DICOMFileReader, this class describes the properties of a single mitk::Images that could be loaded by the file reader. The descriptor contains the following information: - the mitk::Image itself. This will be nullptr after analysis and only be present after actual loading. - a list of frames (mostly: filenames) that went into composition of the mitk::Image. - an assessment of the reader's ability to load this set of files (ReaderImplementationLevel) - this can be used for reader selection when one reader is able to load an image with correct colors and the other is able to produce only gray values, for example - description of aspects of the image. Mostly a key-value list implemented by means of mitk::PropertyList. - for specific keys and possible values, see documentation of specific readers. \note an mitk::Image may both consist of multiple files (the "old" DICOM way) or a mitk::Image may be described by a single DICOM file or even only parts of a DICOM file (the newer multi-frame DICOM classes). To reflect this DICOMImageFrameList describes a list of frames from different or a single file. Described aspects of an image are: - whether pixel spacing is meant to be in-patient or on-detector (mitk::PixelSpacingInterpretation) - details about a possible gantry tilt (intended for use by file readers, may be hidden later) */ class MITKDICOM_EXPORT DICOMImageBlockDescriptor: public IPropertyProvider { public: DICOMImageBlockDescriptor(); ~DICOMImageBlockDescriptor() override; DICOMImageBlockDescriptor(const DICOMImageBlockDescriptor& other); DICOMImageBlockDescriptor& operator=(const DICOMImageBlockDescriptor& other); static DICOMTagList GetTagsOfInterest(); /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) void SetImageFrameList(const DICOMImageFrameList& framelist); /// List of frames that constitute the mitk::Image (DICOMImageFrame%s) const DICOMImageFrameList& GetImageFrameList() const; /// The 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList void SetMitkImage(Image::Pointer image); /// the 3D mitk::Image that is loaded from the DICOM files of a DICOMImageFrameList Image::Pointer GetMitkImage() const; /// Reader's capability to appropriately load this set of frames ReaderImplementationLevel GetReaderImplementationLevel() const; /// Reader's capability to appropriately load this set of frames void SetReaderImplementationLevel(const ReaderImplementationLevel& level); /// Key-value store describing aspects of the image to be loaded void SetProperty(const std::string& key, BaseProperty* value); /// Key-value store describing aspects of the image to be loaded BaseProperty* GetProperty(const std::string& key) const; /// Convenience function around GetProperty() std::string GetPropertyAsString(const std::string&) const; + /** Returns the pointer to the split reason of this block descriptor.*/ + IOVolumeSplitReason::ConstPointer GetSplitReason() const; + /** Returns the pointer to the split reason of this block descriptor.*/ + IOVolumeSplitReason::Pointer GetSplitReason(); + /** Sets the split reason for the block descriptor.*/ + void SetSplitReason(IOVolumeSplitReason::Pointer reason); + /// Convenience function around SetProperty() void SetFlag(const std::string& key, bool value); /// Convenience function around GetProperty() bool GetFlag(const std::string& key, bool defaultValue) const; /// Convenience function around SetProperty() void SetIntProperty(const std::string& key, int value); /// Convenience function around GetProperty() int GetIntProperty(const std::string& key, int defaultValue) const; BaseProperty::ConstPointer GetConstProperty(const std::string &propertyKey, const std::string &contextName = "", bool fallBackOnDefaultContext = true) const override; std::vector GetPropertyKeys(const std::string &contextName = "", bool includeDefaultContext = false) const override; std::vector GetPropertyContextNames() const override; private: // For future implementation: load slice-by-slice, mark this using these methods void SetSliceIsLoaded(unsigned int index, bool isLoaded); // For future implementation: load slice-by-slice, mark this using these methods bool IsSliceLoaded(unsigned int index) const; // For future implementation: load slice-by-slice, mark this using these methods bool AllSlicesAreLoaded() const; public: /// Describe how the mitk::Image's pixel spacing should be interpreted PixelSpacingInterpretation GetPixelSpacingInterpretation() const; /// Describe the correct x/y pixel spacing of the mitk::Image (which some readers might need to adjust after loading) void GetDesiredMITKImagePixelSpacing(ScalarType& spacingXinMM, ScalarType& spacingYinMM) const; /// Describe the gantry tilt of the acquisition void SetTiltInformation(const GantryTiltInformation& info); /// Describe the gantry tilt of the acquisition const GantryTiltInformation GetTiltInformation() const; /// SOP Class UID of this set of frames void SetSOPClassUID(const std::string& uid); /// SOP Class UID of this set of frames std::string GetSOPClassUID() const; /// SOP Class as human readable name (e.g. "CT Image Storage") std::string GetSOPClassUIDAsName() const; /**Convenience method that returns the property timesteps*/ int GetNumberOfTimeSteps() const; /**return the number of frames that constitute one timestep.*/ int GetNumberOfFramesPerTimeStep() const; void SetTagCache(DICOMTagCache* privateCache); /** Type specifies additional tags of interest. Key is the tag path of interest. * The value is an optional user defined name for the property that should be used to store the tag value(s). * Empty value is default and will imply to use the found DICOMTagPath as property name.*/ typedef std::map AdditionalTagsMapType; /** * \brief Set a list of DICOMTagPaths that specify all DICOM-Tags that will be copied into the property of the mitk::Image. * * This method can be used to specify a list of DICOM-tags that shall be available after the loading. * The value in the tagMap is an optional user defined name for the property key that should be used * when storing the property). Empty value is default and will imply to use the found DICOMTagPath * as property key. * By default the content of the DICOM tags will be stored in a StringLookupTable on the mitk::Image. * This behaviour can be changed by setting a different TagLookupTableToPropertyFunctor via * SetTagLookupTableToPropertyFunctor(). */ void SetAdditionalTagsOfInterest(const AdditionalTagsMapType& tagMap); typedef std::function TagLookupTableToPropertyFunctor; /** * \brief Set a functor that defines how the slice-specific tag-values are stored in a Property. * * This method sets a functor that is given a StringLookupTable that contains the values of one DICOM tag * mapped to the slice index. * The functor is supposed to store these values in an mitk Property. * * By default, the StringLookupTable is stored in a StringLookupTableProperty except if all values are * identical. In this case, the unique value is stored only once in a StringProperty. */ void SetTagLookupTableToPropertyFunctor(TagLookupTableToPropertyFunctor); /// Print information about this image block to given stream void Print(std::ostream& os, bool filenameDetails) const; private: // read values from tag cache std::string GetPixelSpacing() const; std::string GetImagerPixelSpacing() const; Image::Pointer FixupSpacing(Image* mitkImage); Image::Pointer DescribeImageWithProperties(Image* mitkImage); void UpdateImageDescribingProperties() const; static mitk::BaseProperty::Pointer GetPropertyForDICOMValues(const DICOMCachedValueLookupTable& cacheLookupTable); double stringtodouble(const std::string& str) const; DICOMImageFrameList m_ImageFrameList; Image::Pointer m_MitkImage; BoolList m_SliceIsLoaded; ReaderImplementationLevel m_ReaderImplementationLevel; GantryTiltInformation m_TiltInformation; PropertyList::Pointer m_PropertyList; - mitk::WeakPointer m_TagCache; + IOVolumeSplitReason::Pointer m_SplitReason; + + WeakPointer m_TagCache; mutable bool m_PropertiesOutOfDate; AdditionalTagsMapType m_AdditionalTagMap; std::set m_FoundAdditionalTags; TagLookupTableToPropertyFunctor m_PropertyFunctor; }; } #endif diff --git a/Modules/DICOM/include/mitkDICOMTagBasedSorter.h b/Modules/DICOM/include/mitkDICOMTagBasedSorter.h index b708bf53c1..8cc2b2be5d 100644 --- a/Modules/DICOM/include/mitkDICOMTagBasedSorter.h +++ b/Modules/DICOM/include/mitkDICOMTagBasedSorter.h @@ -1,202 +1,206 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkDICOMTagBasedSorter_h #define mitkDICOMTagBasedSorter_h #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMSortCriterion.h" namespace mitk { /** \ingroup DICOMModule \brief Sort DICOM datasets based on configurable tags. This class implements sorting of input DICOM datasets into multiple outputs as described in \ref DICOMITKSeriesGDCMReader_LoadingStrategy. The logic of sorting and splitting is most simple and most generic: 1. Datasets will be put into different groups, if they differ in their value of specific tags (defined by AddDistinguishingTag()) - there might be multiple distinguishing tags defined - tag values might be processed before comparison by means of TagValueProcessor (e.g. round to a number of decimal places) 2. Each of the groups will be sorted by comparing their tag values using multiple DICOMSortCriterion - DICOMSortCriterion might evaluate a single tag (e.g. Instance Number) or multiple values (as in SortByImagePositionPatient) - only a single DICOMSortCriterion is defined for DICOMTagBasedSorter, because each DICOMSortCriterion holds a "secondary sort criterion", i.e. an application can define multiple tags for sorting by chaining \link DICOMSortCriterion DICOMSortCriteria \endlink - applications should make sure that sorting is always defined (to avoid problems with standard containers), e.g. by adding a comparison of filenames or instance UIDs as a last sorting fallback. */ class MITKDICOM_EXPORT DICOMTagBasedSorter : public DICOMDatasetSorter { public: /** \brief Processes tag values before they are compared. These classes could do some kind of normalization such as rounding, lower case formatting, etc. */ class MITKDICOM_EXPORT TagValueProcessor { public: /// \brief Implements the "processing". virtual std::string operator()(const std::string&) const = 0; virtual TagValueProcessor* Clone() const = 0; virtual ~TagValueProcessor() {} }; /** \brief Cuts a number after configured number of decimal places. An instance of this class can be used to avoid errors when comparing minimally different image orientations. */ class MITKDICOM_EXPORT CutDecimalPlaces : public TagValueProcessor { public: CutDecimalPlaces(unsigned int precision); CutDecimalPlaces(const CutDecimalPlaces& other); unsigned int GetPrecision() const; std::string operator()(const std::string&) const override; TagValueProcessor* Clone() const override; private: unsigned int m_Precision; }; mitkClassMacro( DICOMTagBasedSorter, DICOMDatasetSorter ); itkNewMacro( DICOMTagBasedSorter ); /** \brief Datasets that differ in given tag's value will be sorted into separate outputs. */ void AddDistinguishingTag( const DICOMTag&, TagValueProcessor* tagValueProcessor = nullptr ); DICOMTagList GetDistinguishingTags() const; const TagValueProcessor* GetTagValueProcessorForDistinguishingTag(const DICOMTag&) const; /** \brief Define the sorting criterion (which holds seconardy criteria) */ void SetSortCriterion( DICOMSortCriterion::ConstPointer criterion ); DICOMSortCriterion::ConstPointer GetSortCriterion() const; /** \brief A list of all the tags needed for processing (facilitates scanning). */ DICOMTagList GetTagsOfInterest() override; /** \brief Whether or not groups should be checked for consecutive tag values. When this flag is set (default in constructor=off), the sorter will not only sort in a way that the values of a configured tag are ascending BUT in addition the sorter will enforce a constant numerical distance between values. Having this flag is useful for handling of series with missing slices, e.g. Instance Numbers 1 2 3 5 6 7 8. With the flag set to true, the sorter would split this group into two, because the initial distance of 1 is not kept between Instance Numbers 3 and 5. A special case of this behavior can be configured by SetExpectDistanceOne(). When this additional flag is set to true, the sorter will expect distance 1 exactly. This can help if the second slice is missing already. Without this additional flag, we would "learn" about a wrong distance of 2 (or similar) and then sort completely wrong. */ void SetStrictSorting(bool strict); bool GetStrictSorting() const; /** \brief Flag for a special case in "strict sorting". Please see documentation of SetStrictSorting(). \sa SetStrictSorting */ void SetExpectDistanceOne(bool strict); bool GetExpectDistanceOne() const; /** \brief Actually sort as described in the Detailed Description. */ void Sort() override; /** \brief Print configuration details into given stream. */ void PrintConfiguration(std::ostream& os, const std::string& indent = "") const override; bool operator==(const DICOMDatasetSorter& other) const override; static bool GetDefaultStrictSorting() { return m_DefaultStrictSorting; } static bool GetDefaultExpectDistanceOne() { return m_DefaultExpectDistanceOne; } protected: /** \brief Helper struct to feed into std::sort, configured via DICOMSortCriterion. */ struct ParameterizedDatasetSort { ParameterizedDatasetSort(DICOMSortCriterion::ConstPointer); bool operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right); DICOMSortCriterion::ConstPointer m_SortCriterion; }; DICOMTagBasedSorter(); ~DICOMTagBasedSorter() override; DICOMTagBasedSorter(const DICOMTagBasedSorter& other); DICOMTagBasedSorter& operator=(const DICOMTagBasedSorter& other); /** \brief Helper for SplitInputGroups(). */ std::string BuildGroupID( DICOMDatasetAccess* dataset ); - typedef std::map GroupIDToListType; + using GroupIDToListType = std::map; + + using SplitReasonListType = std::map; /** \brief Implements the "distiguishing tags". To sort datasets into different groups, a long string will be built for each dataset. The string concatenates all tags and their respective values. Datasets that match in all values will end up with the same string. + @param splitReasons Reference to the split reasons vector. It will be also updated by the method to reflect the reasons for the returned groups. */ - GroupIDToListType SplitInputGroups(); + GroupIDToListType SplitInputGroups(SplitReasonListType& splitReasons); /** \brief Implements the sorting step. Relatively simple implementation thanks to std::sort and a parameterization via DICOMSortCriterion. + @param splitReasons Reference to the split reasons vector. It will be also updated by the method to reflect the reasons for the returned groups. */ - GroupIDToListType& SortGroups(GroupIDToListType& groups); + GroupIDToListType& SortGroups(GroupIDToListType& groups, SplitReasonListType& splitReasons); DICOMTagList m_DistinguishingTags; typedef std::map TagValueProcessorMap; TagValueProcessorMap m_TagValueProcessor; DICOMSortCriterion::ConstPointer m_SortCriterion; bool m_StrictSorting; bool m_ExpectDistanceOne; const static bool m_DefaultStrictSorting = false; const static bool m_DefaultExpectDistanceOne = false; }; } #endif diff --git a/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h b/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h index 26e1b0da35..871d0e8341 100644 --- a/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h +++ b/Modules/DICOM/include/mitkEquiDistantBlocksSorter.h @@ -1,211 +1,222 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef mitkEquiDistantBlocksSorter_h #define mitkEquiDistantBlocksSorter_h #include "mitkDICOMDatasetSorter.h" #include "mitkDICOMSortCriterion.h" #include "mitkGantryTiltInformation.h" #include "mitkVector.h" namespace mitk { /** \ingroup DICOMModule \brief Split inputs into blocks of equidistant slices (for use in DICOMITKSeriesGDCMReader). Since inter-slice distance is not recorded in DICOM tags, we must ensure that blocks are made up of slices that have equal distances between neighboring slices. This is especially necessary because itk::ImageSeriesReader is later used for the actual loading, and this class expects (and does nocht verify) equal inter-slice distance (see \ref DICOMITKSeriesGDCMReader_ForcedConfiguration). To achieve such grouping, the inter-slice distance is calculated from the first two different slice positions of a block. Following slices are added to a block as long as they can be added by adding the calculated inter-slice distance to the last slice of the block. Slices that do not fit into the expected distance pattern, are set aside for further analysis. This grouping is done until each file has been assigned to a group. Slices that share a position in space are also sorted into separate blocks during this step. So the result of this step is a set of blocks that contain only slices with equal z spacing and unique slices at each position. During sorting, the origins (documented in tag image position patient) are compared against expected origins (from former origin plus moving direction). As there will be minor differences in numbers (from both calculations and imprecise tag values), we must be a bit tolerant here. The default behavior is to expect that an origin is not further away from the expected position than 30% of the inter-slice distance. To support a legacy behavior of a former loader (DicomSeriesReader), this default can be restricted to a constant number of millimeters by calling SetToleratedOriginOffset(mm). + REMARK: The EquiDistantBlocksSorter assumes that the order of the provided input + is sorted by image position like it is preferred by the reader and does not sort + it again. This assumption can lead to splittings even for complete volumes if + the input is not sorted by image position. The reason is that gaps are detected + because the next slice does not have the assumed distance and will be sorted out. + Detailed implementation in AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). */ class MITKDICOM_EXPORT EquiDistantBlocksSorter : public DICOMDatasetSorter { public: mitkClassMacro( EquiDistantBlocksSorter, DICOMDatasetSorter ); itkNewMacro( EquiDistantBlocksSorter ); DICOMTagList GetTagsOfInterest() override; /** \brief Delegates work to AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). AnalyzeFileForITKImageSeriesReaderSpacingAssumption() is called until it does not create multiple blocks anymore. */ void Sort() override; /** \brief Whether or not to accept images from a tilted acquisition in a single output group. */ void SetAcceptTilt(bool accept); bool GetAcceptTilt() const; /** \brief See class description and SetToleratedOriginOffset(). */ void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct = 0.3); /** \brief See class description and SetToleratedOriginOffsetToAdaptive(). Default value of 0.005 is calculated so that we get a maximum of 1/10mm error when having a measurement crosses 20 slices in z direction (too strict? we don't know better..). */ void SetToleratedOriginOffset(double millimeters = 0.005); double GetToleratedOriginOffset() const; bool IsToleratedOriginOffsetAbsolute() const; void SetAcceptTwoSlicesGroups(bool accept); bool GetAcceptTwoSlicesGroups() const; void PrintConfiguration(std::ostream& os, const std::string& indent = "") const override; bool operator==(const DICOMDatasetSorter& other) const override; protected: /** \brief Return type of AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). Class contains the grouping result of method AnalyzeFileForITKImageSeriesReaderSpacingAssumption(), which takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. The result contains of two blocks: a first one is the grouping result, all of those images can be loaded into one image block because they have an equal origin-to-origin distance without any gaps in-between. */ class SliceGroupingAnalysisResult { public: SliceGroupingAnalysisResult(); /** \brief Grouping result, all same origin-to-origin distance w/o gaps. */ - DICOMDatasetList GetBlockDatasets(); + const DICOMDatasetList& GetBlockDatasets() const; void SetFirstFilenameOfBlock(const std::string& filename); std::string GetFirstFilenameOfBlock() const; void SetLastFilenameOfBlock(const std::string& filename); std::string GetLastFilenameOfBlock() const; /** \brief Remaining files, which could not be grouped. */ - DICOMDatasetList GetUnsortedDatasets(); + const DICOMDatasetList& GetUnsortedDatasets() const; + + IOVolumeSplitReason::ConstPointer GetSplitReason() const; + IOVolumeSplitReason::Pointer GetSplitReason(); /** \brief Whether or not the grouped result contain a gantry tilt. */ bool ContainsGantryTilt(); /** \brief Detailed description of gantry tilt. */ const GantryTiltInformation& GetTiltInfo() const; /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToSortedBlock(DICOMDatasetAccess* dataset); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. */ void AddFileToUnsortedBlock(DICOMDatasetAccess* dataset); void AddFilesToUnsortedBlock(const DICOMDatasetList& datasets); /** \brief Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only. \todo Could make sense to enhance this with an instance of GantryTiltInformation to store the whole result! */ void FlagGantryTilt(const GantryTiltInformation& tiltInfo); /** \brief Only meaningful for use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption. */ void UndoPrematureGrouping(); protected: DICOMDatasetList m_GroupedFiles; DICOMDatasetList m_UnsortedFiles; + IOVolumeSplitReason::Pointer m_SplitReason; + GantryTiltInformation m_TiltInfo; std::string m_FirstFilenameOfBlock; std::string m_LastFilenameOfBlock; }; /** \brief Ensure an equal z-spacing for a group of files. Takes as input a number of images, which are all equally oriented and spatially sorted along their normal direction. Internally used by GetSeries. Returns two lists: the first one contains slices of equal inter-slice spacing. The second list contains remaining files, which need to be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption again. Relevant code that is matched here is in itkImageSeriesReader.txx (ImageSeriesReader::GenerateOutputInformation(void)), lines 176 to 245 (as of ITK 3.20) */ - SliceGroupingAnalysisResult + std::shared_ptr AnalyzeFileForITKImageSeriesReaderSpacingAssumption(const DICOMDatasetList& files, bool groupsOfSimilarImages); /** \brief Safely convert const char* to std::string. */ std::string ConstCharStarToString(const char* s); EquiDistantBlocksSorter(); ~EquiDistantBlocksSorter() override; EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other); EquiDistantBlocksSorter& operator=(const EquiDistantBlocksSorter& other); bool m_AcceptTilt; - typedef std::vector ResultsList; + typedef std::vector > ResultsList; ResultsList m_SliceGroupingResults; double m_ToleratedOriginOffset; bool m_ToleratedOriginOffsetIsAbsolute; bool m_AcceptTwoSlicesGroups; }; } #endif diff --git a/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp b/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp index d6af270fe5..957e913ed8 100644 --- a/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp +++ b/Modules/DICOM/src/mitkDICOMDatasetSorter.cpp @@ -1,117 +1,134 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkDICOMDatasetSorter.h" mitk::DICOMDatasetSorter ::DICOMDatasetSorter() :itk::LightObject() { } mitk::DICOMDatasetSorter ::~DICOMDatasetSorter() { } mitk::DICOMDatasetSorter ::DICOMDatasetSorter(const DICOMDatasetSorter& other ) :itk::LightObject() ,m_Outputs( other.m_Outputs ) { } mitk::DICOMDatasetSorter& mitk::DICOMDatasetSorter ::operator=(const DICOMDatasetSorter& other) { if (this != &other) { m_Input = other.m_Input; m_Outputs = other.m_Outputs; } return *this; } void mitk::DICOMDatasetSorter ::SetInput(DICOMDatasetList datasets) { m_Input = datasets; } const mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetInput() const { return m_Input; } unsigned int mitk::DICOMDatasetSorter ::GetNumberOfOutputs() const { return m_Outputs.size(); } void mitk::DICOMDatasetSorter ::ClearOutputs() { m_Outputs.clear(); + m_SplitReasons.clear(); } void mitk::DICOMDatasetSorter ::SetNumberOfOutputs(unsigned int numberOfOutputs) { m_Outputs.resize(numberOfOutputs); + m_SplitReasons.resize(numberOfOutputs); } void mitk::DICOMDatasetSorter -::SetOutput(unsigned int index, const DICOMDatasetList& output) +::SetOutput(unsigned int index, const DICOMDatasetList& output, IOVolumeSplitReason::ConstPointer splitReason) { if (index < m_Outputs.size()) { m_Outputs[index] = output; + m_SplitReasons[index] = (nullptr == splitReason) ? IOVolumeSplitReason::New() : splitReason->Clone(); } else { std::stringstream ss; - ss << "Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; + ss << "Cannot get output. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } const mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetOutput(unsigned int index) const { return const_cast(this)->GetOutput(index); } mitk::DICOMDatasetList& mitk::DICOMDatasetSorter ::GetOutput(unsigned int index) { if (index < m_Outputs.size()) { return m_Outputs[index]; } else { std::stringstream ss; - ss << "Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; + ss << "Cannot get output. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } + +mitk::IOVolumeSplitReason::ConstPointer +mitk::DICOMDatasetSorter +::GetSplitReason(unsigned int index) const +{ + if (index >= m_Outputs.size()) + { + std::stringstream ss; + ss << "Cannot get split reason. Index " << index << " out of range (" << m_Outputs.size() << " indices reserved)"; + throw std::invalid_argument(ss.str()); + } + + return m_SplitReasons[index]; +} diff --git a/Modules/DICOM/src/mitkDICOMIOMetaInformationPropertyConstants.cpp b/Modules/DICOM/src/mitkDICOMIOMetaInformationPropertyConstants.cpp index beecb531ed..b9fc0b6481 100644 --- a/Modules/DICOM/src/mitkDICOMIOMetaInformationPropertyConstants.cpp +++ b/Modules/DICOM/src/mitkDICOMIOMetaInformationPropertyConstants.cpp @@ -1,67 +1,68 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkDICOMIOMetaInformationPropertyConstants.h" namespace mitk { PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_FILES() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "files" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION_STRING() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "PixelSpacingInterpretationString" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "PixelSpacingInterpretation" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL_STRING() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "ReaderImplementationLevelString" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "ReaderImplementationLevel" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_GANTRY_TILT_CORRECTED() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "GantyTiltCorrected" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_3D_plus_t() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "3D+t" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_GDCM() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "gdcm" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_DCMTK() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "dcmtk" }); } PropertyKeyPath DICOMIOMetaInformationPropertyConstants::READER_CONFIGURATION() { return PropertyKeyPath({ "MITK", "IO", "reader", "DICOM", "configuration" }); } + } diff --git a/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp b/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp index a24947359c..98808873f0 100644 --- a/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp +++ b/Modules/DICOM/src/mitkDICOMITKSeriesGDCMReader.cpp @@ -1,628 +1,634 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ //#define MBILOG_ENABLE_DEBUG #define ENABLE_TIMING #include #include #include "mitkDICOMITKSeriesGDCMReader.h" #include "mitkITKDICOMSeriesReaderHelper.h" #include "mitkGantryTiltInformation.h" #include "mitkDICOMTagBasedSorter.h" #include "mitkDICOMGDCMTagScanner.h" std::mutex mitk::DICOMITKSeriesGDCMReader::s_LocaleMutex; mitk::DICOMITKSeriesGDCMReader::DICOMITKSeriesGDCMReader( unsigned int decimalPlacesForOrientation, bool simpleVolumeImport ) : DICOMFileReader() , m_FixTiltByShearing(m_DefaultFixTiltByShearing) , m_SimpleVolumeReading( simpleVolumeImport ) , m_DecimalPlacesForOrientation( decimalPlacesForOrientation ) , m_ExternalCache(false) { this->EnsureMandatorySortersArePresent( decimalPlacesForOrientation, simpleVolumeImport ); } mitk::DICOMITKSeriesGDCMReader::DICOMITKSeriesGDCMReader( const DICOMITKSeriesGDCMReader& other ) : DICOMFileReader( other ) , m_FixTiltByShearing( other.m_FixTiltByShearing) +, m_SimpleVolumeReading( other.m_SimpleVolumeReading) , m_SortingResultInProgress( other.m_SortingResultInProgress ) , m_Sorter( other.m_Sorter ) , m_EquiDistantBlocksSorter( other.m_EquiDistantBlocksSorter->Clone() ) , m_NormalDirectionConsistencySorter( other.m_NormalDirectionConsistencySorter->Clone() ) , m_ReplacedCLocales( other.m_ReplacedCLocales ) , m_ReplacedCinLocales( other.m_ReplacedCinLocales ) , m_DecimalPlacesForOrientation( other.m_DecimalPlacesForOrientation ) , m_TagCache( other.m_TagCache ) , m_ExternalCache(other.m_ExternalCache) { } mitk::DICOMITKSeriesGDCMReader::~DICOMITKSeriesGDCMReader() { } mitk::DICOMITKSeriesGDCMReader& mitk::DICOMITKSeriesGDCMReader:: operator=( const DICOMITKSeriesGDCMReader& other ) { if ( this != &other ) { DICOMFileReader::operator =( other ); this->m_FixTiltByShearing = other.m_FixTiltByShearing; + this->m_SimpleVolumeReading = other.m_SimpleVolumeReading; this->m_SortingResultInProgress = other.m_SortingResultInProgress; this->m_Sorter = other.m_Sorter; // TODO should clone the list items this->m_EquiDistantBlocksSorter = other.m_EquiDistantBlocksSorter->Clone(); this->m_NormalDirectionConsistencySorter = other.m_NormalDirectionConsistencySorter->Clone(); this->m_ReplacedCLocales = other.m_ReplacedCLocales; this->m_ReplacedCinLocales = other.m_ReplacedCinLocales; this->m_DecimalPlacesForOrientation = other.m_DecimalPlacesForOrientation; this->m_TagCache = other.m_TagCache; } return *this; } bool mitk::DICOMITKSeriesGDCMReader::operator==( const DICOMFileReader& other ) const { if ( const auto* otherSelf = dynamic_cast( &other ) ) { if ( this->m_FixTiltByShearing == otherSelf->m_FixTiltByShearing && *( this->m_EquiDistantBlocksSorter ) == *( otherSelf->m_EquiDistantBlocksSorter ) && ( fabs( this->m_DecimalPlacesForOrientation - otherSelf->m_DecimalPlacesForOrientation ) < eps ) ) { // test sorters for equality if ( this->m_Sorter.size() != otherSelf->m_Sorter.size() ) return false; auto mySorterIter = this->m_Sorter.cbegin(); auto oSorterIter = otherSelf->m_Sorter.cbegin(); for ( ; mySorterIter != this->m_Sorter.cend() && oSorterIter != otherSelf->m_Sorter.cend(); ++mySorterIter, ++oSorterIter ) { if ( !( **mySorterIter == **oSorterIter ) ) return false; // this sorter differs } // nothing differs ==> all is equal return true; } else { return false; } } else { return false; } } void mitk::DICOMITKSeriesGDCMReader::SetFixTiltByShearing( bool on ) { this->Modified(); m_FixTiltByShearing = on; } bool mitk::DICOMITKSeriesGDCMReader::GetFixTiltByShearing() const { return m_FixTiltByShearing; } void mitk::DICOMITKSeriesGDCMReader::SetAcceptTwoSlicesGroups( bool accept ) const { this->Modified(); m_EquiDistantBlocksSorter->SetAcceptTwoSlicesGroups( accept ); } bool mitk::DICOMITKSeriesGDCMReader::GetAcceptTwoSlicesGroups() const { return m_EquiDistantBlocksSorter->GetAcceptTwoSlicesGroups(); } void mitk::DICOMITKSeriesGDCMReader::InternalPrintConfiguration( std::ostream& os ) const { unsigned int sortIndex( 1 ); for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sortIndex, ++sorterIter ) { os << "Sorting step " << sortIndex << ":" << std::endl; ( *sorterIter )->PrintConfiguration( os, " " ); } os << "Sorting step " << sortIndex << ":" << std::endl; m_EquiDistantBlocksSorter->PrintConfiguration( os, " " ); } std::string mitk::DICOMITKSeriesGDCMReader::GetActiveLocale() { return setlocale( LC_NUMERIC, nullptr ); } void mitk::DICOMITKSeriesGDCMReader::PushLocale() const { s_LocaleMutex.lock(); std::string currentCLocale = setlocale( LC_NUMERIC, nullptr ); m_ReplacedCLocales.push( currentCLocale ); setlocale( LC_NUMERIC, "C" ); std::locale currentCinLocale( std::cin.getloc() ); m_ReplacedCinLocales.push( currentCinLocale ); std::locale l( "C" ); std::cin.imbue( l ); s_LocaleMutex.unlock(); } void mitk::DICOMITKSeriesGDCMReader::PopLocale() const { s_LocaleMutex.lock(); if ( !m_ReplacedCLocales.empty() ) { setlocale( LC_NUMERIC, m_ReplacedCLocales.top().c_str() ); m_ReplacedCLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } if ( !m_ReplacedCinLocales.empty() ) { std::cin.imbue( m_ReplacedCinLocales.top() ); m_ReplacedCinLocales.pop(); } else { MITK_WARN << "Mismatched PopLocale on DICOMITKSeriesGDCMReader."; } s_LocaleMutex.unlock(); } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::DICOMITKSeriesGDCMReader::Condense3DBlocks( SortingBlockList& input ) { return input; // to be implemented differently by sub-classes } #if defined( MBILOG_ENABLE_DEBUG ) || defined( ENABLE_TIMING ) #define timeStart( part ) timer.Start( part ); #define timeStop( part ) timer.Stop( part ); #else #define timeStart( part ) #define timeStop( part ) #endif void mitk::DICOMITKSeriesGDCMReader::AnalyzeInputFiles() { itk::TimeProbesCollectorBase timer; timeStart( "Reset" ); this->ClearOutputs(); timeStop( "Reset" ); // prepare initial sorting (== list of input files) const StringList inputFilenames = this->GetInputFiles(); timeStart( "Check input for DCM" ); if ( inputFilenames.empty() || !this->CanHandleFile( inputFilenames.front() ) // first || !this->CanHandleFile( inputFilenames.back() ) // last || !this->CanHandleFile( inputFilenames[inputFilenames.size() / 2] ) // roughly central file ) { // TODO a read-as-many-as-possible fallback could be implemented here MITK_DEBUG << "Reader unable to process files.."; return; } timeStop( "Check input for DCM" ); // scan files for sorting-relevant tags if ( m_TagCache.IsNull() || ( m_TagCache->GetMTime()GetMTime() && !m_ExternalCache )) { timeStart( "Tag scanning" ); DICOMGDCMTagScanner::Pointer filescanner = DICOMGDCMTagScanner::New(); filescanner->SetInputFiles( inputFilenames ); filescanner->AddTagPaths( this->GetTagsOfInterest() ); PushLocale(); filescanner->Scan(); PopLocale(); m_TagCache = filescanner->GetScanCache(); // keep alive and make accessible to sub-classes timeStop("Tag scanning"); } else { // ensure that the tag cache contains our required tags AND files and has scanned! } m_SortingResultInProgress.clear(); - m_SortingResultInProgress.push_back(m_TagCache->GetFrameInfoList()); + m_SortingResultInProgress.emplace_back(m_TagCache->GetFrameInfoList(), IOVolumeSplitReason::New()); // sort and split blocks as configured timeStart( "Sorting frames" ); unsigned int sorterIndex = 0; for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sorterIndex, ++sorterIter ) { std::stringstream ss; ss << "Sorting step " << sorterIndex; timeStart( ss.str().c_str() ); m_SortingResultInProgress = this->InternalExecuteSortingStep( sorterIndex, *sorterIter, m_SortingResultInProgress ); timeStop( ss.str().c_str() ); } if ( !m_SimpleVolumeReading ) { // a last extra-sorting step: ensure equidistant slices timeStart( "EquiDistantBlocksSorter" ); m_SortingResultInProgress = this->InternalExecuteSortingStep( sorterIndex++, m_EquiDistantBlocksSorter.GetPointer(), m_SortingResultInProgress ); timeStop( "EquiDistantBlocksSorter" ); } timeStop( "Sorting frames" ); timeStart( "Condensing 3D blocks" ); m_SortingResultInProgress = this->Condense3DBlocks( m_SortingResultInProgress ); timeStop( "Condensing 3D blocks" ); // provide final result as output timeStart( "Output" ); unsigned int o = this->GetNumberOfOutputs(); this->SetNumberOfOutputs( o + m_SortingResultInProgress.size() ); // Condense3DBlocks may already have added outputs! for ( auto blockIter = m_SortingResultInProgress.cbegin(); blockIter != m_SortingResultInProgress.cend(); ++o, ++blockIter ) { - const DICOMDatasetAccessingImageFrameList& gdcmFrameInfoList = *blockIter; + const auto& gdcmFrameInfoList = blockIter->first; + auto& splitReason = blockIter->second; + assert( !gdcmFrameInfoList.empty() ); // reverse frames if necessary // update tilt information from absolute last sorting const DICOMDatasetList datasetList = ConvertToDICOMDatasetList( gdcmFrameInfoList ); m_NormalDirectionConsistencySorter->SetInput( datasetList ); m_NormalDirectionConsistencySorter->Sort(); const DICOMDatasetAccessingImageFrameList sortedGdcmInfoFrameList = ConvertToDICOMDatasetAccessingImageFrameList( m_NormalDirectionConsistencySorter->GetOutput( 0 ) ); const GantryTiltInformation& tiltInfo = m_NormalDirectionConsistencySorter->GetTiltInformation(); // set frame list for current block const DICOMImageFrameList frameList = ConvertToDICOMImageFrameList( sortedGdcmInfoFrameList ); assert( !frameList.empty() ); DICOMImageBlockDescriptor block; block.SetTagCache( this->GetTagCache() ); // important: this must be before SetImageFrameList(), because // SetImageFrameList will trigger reading of lots of interesting // tags! block.SetAdditionalTagsOfInterest( GetAdditionalTagsOfInterest() ); block.SetTagLookupTableToPropertyFunctor( GetTagLookupTableToPropertyFunctor() ); block.SetImageFrameList( frameList ); block.SetTiltInformation( tiltInfo ); + block.SetSplitReason(splitReason); block.SetReaderImplementationLevel( this->GetReaderImplementationLevel( block.GetSOPClassUID() ) ); this->SetOutput( o, block ); } timeStop( "Output" ); #if defined( MBILOG_ENABLE_DEBUG ) || defined( ENABLE_TIMING ) std::cout << "---------------------------------------------------------------" << std::endl; timer.Report( std::cout ); std::cout << "---------------------------------------------------------------" << std::endl; #endif } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::DICOMITKSeriesGDCMReader::InternalExecuteSortingStep( unsigned int sortingStepIndex, const DICOMDatasetSorter::Pointer& sorter, const SortingBlockList& input ) { SortingBlockList nextStepSorting; // we should not modify our input list while processing it std::stringstream ss; ss << "Sorting step " << sortingStepIndex << " '"; #if defined( MBILOG_ENABLE_DEBUG ) sorter->PrintConfiguration( ss ); #endif ss << "'"; nextStepSorting.clear(); MITK_DEBUG << "================================================================================"; MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ": " << input.size() << " groups input"; #if defined( MBILOG_ENABLE_DEBUG ) unsigned int groupIndex = 0; #endif for ( auto blockIter = input.cbegin(); blockIter != input.cend(); #if defined( MBILOG_ENABLE_DEBUG ) ++groupIndex, #endif ++blockIter ) { - const DICOMDatasetAccessingImageFrameList& gdcmInfoFrameList = *blockIter; + const auto& [gdcmInfoFrameList, inputSplitReason] = *blockIter; const DICOMDatasetList datasetList = ConvertToDICOMDatasetList( gdcmInfoFrameList ); #if defined( MBILOG_ENABLE_DEBUG ) MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "DICOMITKSeriesGDCMReader: " << ss.str() << ", dataset group " << groupIndex << " (" << datasetList.size() << " datasets): "; for ( auto oi = datasetList.cbegin(); oi != datasetList.cend(); ++oi ) { MITK_DEBUG << " INPUT : " << ( *oi )->GetFilenameIfAvailable(); } #endif sorter->SetInput( datasetList ); sorter->Sort(); unsigned int numberOfResultingBlocks = sorter->GetNumberOfOutputs(); for ( unsigned int b = 0; b < numberOfResultingBlocks; ++b ) { const DICOMDatasetList blockResult = sorter->GetOutput( b ); for ( auto oi = blockResult.cbegin(); oi != blockResult.cend(); ++oi ) { MITK_DEBUG << " OUTPUT(" << b << ") :" << ( *oi )->GetFilenameIfAvailable(); } DICOMDatasetAccessingImageFrameList sortedGdcmInfoFrameList = ConvertToDICOMDatasetAccessingImageFrameList( blockResult ); - nextStepSorting.push_back( sortedGdcmInfoFrameList ); + + nextStepSorting.emplace_back(sortedGdcmInfoFrameList, inputSplitReason->ExtendReason(sorter->GetSplitReason(b)) ); } } return nextStepSorting; } mitk::ReaderImplementationLevel mitk::DICOMITKSeriesGDCMReader::GetReaderImplementationLevel( const std::string sopClassUID ) { if ( sopClassUID.empty() ) { return SOPClassUnknown; } gdcm::UIDs uidKnowledge; uidKnowledge.SetFromUID( sopClassUID.c_str() ); gdcm::UIDs::TSName gdcmType = static_cast((gdcm::UIDs::TSType)uidKnowledge); switch ( gdcmType ) { case gdcm::UIDs::CTImageStorage: case gdcm::UIDs::MRImageStorage: case gdcm::UIDs::PositronEmissionTomographyImageStorage: case gdcm::UIDs::ComputedRadiographyImageStorage: case gdcm::UIDs::DigitalXRayImageStorageForPresentation: case gdcm::UIDs::DigitalXRayImageStorageForProcessing: return SOPClassSupported; case gdcm::UIDs::NuclearMedicineImageStorage: return SOPClassPartlySupported; case gdcm::UIDs::SecondaryCaptureImageStorage: return SOPClassImplemented; default: return SOPClassUnsupported; } } // void AllocateOutputImages(); bool mitk::DICOMITKSeriesGDCMReader::LoadImages() { bool success = true; unsigned int numberOfOutputs = this->GetNumberOfOutputs(); for ( unsigned int o = 0; o < numberOfOutputs; ++o ) { success &= this->LoadMitkImageForOutput( o ); } return success; } bool mitk::DICOMITKSeriesGDCMReader::LoadMitkImageForImageBlockDescriptor( DICOMImageBlockDescriptor& block ) const { PushLocale(); const DICOMImageFrameList& frames = block.GetImageFrameList(); const GantryTiltInformation tiltInfo = block.GetTiltInformation(); bool hasTilt = tiltInfo.IsRegularGantryTilt(); ITKDICOMSeriesReaderHelper::StringContainer filenames; filenames.reserve( frames.size() ); for ( auto frameIter = frames.cbegin(); frameIter != frames.cend(); ++frameIter ) { filenames.push_back( ( *frameIter )->Filename ); } mitk::ITKDICOMSeriesReaderHelper helper; bool success( true ); try { mitk::Image::Pointer mitkImage = helper.Load( filenames, m_FixTiltByShearing && hasTilt, tiltInfo ); block.SetMitkImage( mitkImage ); } catch ( const std::exception& e ) { success = false; MITK_ERROR << "Exception during image loading: " << e.what(); } PopLocale(); return success; } bool mitk::DICOMITKSeriesGDCMReader::LoadMitkImageForOutput( unsigned int o ) { DICOMImageBlockDescriptor& block = this->InternalGetOutput( o ); return this->LoadMitkImageForImageBlockDescriptor( block ); } bool mitk::DICOMITKSeriesGDCMReader::CanHandleFile( const std::string& filename ) { return ITKDICOMSeriesReaderHelper::CanHandleFile( filename ); } void mitk::DICOMITKSeriesGDCMReader::AddSortingElement( DICOMDatasetSorter* sorter, bool atFront ) { assert( sorter ); if ( atFront ) { m_Sorter.push_front( sorter ); } else { m_Sorter.push_back( sorter ); } this->Modified(); } mitk::DICOMITKSeriesGDCMReader::ConstSorterList mitk::DICOMITKSeriesGDCMReader::GetFreelyConfiguredSortingElements() const { std::list result; unsigned int sortIndex( 0 ); for ( auto sorterIter = m_Sorter.begin(); sorterIter != m_Sorter.end(); ++sortIndex, ++sorterIter ) { if ( sortIndex > 0 ) // ignore first element (see EnsureMandatorySortersArePresent) { result.push_back( ( *sorterIter ).GetPointer() ); } } return result; } void mitk::DICOMITKSeriesGDCMReader::EnsureMandatorySortersArePresent( unsigned int decimalPlacesForOrientation, bool simpleVolumeImport ) { DICOMTagBasedSorter::Pointer splitter = DICOMTagBasedSorter::New(); splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0010) ); // Number of Rows splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0011) ); // Number of Columns splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0030) ); // Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x1164) ); // Imager Pixel Spacing splitter->AddDistinguishingTag( DICOMTag(0x0020, 0x0037), new mitk::DICOMTagBasedSorter::CutDecimalPlaces(decimalPlacesForOrientation) ); // Image Orientation (Patient) splitter->AddDistinguishingTag( DICOMTag(0x0018, 0x0050) ); // Slice Thickness if ( simpleVolumeImport ) { MITK_DEBUG << "Simple volume reading: ignoring number of frames"; } else { splitter->AddDistinguishingTag( DICOMTag(0x0028, 0x0008) ); // Number of Frames } this->AddSortingElement( splitter, true ); // true = at front if ( m_EquiDistantBlocksSorter.IsNull() ) { m_EquiDistantBlocksSorter = mitk::EquiDistantBlocksSorter::New(); } m_EquiDistantBlocksSorter->SetAcceptTilt( m_FixTiltByShearing ); if ( m_NormalDirectionConsistencySorter.IsNull() ) { m_NormalDirectionConsistencySorter = mitk::NormalDirectionConsistencySorter::New(); } } void mitk::DICOMITKSeriesGDCMReader::SetToleratedOriginOffsetToAdaptive( double fractionOfInterSliceDistance ) const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); m_EquiDistantBlocksSorter->SetToleratedOriginOffsetToAdaptive( fractionOfInterSliceDistance ); this->Modified(); } void mitk::DICOMITKSeriesGDCMReader::SetToleratedOriginOffset( double millimeters ) const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); m_EquiDistantBlocksSorter->SetToleratedOriginOffset( millimeters ); this->Modified(); } double mitk::DICOMITKSeriesGDCMReader::GetToleratedOriginError() const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); return m_EquiDistantBlocksSorter->GetToleratedOriginOffset(); } bool mitk::DICOMITKSeriesGDCMReader::IsToleratedOriginOffsetAbsolute() const { assert( m_EquiDistantBlocksSorter.IsNotNull() ); return m_EquiDistantBlocksSorter->IsToleratedOriginOffsetAbsolute(); } double mitk::DICOMITKSeriesGDCMReader::GetDecimalPlacesForOrientation() const { return m_DecimalPlacesForOrientation; } mitk::DICOMTagCache::Pointer mitk::DICOMITKSeriesGDCMReader::GetTagCache() const { return m_TagCache; } void mitk::DICOMITKSeriesGDCMReader::SetTagCache( const DICOMTagCache::Pointer& tagCache ) { m_TagCache = tagCache; m_ExternalCache = tagCache.IsNotNull(); } mitk::DICOMTagPathList mitk::DICOMITKSeriesGDCMReader::GetTagsOfInterest() const { DICOMTagPathList completeList; // check all configured sorters for ( auto sorterIter = m_Sorter.cbegin(); sorterIter != m_Sorter.cend(); ++sorterIter ) { assert( sorterIter->IsNotNull() ); const DICOMTagList tags = ( *sorterIter )->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); } // check our own forced sorters DICOMTagList tags = m_EquiDistantBlocksSorter->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); tags = m_NormalDirectionConsistencySorter->GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); // add the tags for DICOMImageBlockDescriptor tags = DICOMImageBlockDescriptor::GetTagsOfInterest(); completeList.insert( completeList.end(), tags.cbegin(), tags.cend() ); const AdditionalTagsMapType tagList = GetAdditionalTagsOfInterest(); for ( auto iter = tagList.cbegin(); iter != tagList.cend(); ++iter ) { completeList.push_back( iter->first ) ; } return completeList; } diff --git a/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp b/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp index 334d8da6fb..de4866f36c 100644 --- a/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp +++ b/Modules/DICOM/src/mitkDICOMImageBlockDescriptor.cpp @@ -1,912 +1,940 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkDICOMImageBlockDescriptor.h" #include "mitkStringProperty.h" #include "mitkLevelWindowProperty.h" #include "mitkPropertyKeyPath.h" #include "mitkDICOMIOMetaInformationPropertyConstants.h" +#include "mitkIOMetaInformationPropertyConstants.h" #include #include #include #include mitk::DICOMImageBlockDescriptor::DICOMImageBlockDescriptor() : m_ReaderImplementationLevel( SOPClassUnknown ) , m_PropertyList( PropertyList::New() ) +, m_SplitReason(IOVolumeSplitReason::New()) , m_TagCache( nullptr ) , m_PropertiesOutOfDate( true ) { m_PropertyFunctor = &mitk::DICOMImageBlockDescriptor::GetPropertyForDICOMValues; } mitk::DICOMImageBlockDescriptor::~DICOMImageBlockDescriptor() { } mitk::DICOMImageBlockDescriptor::DICOMImageBlockDescriptor( const DICOMImageBlockDescriptor& other ) : m_ImageFrameList( other.m_ImageFrameList ) , m_MitkImage( other.m_MitkImage ) , m_SliceIsLoaded( other.m_SliceIsLoaded ) , m_ReaderImplementationLevel( other.m_ReaderImplementationLevel ) , m_TiltInformation( other.m_TiltInformation ) , m_PropertyList( other.m_PropertyList->Clone() ) +, m_SplitReason( other.m_SplitReason->Clone() ) , m_TagCache( other.m_TagCache ) , m_PropertiesOutOfDate( other.m_PropertiesOutOfDate ) , m_AdditionalTagMap(other.m_AdditionalTagMap) , m_FoundAdditionalTags(other.m_FoundAdditionalTags) , m_PropertyFunctor(other.m_PropertyFunctor) { if ( m_MitkImage ) { m_MitkImage = m_MitkImage->Clone(); } } mitk::DICOMImageBlockDescriptor& mitk::DICOMImageBlockDescriptor:: operator=( const DICOMImageBlockDescriptor& other ) { if ( this != &other ) { m_ImageFrameList = other.m_ImageFrameList; m_MitkImage = other.m_MitkImage; m_SliceIsLoaded = other.m_SliceIsLoaded; m_ReaderImplementationLevel = other.m_ReaderImplementationLevel; m_TiltInformation = other.m_TiltInformation; m_AdditionalTagMap = other.m_AdditionalTagMap; m_FoundAdditionalTags = other.m_FoundAdditionalTags; m_PropertyFunctor = other.m_PropertyFunctor; if ( other.m_PropertyList ) { m_PropertyList = other.m_PropertyList->Clone(); } + if (other.m_SplitReason) + { + m_SplitReason = other.m_SplitReason->Clone(); + } + if ( other.m_MitkImage ) { m_MitkImage = other.m_MitkImage->Clone(); } m_TagCache = other.m_TagCache; m_PropertiesOutOfDate = other.m_PropertiesOutOfDate; } return *this; } mitk::DICOMTagList mitk::DICOMImageBlockDescriptor::GetTagsOfInterest() { DICOMTagList completeList; completeList.push_back( DICOMTag( 0x0018, 0x1164 ) ); // pixel spacing completeList.push_back( DICOMTag( 0x0028, 0x0030 ) ); // imager pixel spacing completeList.push_back( DICOMTag( 0x0008, 0x0018 ) ); // sop instance UID completeList.push_back( DICOMTag( 0x0008, 0x0016 ) ); // sop class UID completeList.push_back( DICOMTag( 0x0020, 0x0011 ) ); // series number completeList.push_back( DICOMTag( 0x0008, 0x1030 ) ); // study description completeList.push_back( DICOMTag( 0x0008, 0x103e ) ); // series description completeList.push_back( DICOMTag( 0x0008, 0x0060 ) ); // modality completeList.push_back( DICOMTag( 0x0018, 0x0024 ) ); // sequence name completeList.push_back( DICOMTag( 0x0020, 0x0037 ) ); // image orientation completeList.push_back( DICOMTag( 0x0020, 0x1041 ) ); // slice location completeList.push_back( DICOMTag( 0x0020, 0x0012 ) ); // acquisition number completeList.push_back( DICOMTag( 0x0020, 0x0013 ) ); // instance number completeList.push_back( DICOMTag( 0x0020, 0x0032 ) ); // image position patient completeList.push_back( DICOMTag( 0x0028, 0x1050 ) ); // window center completeList.push_back( DICOMTag( 0x0028, 0x1051 ) ); // window width completeList.push_back( DICOMTag( 0x0008, 0x0008 ) ); // image type completeList.push_back( DICOMTag( 0x0028, 0x0004 ) ); // photometric interpretation return completeList; } void mitk::DICOMImageBlockDescriptor::SetAdditionalTagsOfInterest( const AdditionalTagsMapType& tagMap) { m_AdditionalTagMap = tagMap; } void mitk::DICOMImageBlockDescriptor::SetTiltInformation( const GantryTiltInformation& info ) { m_TiltInformation = info; } const mitk::GantryTiltInformation mitk::DICOMImageBlockDescriptor::GetTiltInformation() const { return m_TiltInformation; } void mitk::DICOMImageBlockDescriptor::SetImageFrameList( const DICOMImageFrameList& framelist ) { m_ImageFrameList = framelist; m_SliceIsLoaded.resize( framelist.size() ); m_SliceIsLoaded.assign( framelist.size(), false ); m_PropertiesOutOfDate = true; } const mitk::DICOMImageFrameList& mitk::DICOMImageBlockDescriptor::GetImageFrameList() const { return m_ImageFrameList; } void mitk::DICOMImageBlockDescriptor::SetMitkImage( Image::Pointer image ) { if ( m_MitkImage != image ) { if ( m_TagCache.IsExpired() ) { MITK_ERROR << "Unable to describe MITK image with properties without a tag-cache object!"; m_MitkImage = nullptr; return; } if ( m_ImageFrameList.empty() ) { MITK_ERROR << "Unable to describe MITK image with properties without a frame list!"; m_MitkImage = nullptr; return; } // Should verify that the image matches m_ImageFrameList and m_TagCache // however, this is hard to do without re-analyzing all // TODO we should at least make sure that the number of frames is identical (plus rows/columns, // orientation) // without gantry tilt correction, we can also check image origin m_MitkImage = this->DescribeImageWithProperties( this->FixupSpacing( image ) ); } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::GetMitkImage() const { return m_MitkImage; } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::FixupSpacing( Image* mitkImage ) { if ( mitkImage ) { Vector3D imageSpacing = mitkImage->GetGeometry()->GetSpacing(); ScalarType desiredSpacingX = imageSpacing[0]; ScalarType desiredSpacingY = imageSpacing[1]; this->GetDesiredMITKImagePixelSpacing( desiredSpacingX, desiredSpacingY ); // prefer pixel spacing over imager pixel spacing if ( desiredSpacingX <= 0 || desiredSpacingY <= 0 ) { return mitkImage; } MITK_DEBUG << "Loaded image with spacing " << imageSpacing[0] << ", " << imageSpacing[1]; MITK_DEBUG << "Found correct spacing info " << desiredSpacingX << ", " << desiredSpacingY; imageSpacing[0] = desiredSpacingX; imageSpacing[1] = desiredSpacingY; mitkImage->GetGeometry()->SetSpacing( imageSpacing ); } return mitkImage; } void mitk::DICOMImageBlockDescriptor::SetSliceIsLoaded( unsigned int index, bool isLoaded ) { if ( index < m_SliceIsLoaded.size() ) { m_SliceIsLoaded[index] = isLoaded; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor::IsSliceLoaded( unsigned int index ) const { if ( index < m_SliceIsLoaded.size() ) { return m_SliceIsLoaded[index]; } else { std::stringstream ss; ss << "Index " << index << " out of range (" << m_SliceIsLoaded.size() << " indices reserved)"; throw std::invalid_argument( ss.str() ); } } bool mitk::DICOMImageBlockDescriptor::AllSlicesAreLoaded() const { bool allLoaded = true; for ( auto iter = m_SliceIsLoaded.cbegin(); iter != m_SliceIsLoaded.cend(); ++iter ) { allLoaded &= *iter; } return allLoaded; } /* PS defined IPS defined PS==IPS 0 0 --> UNKNOWN spacing, loader will invent 0 1 --> spacing as at detector surface 1 0 --> spacing as in patient 1 1 0 --> detector surface spacing CORRECTED for geometrical magnifications: spacing as in patient 1 1 1 --> detector surface spacing NOT corrected for geometrical magnifications: spacing as at detector */ mitk::PixelSpacingInterpretation mitk::DICOMImageBlockDescriptor::GetPixelSpacingInterpretation() const { if ( m_ImageFrameList.empty() || m_TagCache.IsExpired() ) { MITK_ERROR << "Invalid call to GetPixelSpacingInterpretation. Need to have initialized tag-cache!"; return SpacingUnknown; } const std::string pixelSpacing = this->GetPixelSpacing(); const std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); if ( pixelSpacing.empty() ) { if ( imagerPixelSpacing.empty() ) { return SpacingUnknown; } else { return SpacingAtDetector; } } else // Pixel Spacing defined { if ( imagerPixelSpacing.empty() ) { return SpacingInPatient; } else if ( pixelSpacing != imagerPixelSpacing ) { return SpacingInPatient; } else { return SpacingAtDetector; } } } std::string mitk::DICOMImageBlockDescriptor::GetPixelSpacing() const { auto tagCache = m_TagCache.Lock(); if ( m_ImageFrameList.empty() || tagCache.IsNull() ) { MITK_ERROR << "Invalid call to GetPixelSpacing. Need to have initialized tag-cache!"; return std::string( "" ); } static const DICOMTag tagPixelSpacing( 0x0028, 0x0030 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagPixelSpacing ).value; } std::string mitk::DICOMImageBlockDescriptor::GetImagerPixelSpacing() const { auto tagCache = m_TagCache.Lock(); if ( m_ImageFrameList.empty() || tagCache.IsNull() ) { MITK_ERROR << "Invalid call to GetImagerPixelSpacing. Need to have initialized tag-cache!"; return std::string( "" ); } static const DICOMTag tagImagerPixelSpacing( 0x0018, 0x1164 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagImagerPixelSpacing ).value; } void mitk::DICOMImageBlockDescriptor::GetDesiredMITKImagePixelSpacing( ScalarType& spacingX, ScalarType& spacingY ) const { const std::string pixelSpacing = this->GetPixelSpacing(); // preference for "in patient" pixel spacing if ( !DICOMStringToSpacing( pixelSpacing, spacingX, spacingY ) ) { const std::string imagerPixelSpacing = this->GetImagerPixelSpacing(); // fallback to "on detector" spacing if ( !DICOMStringToSpacing( imagerPixelSpacing, spacingX, spacingY ) ) { // at this point we have no hints whether the spacing is correct // do a quick sanity check and either trust in the input or set both to 1 // We assume neither spacing to be negative, zero or unexpectedly large for // medical images if (spacingX < mitk::eps || spacingX > 1000 || spacingY < mitk::eps || spacingY > 1000) { spacingX = spacingY = 1.0; } } } } void mitk::DICOMImageBlockDescriptor::SetProperty( const std::string& key, BaseProperty* value ) { m_PropertyList->SetProperty( key, value ); } mitk::BaseProperty* mitk::DICOMImageBlockDescriptor::GetProperty( const std::string& key ) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetProperty( key ); } std::string mitk::DICOMImageBlockDescriptor::GetPropertyAsString( const std::string& key ) const { this->UpdateImageDescribingProperties(); const mitk::BaseProperty::Pointer property = m_PropertyList->GetProperty( key ); if ( property.IsNotNull() ) { return property->GetValueAsString(); } else { return std::string( "" ); } } +mitk::IOVolumeSplitReason::ConstPointer mitk::DICOMImageBlockDescriptor::GetSplitReason() const +{ + return m_SplitReason; +} + +mitk::IOVolumeSplitReason::Pointer mitk::DICOMImageBlockDescriptor::GetSplitReason() +{ + return m_SplitReason; +} + +void mitk::DICOMImageBlockDescriptor::SetSplitReason(IOVolumeSplitReason::Pointer reason) +{ + m_SplitReason = reason; +} + void mitk::DICOMImageBlockDescriptor::SetFlag( const std::string& key, bool value ) { m_PropertyList->ReplaceProperty( key, BoolProperty::New( value ) ); } bool mitk::DICOMImageBlockDescriptor::GetFlag( const std::string& key, bool defaultValue ) const { this->UpdateImageDescribingProperties(); BoolProperty::ConstPointer boolProp = dynamic_cast( this->GetProperty( key ) ); if ( boolProp.IsNotNull() ) { return boolProp->GetValue(); } else { return defaultValue; } } void mitk::DICOMImageBlockDescriptor::SetIntProperty( const std::string& key, int value ) { m_PropertyList->ReplaceProperty( key, IntProperty::New( value ) ); } int mitk::DICOMImageBlockDescriptor::GetIntProperty( const std::string& key, int defaultValue ) const { this->UpdateImageDescribingProperties(); IntProperty::ConstPointer intProp = dynamic_cast( this->GetProperty( key ) ); if ( intProp.IsNotNull() ) { return intProp->GetValue(); } else { return defaultValue; } } double mitk::DICOMImageBlockDescriptor::stringtodouble( const std::string& str ) const { double d; std::string trimmedstring( str ); try { trimmedstring = trimmedstring.erase( trimmedstring.find_last_not_of( " \n\r\t" ) + 1 ); } catch ( ... ) { // no last not of } std::string firstcomponent( trimmedstring ); try { firstcomponent = trimmedstring.erase( trimmedstring.find_first_of( "\\" ) ); } catch ( ... ) { // no last not of } std::istringstream converter( firstcomponent ); if ( !firstcomponent.empty() && ( converter >> d ) && converter.eof() ) { return d; } else { throw std::invalid_argument( "Argument is not a convertible number" ); } } mitk::Image::Pointer mitk::DICOMImageBlockDescriptor::DescribeImageWithProperties( Image* mitkImage ) { // TODO: this is a collection of properties that have been provided by the // legacy DicomSeriesReader. // We should at some point clean up this collection and name them in a more // consistent way! if ( !mitkImage ) return mitkImage; mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_FILES()), this->GetProperty("filenamesForSlices")); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION_STRING()), StringProperty::New(PixelSpacingInterpretationToString(this->GetPixelSpacingInterpretation()))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION()), GenericProperty::New(this->GetPixelSpacingInterpretation())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL_STRING()), StringProperty::New(ReaderImplementationLevelToString(m_ReaderImplementationLevel))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL()), GenericProperty::New(m_ReaderImplementationLevel)); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_GANTRY_TILT_CORRECTED()), BoolProperty::New(this->GetTiltInformation().IsRegularGantryTilt())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_3D_plus_t()), BoolProperty::New(this->GetFlag("3D+t", false))); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_GDCM()), StringProperty::New(gdcm::Version::GetVersion())); mitkImage->SetProperty(PropertyKeyPathToPropertyName(DICOMIOMetaInformationPropertyConstants::READER_DCMTK()), StringProperty::New(PACKAGE_VERSION)); + if (nullptr != m_SplitReason && m_SplitReason->HasReasons()) + { + mitkImage->SetProperty(PropertyKeyPathToPropertyName(IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON()), StringProperty::New(IOVolumeSplitReason::ToJSON(m_SplitReason).dump())); + } + // get all found additional tags of interest for (const auto &tag : m_FoundAdditionalTags) { BaseProperty* prop = this->GetProperty(tag); if (prop) { mitkImage->SetProperty(tag.c_str(), prop); } } ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// //// Deprecated properties should be removed sooner then later (see above) ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // first part: add some tags that describe individual slices // these properties are defined at analysis time (see UpdateImageDescribingProperties()) const char* propertyKeySliceLocation = "dicom.image.0020.1041"; const char* propertyKeyInstanceNumber = "dicom.image.0020.0013"; const char* propertyKeySOPInstanceUID = "dicom.image.0008.0018"; mitkImage->SetProperty( propertyKeySliceLocation, this->GetProperty( "sliceLocationForSlices" ) ); mitkImage->SetProperty( propertyKeyInstanceNumber, this->GetProperty( "instanceNumberForSlices" ) ); mitkImage->SetProperty( propertyKeySOPInstanceUID, this->GetProperty( "SOPInstanceUIDForSlices" ) ); mitkImage->SetProperty( "files", this->GetProperty( "filenamesForSlices_deprecated" ) ); // second part: add properties that describe the whole image block mitkImage->SetProperty( "dicomseriesreader.SOPClassUID", StringProperty::New( this->GetSOPClassUID() ) ); mitkImage->SetProperty( "dicomseriesreader.SOPClass", StringProperty::New( this->GetSOPClassUIDAsName() ) ); mitkImage->SetProperty( "dicomseriesreader.PixelSpacingInterpretationString", StringProperty::New( PixelSpacingInterpretationToString( this->GetPixelSpacingInterpretation() ) ) ); mitkImage->SetProperty( "dicomseriesreader.PixelSpacingInterpretation", GenericProperty::New( this->GetPixelSpacingInterpretation() ) ); mitkImage->SetProperty( "dicomseriesreader.ReaderImplementationLevelString", StringProperty::New( ReaderImplementationLevelToString( m_ReaderImplementationLevel ) ) ); mitkImage->SetProperty( "dicomseriesreader.ReaderImplementationLevel", GenericProperty::New( m_ReaderImplementationLevel ) ); mitkImage->SetProperty( "dicomseriesreader.GantyTiltCorrected", BoolProperty::New( this->GetTiltInformation().IsRegularGantryTilt() ) ); mitkImage->SetProperty( "dicomseriesreader.3D+t", BoolProperty::New( this->GetFlag( "3D+t", false ) ) ); // level window const std::string windowCenter = this->GetPropertyAsString( "windowCenter" ); const std::string windowWidth = this->GetPropertyAsString( "windowWidth" ); try { const double level = stringtodouble( windowCenter ); const double window = stringtodouble( windowWidth ); mitkImage->SetProperty( "levelwindow", LevelWindowProperty::New( LevelWindow( level, window ) ) ); } catch ( ... ) { // nothing, no levelwindow to be predicted... } const std::string modality = this->GetPropertyAsString( "modality" ); mitkImage->SetProperty( "modality", StringProperty::New( modality ) ); mitkImage->SetProperty( "dicom.pixel.PhotometricInterpretation", this->GetProperty( "photometricInterpretation" ) ); mitkImage->SetProperty( "dicom.image.imagetype", this->GetProperty( "imagetype" ) ); mitkImage->SetProperty( "dicom.study.StudyDescription", this->GetProperty( "studyDescription" ) ); mitkImage->SetProperty( "dicom.series.SeriesDescription", this->GetProperty( "seriesDescription" ) ); mitkImage->SetProperty( "dicom.pixel.Rows", this->GetProperty( "rows" ) ); mitkImage->SetProperty( "dicom.pixel.Columns", this->GetProperty( "columns" ) ); // fourth part: get something from ImageIO. BUT this needs to be created elsewhere. or not at all! return mitkImage; } void mitk::DICOMImageBlockDescriptor::SetReaderImplementationLevel( const ReaderImplementationLevel& level ) { m_ReaderImplementationLevel = level; } mitk::ReaderImplementationLevel mitk::DICOMImageBlockDescriptor::GetReaderImplementationLevel() const { return m_ReaderImplementationLevel; } std::string mitk::DICOMImageBlockDescriptor::GetSOPClassUID() const { auto tagCache = m_TagCache.Lock(); if ( !m_ImageFrameList.empty() && tagCache.IsNotNull() ) { static const DICOMTag tagSOPClassUID( 0x0008, 0x0016 ); return tagCache->GetTagValue( m_ImageFrameList.front(), tagSOPClassUID ).value; } else { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUID(). Need to have initialized tag-cache!"; return std::string( "" ); } } std::string mitk::DICOMImageBlockDescriptor::GetSOPClassUIDAsName() const { if ( !m_ImageFrameList.empty() && !m_TagCache.IsExpired() ) { gdcm::UIDs uidKnowledge; uidKnowledge.SetFromUID( this->GetSOPClassUID().c_str() ); const char* name = uidKnowledge.GetName(); if ( name ) { return std::string( name ); } else { return std::string( "" ); } } else { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::GetSOPClassUIDAsName(). Need to have " "initialized tag-cache!"; return std::string( "" ); } } int mitk::DICOMImageBlockDescriptor::GetNumberOfTimeSteps() const { int result = 1; this->m_PropertyList->GetIntProperty("timesteps", result); return result; }; int mitk::DICOMImageBlockDescriptor::GetNumberOfFramesPerTimeStep() const { const int numberOfTimesteps = this->GetNumberOfTimeSteps(); int numberOfFramesPerTimestep = this->m_ImageFrameList.size() / numberOfTimesteps; assert(int(double((double)this->m_ImageFrameList.size() / (double)numberOfTimesteps)) == numberOfFramesPerTimestep); // this should hold return numberOfFramesPerTimestep; }; void mitk::DICOMImageBlockDescriptor::SetTagCache( DICOMTagCache* privateCache ) { // this must only be used during loading and never afterwards m_TagCache = privateCache; } #define printPropertyRange( label, property_name ) \ \ { \ const std::string first = this->GetPropertyAsString( #property_name "First" ); \ const std::string last = this->GetPropertyAsString( #property_name "Last" ); \ if ( !first.empty() || !last.empty() ) \ { \ if ( first == last ) \ { \ os << " " label ": '" << first << "'" << std::endl; \ } \ else \ { \ os << " " label ": '" << first << "' - '" << last << "'" << std::endl; \ } \ } \ \ } #define printProperty( label, property_name ) \ \ { \ const std::string first = this->GetPropertyAsString( #property_name ); \ if ( !first.empty() ) \ { \ os << " " label ": '" << first << "'" << std::endl; \ } \ \ } #define printBool( label, commands ) \ \ { \ os << " " label ": '" << ( commands ? "yes" : "no" ) << "'" << std::endl; \ \ } void mitk::DICOMImageBlockDescriptor::Print(std::ostream& os, bool filenameDetails) const { os << " Number of Frames: '" << m_ImageFrameList.size() << "'" << std::endl; os << " SOP class: '" << this->GetSOPClassUIDAsName() << "'" << std::endl; printProperty( "Series Number", seriesNumber ); printProperty( "Study Description", studyDescription ); printProperty( "Series Description", seriesDescription ); printProperty( "Modality", modality ); printProperty( "Sequence Name", sequenceName ); printPropertyRange( "Slice Location", sliceLocation ); printPropertyRange( "Acquisition Number", acquisitionNumber ); printPropertyRange( "Instance Number", instanceNumber ); printPropertyRange( "Image Position", imagePositionPatient ); printProperty( "Image Orientation", orientation ); os << " Pixel spacing interpretation: '" << PixelSpacingInterpretationToString( this->GetPixelSpacingInterpretation() ) << "'" << std::endl; printBool( "Gantry Tilt", this->GetTiltInformation().IsRegularGantryTilt() ) // printBool("3D+t", this->GetFlag("3D+t",false)) // os << " MITK image loaded: '" << (this->GetMitkImage().IsNotNull() ? "yes" : "no") << "'" << // std::endl; if ( filenameDetails ) { os << " Files in this image block:" << std::endl; for ( auto frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++frameIter ) { os << " " << ( *frameIter )->Filename; if ( ( *frameIter )->FrameNo > 0 ) { os << ", " << ( *frameIter )->FrameNo; } os << std::endl; } } } #define storeTagValueToProperty( tag_name, tag_g, tag_e ) \ \ { \ const DICOMTag t( tag_g, tag_e ); \ const std::string tagValue = tagCache->GetTagValue( firstFrame, t ).value; \ const_cast( this ) \ ->SetProperty( #tag_name, StringProperty::New( tagValue ) ); \ \ } #define storeTagValueRangeToProperty( tag_name, tag_g, tag_e ) \ \ { \ const DICOMTag t( tag_g, tag_e ); \ const std::string tagValueFirst = tagCache->GetTagValue( firstFrame, t ).value; \ const std::string tagValueLast = tagCache->GetTagValue( lastFrame, t ).value; \ const_cast( this ) \ ->SetProperty( #tag_name "First", StringProperty::New( tagValueFirst ) ); \ const_cast( this ) \ ->SetProperty( #tag_name "Last", StringProperty::New( tagValueLast ) ); \ \ } void mitk::DICOMImageBlockDescriptor::UpdateImageDescribingProperties() const { if ( !m_PropertiesOutOfDate ) return; if ( !m_ImageFrameList.empty() ) { auto tagCache = m_TagCache.Lock(); if (tagCache.IsNull()) { MITK_ERROR << "Invalid call to DICOMImageBlockDescriptor::UpdateImageDescribingProperties(). Need to " "have initialized tag-cache!"; return; } const DICOMImageFrameInfo::Pointer firstFrame = m_ImageFrameList.front(); const DICOMImageFrameInfo::Pointer lastFrame = m_ImageFrameList.back(); // see macros above storeTagValueToProperty( seriesNumber, 0x0020, 0x0011 ); storeTagValueToProperty( studyDescription, 0x0008, 0x1030 ); storeTagValueToProperty( seriesDescription, 0x0008, 0x103e ); storeTagValueToProperty( modality, 0x0008, 0x0060 ); storeTagValueToProperty( sequenceName, 0x0018, 0x0024 ); storeTagValueToProperty( orientation, 0x0020, 0x0037 ); storeTagValueToProperty( rows, 0x0028, 0x0010 ); storeTagValueToProperty( columns, 0x0028, 0x0011 ); storeTagValueRangeToProperty( sliceLocation, 0x0020, 0x1041 ); storeTagValueRangeToProperty( acquisitionNumber, 0x0020, 0x0012 ); storeTagValueRangeToProperty( instanceNumber, 0x0020, 0x0013 ); storeTagValueRangeToProperty( imagePositionPatient, 0x0020, 0x0032 ); storeTagValueToProperty( windowCenter, 0x0028, 0x1050 ); storeTagValueToProperty( windowWidth, 0x0028, 0x1051 ); storeTagValueToProperty( imageType, 0x0008, 0x0008 ); storeTagValueToProperty( photometricInterpretation, 0x0028, 0x0004 ); // some per-image attributes // frames are just numbered starting from 0. timestep 1 (the second time-step) has frames starting at // (number-of-frames-per-timestep) // std::string propertyKeySliceLocation = "dicom.image.0020.1041"; // std::string propertyKeyInstanceNumber = "dicom.image.0020.0013"; // std::string propertyKeySOPInstanceNumber = "dicom.image.0008.0018"; StringLookupTable sliceLocationForSlices; StringLookupTable instanceNumberForSlices; StringLookupTable SOPInstanceUIDForSlices; StringLookupTable filenamesForSlices_deprecated; DICOMCachedValueLookupTable filenamesForSlices; const DICOMTag tagSliceLocation( 0x0020, 0x1041 ); const DICOMTag tagInstanceNumber( 0x0020, 0x0013 ); const DICOMTag tagSOPInstanceNumber( 0x0008, 0x0018 ); std::unordered_map additionalTagResultList; unsigned int slice(0); int timePoint(-1); const int framesPerTimeStep = this->GetNumberOfFramesPerTimeStep(); for ( auto frameIter = m_ImageFrameList.begin(); frameIter != m_ImageFrameList.end(); ++slice, ++frameIter ) { unsigned int zSlice = slice%framesPerTimeStep; if ( zSlice == 0) { timePoint++; } const std::string sliceLocation = tagCache->GetTagValue( *frameIter, tagSliceLocation ).value; sliceLocationForSlices.SetTableValue( slice, sliceLocation ); const std::string instanceNumber = tagCache->GetTagValue( *frameIter, tagInstanceNumber ).value; instanceNumberForSlices.SetTableValue( slice, instanceNumber ); const std::string sopInstanceUID = tagCache->GetTagValue( *frameIter, tagSOPInstanceNumber ).value; SOPInstanceUIDForSlices.SetTableValue( slice, sopInstanceUID ); const std::string filename = ( *frameIter )->Filename; filenamesForSlices_deprecated.SetTableValue( slice, filename ); filenamesForSlices.SetTableValue(slice, { static_cast(timePoint), zSlice, filename }); MITK_DEBUG << "Tag info for slice " << slice << ": SL '" << sliceLocation << "' IN '" << instanceNumber << "' SOP instance UID '" << sopInstanceUID << "'"; for (const auto& tag : m_AdditionalTagMap) { const DICOMTagCache::FindingsListType findings = tagCache->GetTagValue( *frameIter, tag.first ); for (const auto& finding : findings) { if (finding.isValid) { std::string propKey = (tag.second.empty()) ? DICOMTagPathToPropertyName(finding.path) : tag.second; DICOMCachedValueInfo info{ static_cast(timePoint), zSlice, finding.value }; additionalTagResultList[propKey].SetTableValue(slice, info); } } } } // add property or properties with proper names auto* thisInstance = const_cast( this ); thisInstance->SetProperty( "sliceLocationForSlices", StringLookupTableProperty::New( sliceLocationForSlices ) ); thisInstance->SetProperty( "instanceNumberForSlices", StringLookupTableProperty::New( instanceNumberForSlices ) ); thisInstance->SetProperty( "SOPInstanceUIDForSlices", StringLookupTableProperty::New( SOPInstanceUIDForSlices ) ); thisInstance->SetProperty( "filenamesForSlices_deprecated", StringLookupTableProperty::New( filenamesForSlices_deprecated ) ); thisInstance->SetProperty("filenamesForSlices", m_PropertyFunctor(filenamesForSlices)); //add properties for additional tags of interest for ( auto iter = additionalTagResultList.cbegin(); iter != additionalTagResultList.cend(); ++iter ) { thisInstance->SetProperty( iter->first, m_PropertyFunctor( iter->second ) ); thisInstance->m_FoundAdditionalTags.insert(m_FoundAdditionalTags.cend(),iter->first); } m_PropertiesOutOfDate = false; } } mitk::BaseProperty::Pointer mitk::DICOMImageBlockDescriptor::GetPropertyForDICOMValues(const DICOMCachedValueLookupTable& cacheLookupTable) { const auto& lookupTable = cacheLookupTable.GetLookupTable(); typedef std::pair PairType; if ( std::adjacent_find( lookupTable.cbegin(), lookupTable.cend(), []( const PairType& lhs, const PairType& rhs ) { return lhs.second.Value != rhs.second.Value; } ) == lookupTable.cend() ) { return static_cast( mitk::StringProperty::New(cacheLookupTable.GetTableValue(0).Value).GetPointer()); } StringLookupTable stringTable; for (const auto &element : lookupTable) { stringTable.SetTableValue(element.first, element.second.Value); } return static_cast( mitk::StringLookupTableProperty::New(stringTable).GetPointer()); } void mitk::DICOMImageBlockDescriptor::SetTagLookupTableToPropertyFunctor( TagLookupTableToPropertyFunctor functor ) { if ( functor != nullptr ) { m_PropertyFunctor = functor; } } mitk::BaseProperty::ConstPointer mitk::DICOMImageBlockDescriptor::GetConstProperty(const std::string &propertyKey, const std::string &/*contextName*/, bool /*fallBackOnDefaultContext*/) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetConstProperty(propertyKey); }; std::vector mitk::DICOMImageBlockDescriptor::GetPropertyKeys(const std::string &/*contextName*/, bool /*includeDefaultContext*/) const { this->UpdateImageDescribingProperties(); return m_PropertyList->GetPropertyKeys(); }; std::vector mitk::DICOMImageBlockDescriptor::GetPropertyContextNames() const { return std::vector(); }; diff --git a/Modules/DICOM/src/mitkDICOMTagBasedSorter.cpp b/Modules/DICOM/src/mitkDICOMTagBasedSorter.cpp index fc354a7bd6..7c8977956b 100644 --- a/Modules/DICOM/src/mitkDICOMTagBasedSorter.cpp +++ b/Modules/DICOM/src/mitkDICOMTagBasedSorter.cpp @@ -1,601 +1,633 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkDICOMTagBasedSorter.h" #include #include mitk::DICOMTagBasedSorter::CutDecimalPlaces ::CutDecimalPlaces(unsigned int precision) :m_Precision(precision) { } mitk::DICOMTagBasedSorter::CutDecimalPlaces ::CutDecimalPlaces(const CutDecimalPlaces& other) :m_Precision(other.m_Precision) { } std::string mitk::DICOMTagBasedSorter::CutDecimalPlaces ::operator()(const std::string& input) const { // be a bit tolerant for tags such as image orientation orientation, let only the first few digits matter (https://phabricator.mitk.org/T12263) // iterate all fields, convert each to a number, cut this number as configured, then return a concatenated string with all cut-off numbers std::ostringstream resultString; resultString.str(std::string()); resultString.clear(); resultString.setf(std::ios::fixed, std::ios::floatfield); resultString.precision(m_Precision); std::stringstream ss(input); ss.str(input); ss.clear(); std::string item; double number(0); std::istringstream converter(item); while (std::getline(ss, item, '\\')) { converter.str(item); converter.clear(); if (converter >> number && converter.eof()) { // converted to double resultString << number; } else { // did not convert to double resultString << item; // just paste the unmodified string } if (!ss.eof()) { resultString << "\\"; } } return resultString.str(); } mitk::DICOMTagBasedSorter::TagValueProcessor* mitk::DICOMTagBasedSorter::CutDecimalPlaces ::Clone() const { return new CutDecimalPlaces(*this); } unsigned int mitk::DICOMTagBasedSorter::CutDecimalPlaces ::GetPrecision() const { return m_Precision; } mitk::DICOMTagBasedSorter ::DICOMTagBasedSorter() :DICOMDatasetSorter() ,m_StrictSorting(m_DefaultStrictSorting) ,m_ExpectDistanceOne(m_DefaultExpectDistanceOne) { } mitk::DICOMTagBasedSorter ::~DICOMTagBasedSorter() { for(auto ti = m_TagValueProcessor.cbegin(); ti != m_TagValueProcessor.cend(); ++ti) { delete ti->second; } } mitk::DICOMTagBasedSorter ::DICOMTagBasedSorter(const DICOMTagBasedSorter& other ) :DICOMDatasetSorter(other) ,m_DistinguishingTags( other.m_DistinguishingTags ) ,m_SortCriterion( other.m_SortCriterion ) ,m_StrictSorting( other.m_StrictSorting ) ,m_ExpectDistanceOne( other.m_ExpectDistanceOne ) { for(auto ti = other.m_TagValueProcessor.cbegin(); ti != other.m_TagValueProcessor.cend(); ++ti) { m_TagValueProcessor[ti->first] = ti->second->Clone(); } } mitk::DICOMTagBasedSorter& mitk::DICOMTagBasedSorter ::operator=(const DICOMTagBasedSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); m_DistinguishingTags = other.m_DistinguishingTags; m_SortCriterion = other.m_SortCriterion; m_StrictSorting = other.m_StrictSorting; m_ExpectDistanceOne = other.m_ExpectDistanceOne; for(auto ti = other.m_TagValueProcessor.cbegin(); ti != other.m_TagValueProcessor.cend(); ++ti) { m_TagValueProcessor[ti->first] = ti->second->Clone(); } } return *this; } bool mitk::DICOMTagBasedSorter ::operator==(const DICOMDatasetSorter& other) const { if (const auto* otherSelf = dynamic_cast(&other)) { if (this->m_StrictSorting != otherSelf->m_StrictSorting) return false; if (this->m_ExpectDistanceOne != otherSelf->m_ExpectDistanceOne) return false; bool allTagsPresentAndEqual(true); if (this->m_DistinguishingTags.size() != otherSelf->m_DistinguishingTags.size()) return false; for (auto myTag = this->m_DistinguishingTags.cbegin(); myTag != this->m_DistinguishingTags.cend(); ++myTag) { allTagsPresentAndEqual &= (std::find( otherSelf->m_DistinguishingTags.cbegin(), otherSelf->m_DistinguishingTags.cend(), *myTag ) != otherSelf->m_DistinguishingTags.cend()); // other contains this tags // since size is equal, we don't need to check the inverse } if (!allTagsPresentAndEqual) return false; if (this->m_SortCriterion.IsNotNull() && otherSelf->m_SortCriterion.IsNotNull()) { return *(this->m_SortCriterion) == *(otherSelf->m_SortCriterion); } else { return this->m_SortCriterion.IsNull() && otherSelf->m_SortCriterion.IsNull(); } } else { return false; } } void mitk::DICOMTagBasedSorter ::PrintConfiguration(std::ostream& os, const std::string& indent) const { os << indent << "Tag based sorting " << "(strict=" << (m_StrictSorting?"true":"false") << ", expectDistanceOne=" << (m_ExpectDistanceOne?"true":"false") << "):" << std::endl; for (auto tagIter = m_DistinguishingTags.begin(); tagIter != m_DistinguishingTags.end(); ++tagIter) { os << indent << " Split on "; tagIter->Print(os); os << std::endl; } DICOMSortCriterion::ConstPointer crit = m_SortCriterion.GetPointer(); while (crit.IsNotNull()) { os << indent << " Sort by "; crit->Print(os); os << std::endl; crit = crit->GetSecondaryCriterion(); } } void mitk::DICOMTagBasedSorter ::SetStrictSorting(bool strict) { m_StrictSorting = strict; } bool mitk::DICOMTagBasedSorter ::GetStrictSorting() const { return m_StrictSorting; } void mitk::DICOMTagBasedSorter ::SetExpectDistanceOne(bool strict) { m_ExpectDistanceOne = strict; } bool mitk::DICOMTagBasedSorter ::GetExpectDistanceOne() const { return m_ExpectDistanceOne; } mitk::DICOMTagList mitk::DICOMTagBasedSorter ::GetTagsOfInterest() { DICOMTagList allTags = m_DistinguishingTags; if (m_SortCriterion.IsNotNull()) { const DICOMTagList sortingRelevantTags = m_SortCriterion->GetAllTagsOfInterest(); allTags.insert( allTags.end(), sortingRelevantTags.cbegin(), sortingRelevantTags.cend() ); // append } return allTags; } mitk::DICOMTagList mitk::DICOMTagBasedSorter ::GetDistinguishingTags() const { return m_DistinguishingTags; } const mitk::DICOMTagBasedSorter::TagValueProcessor* mitk::DICOMTagBasedSorter ::GetTagValueProcessorForDistinguishingTag(const DICOMTag& tag) const { auto loc = m_TagValueProcessor.find(tag); if (loc != m_TagValueProcessor.cend()) { return loc->second; } else { return nullptr; } } void mitk::DICOMTagBasedSorter ::AddDistinguishingTag( const DICOMTag& tag, TagValueProcessor* tagValueProcessor ) { m_DistinguishingTags.push_back(tag); m_TagValueProcessor[tag] = tagValueProcessor; } void mitk::DICOMTagBasedSorter ::SetSortCriterion( DICOMSortCriterion::ConstPointer criterion ) { m_SortCriterion = criterion; } mitk::DICOMSortCriterion::ConstPointer mitk::DICOMTagBasedSorter ::GetSortCriterion() const { return m_SortCriterion; } void mitk::DICOMTagBasedSorter ::Sort() { + SplitReasonListType splitReasons; + // 1. split - // 2. sort each group - GroupIDToListType groups = this->SplitInputGroups(); - GroupIDToListType& sortedGroups = this->SortGroups( groups ); + GroupIDToListType groups = this->SplitInputGroups(splitReasons); + + // 2. sort each group (can also lead to a split due to distance) + GroupIDToListType& sortedGroups = this->SortGroups( groups, splitReasons); // 3. define output this->SetNumberOfOutputs(sortedGroups.size()); unsigned int outputIndex(0); for (auto groupIter = sortedGroups.cbegin(); groupIter != sortedGroups.cend(); ++outputIndex, ++groupIter) { - this->SetOutput(outputIndex, groupIter->second); + this->SetOutput(outputIndex, groupIter->second, splitReasons[groupIter->first]); } } std::string mitk::DICOMTagBasedSorter ::BuildGroupID( DICOMDatasetAccess* dataset ) { // just concatenate all tag values assert(dataset); std::stringstream groupID; groupID << "g"; for (auto tagIter = m_DistinguishingTags.cbegin(); tagIter != m_DistinguishingTags.cend(); ++tagIter) { groupID << tagIter->GetGroup() << tagIter->GetElement(); // make group/element part of the id to cover empty tags DICOMDatasetFinding rawTagValue = dataset->GetTagValueAsString(*tagIter); std::string processedTagValue; if ( m_TagValueProcessor[*tagIter] != nullptr && rawTagValue.isValid) { processedTagValue = (*m_TagValueProcessor[*tagIter])(rawTagValue.value); } else { processedTagValue = rawTagValue.value; } groupID << "#" << processedTagValue; } // shorten ID? return groupID.str(); } mitk::DICOMTagBasedSorter::GroupIDToListType mitk::DICOMTagBasedSorter -::SplitInputGroups() +::SplitInputGroups(SplitReasonListType& splitReasons) { DICOMDatasetList input = GetInput(); // copy GroupIDToListType listForGroupID; for (auto dsIter = input.cbegin(); dsIter != input.cend(); ++dsIter) { DICOMDatasetAccess* dataset = *dsIter; assert(dataset); const std::string groupID = this->BuildGroupID( dataset ); MITK_DEBUG << "Group ID for for " << dataset->GetFilenameIfAvailable() << ": " << groupID; listForGroupID[groupID].push_back(dataset); } MITK_DEBUG << "After tag based splitting: " << listForGroupID.size() << " groups"; + splitReasons.clear(); + if (listForGroupID.size() == 1) + { + //no split -> no reason + splitReasons[listForGroupID.begin()->first] = IOVolumeSplitReason::New(); + } + else + { + for (auto& [key, value] : listForGroupID) + { + auto reason = IOVolumeSplitReason::New(); + reason->AddReason(IOVolumeSplitReason::ReasonType::ValueSplitDifference); + splitReasons[key] = reason; + } + } + return listForGroupID; } mitk::DICOMTagBasedSorter::GroupIDToListType& mitk::DICOMTagBasedSorter -::SortGroups(GroupIDToListType& groups) +::SortGroups(GroupIDToListType& groups, SplitReasonListType& splitReasons) { if (m_SortCriterion.IsNotNull()) { /* Three steps here: 1. sort within each group - this may result in orders such as 1 2 3 4 6 7 8 10 12 13 14 2. create new groups by enforcing consecutive order within each group - resorts above example like 1 2 3 4 ; 6 7 8 ; 10 ; 12 13 14 3. sort all of the groups (not WITHIN each group) by their first frame - if earlier "distinguish" steps created groups like 6 7 8 ; 1 2 3 4 ; 10, then this step would sort them like 1 2 3 4 ; 6 7 8 ; 10 */ // Step 1: sort within the groups // for each output // sort by all configured tags, use secondary tags when equal or empty // make configurable: // - sorting order (ascending, descending) // - sort numerically // - ... ? #ifdef MBILOG_ENABLE_DEBUG unsigned int groupIndex(0); #endif for (auto gIter = groups.begin(); gIter != groups.end(); #ifdef MBILOG_ENABLE_DEBUG ++groupIndex, #endif ++gIter) { DICOMDatasetList& dsList = gIter->second; #ifdef MBILOG_ENABLE_DEBUG MITK_DEBUG << " --------------------------------------------------------------------------------"; MITK_DEBUG << " DICOMTagBasedSorter before sorting group : " << groupIndex; for (auto oi = dsList.begin(); oi != dsList.cend(); ++oi) { MITK_DEBUG << " INPUT : " << (*oi)->GetFilenameIfAvailable(); } #endif // #ifdef MBILOG_ENABLE_DEBUG std::sort( dsList.begin(), dsList.end(), ParameterizedDatasetSort( m_SortCriterion ) ); #ifdef MBILOG_ENABLE_DEBUG MITK_DEBUG << " --------------------------------------------------------------------------------"; MITK_DEBUG << " DICOMTagBasedSorter after sorting group : " << groupIndex; for (auto oi = dsList.cbegin(); oi != dsList.cend(); ++oi) { MITK_DEBUG << " OUTPUT : " << (*oi)->GetFilenameIfAvailable(); } MITK_DEBUG << " --------------------------------------------------------------------------------"; #endif // MBILOG_ENABLE_DEBUG } GroupIDToListType consecutiveGroups; + SplitReasonListType consecutiveReasons; if (m_StrictSorting) { // Step 2: create new groups by enforcing consecutive order within each group unsigned int groupIndex(0); for (auto gIter = groups.begin(); gIter != groups.end(); ++gIter) { std::stringstream groupKey; groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + std::string groupKeyStr = groupKey.str(); DICOMDatasetList& dsList = gIter->second; + DICOMDatasetAccess* previousDS(nullptr); unsigned int dsIndex(0); double constantDistance(0.0); bool constantDistanceInitialized(false); for (auto dataset = dsList.cbegin(); dataset != dsList.cend(); ++dsIndex, ++dataset) { + bool splitted = false; if (dsIndex >0) // ignore the first dataset, we cannot check any distances yet.. { // for the second and every following dataset: // let the sorting criterion calculate a "distance" // if the distance is not 1, split off a new group! const double currentDistance = m_SortCriterion->NumericDistance(previousDS, *dataset); if (constantDistanceInitialized) { if (fabs(currentDistance - constantDistance) < fabs(constantDistance * 0.01)) // ok, deviation of up to 1% of distance is tolerated { // nothing to do, just ok MITK_DEBUG << "Checking currentDistance==" << currentDistance << ": small enough"; } //else if (currentDistance < mitk::eps) // close enough to 0 else { MITK_DEBUG << "Split consecutive group at index " << dsIndex << " (current distance " << currentDistance << ", constant distance " << constantDistance << ")"; // split! this is done by simply creating a new group (key) groupKey.str(std::string()); groupKey.clear(); groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + groupKeyStr = groupKey.str(); + splitted = true; } } else { // second slice: learn about the expected distance! // heuristic: if distance is an integer, we check for a special case: // if the distance is integer and not 1/-1, then we assume // a missing slice right after the first slice // ==> split off slices // in all other cases: second dataset at this position, no need to split already, we are still learning about the images // addition to the above: when sorting by imagepositions, a distance other than 1 between the first two slices is // not unusual, actually expected... then we should not split if (m_ExpectDistanceOne) { if ((currentDistance - (int)currentDistance == 0.0) && fabs(currentDistance) != 1.0) // exact comparison. An integer should not be expressed as 1.000000000000000000000000001! { MITK_DEBUG << "Split consecutive group at index " << dsIndex << " (special case: expected distance 1 exactly)"; groupKey.str(std::string()); groupKey.clear(); groupKey << std::setfill('0') << std::setw(6) << groupIndex++; + groupKeyStr = groupKey.str(); + splitted = true; } } MITK_DEBUG << "Initialize strict distance to currentDistance=" << currentDistance; constantDistance = currentDistance; constantDistanceInitialized = true; } } - consecutiveGroups[groupKey.str()].push_back(*dataset); + consecutiveGroups[groupKeyStr].push_back(*dataset); + + if (consecutiveReasons.find(groupKeyStr) == consecutiveReasons.end()) + { + auto dsReason = splitReasons[gIter->first]->Clone(); + if (splitted) + dsReason->AddReason(IOVolumeSplitReason::ReasonType::ValueSortDistance); + consecutiveReasons[groupKeyStr] = dsReason; + } + previousDS = *dataset; } } } else { consecutiveGroups = groups; } // Step 3: sort all of the groups (not WITHIN each group) by their first frame /* build a list-1 of datasets with the first dataset one of each group sort this list-1 build a new result list-2: - iterate list-1, for each dataset - find the group that contains this dataset - add this group as the next element to list-2 return list-2 as the sorted output */ DICOMDatasetList firstSlices; for (auto gIter = consecutiveGroups.cbegin(); gIter != consecutiveGroups.cend(); ++gIter) { assert(!gIter->second.empty()); firstSlices.push_back(gIter->second.front()); } std::sort( firstSlices.begin(), firstSlices.end(), ParameterizedDatasetSort( m_SortCriterion ) ); GroupIDToListType sortedResultBlocks; - unsigned int groupKeyValue(0); - for (auto firstSlice = firstSlices.cbegin(); - firstSlice != firstSlices.cend(); - ++firstSlice) + SplitReasonListType sortedResultsReasons; + + for (auto& [key, group] : consecutiveGroups) { - for (auto gIter = consecutiveGroups.cbegin(); - gIter != consecutiveGroups.cend(); - ++groupKeyValue, ++gIter) - { - if (gIter->second.front() == *firstSlice) - { - std::stringstream groupKey; - groupKey << std::setfill('0') << std::setw(6) << groupKeyValue; // try more than 999,999 groups and you are doomed (your application already is) - sortedResultBlocks[groupKey.str()] = gIter->second; - } - } + auto findSliceIterator = std::find(firstSlices.begin(), firstSlices.end(), group.front()); + + std::stringstream groupKey; + groupKey << std::setfill('0') << std::setw(6) << std::distance(firstSlices.begin(),findSliceIterator); // try more than 999,999 groups and you are doomed (your application already is) + const auto groupKeyStr = groupKey.str(); + sortedResultBlocks[groupKeyStr] = group; + sortedResultsReasons[groupKeyStr] = consecutiveReasons[key]; } groups = sortedResultBlocks; + splitReasons = sortedResultsReasons; } #ifdef MBILOG_ENABLE_DEBUG unsigned int groupIndex( 0 ); for ( auto gIter = groups.begin(); gIter != groups.end(); ++groupIndex, ++gIter ) { DICOMDatasetList& dsList = gIter->second; MITK_DEBUG << " --------------------------------------------------------------------------------"; MITK_DEBUG << " DICOMTagBasedSorter after sorting group : " << groupIndex; for ( auto oi = dsList.begin(); oi != dsList.end(); ++oi ) { MITK_DEBUG << " OUTPUT : " << ( *oi )->GetFilenameIfAvailable(); } MITK_DEBUG << " --------------------------------------------------------------------------------"; } #endif // MBILOG_ENABLE_DEBUG return groups; } mitk::DICOMTagBasedSorter::ParameterizedDatasetSort ::ParameterizedDatasetSort(DICOMSortCriterion::ConstPointer criterion) :m_SortCriterion(criterion) { } bool mitk::DICOMTagBasedSorter::ParameterizedDatasetSort ::operator() (const mitk::DICOMDatasetAccess* left, const mitk::DICOMDatasetAccess* right) { assert(left); assert(right); assert(m_SortCriterion.IsNotNull()); return m_SortCriterion->IsLeftBeforeRight(left, right); } diff --git a/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp b/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp index 28f9ab9a7e..5b16bae486 100644 --- a/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp +++ b/Modules/DICOM/src/mitkEquiDistantBlocksSorter.cpp @@ -1,570 +1,620 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ //#define MBILOG_ENABLE_DEBUG #include "mitkEquiDistantBlocksSorter.h" mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult -::SliceGroupingAnalysisResult() +::SliceGroupingAnalysisResult() : m_SplitReason(IOVolumeSplitReason::New()) { } -mitk::DICOMDatasetList +const mitk::DICOMDatasetList& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult -::GetBlockDatasets() +::GetBlockDatasets() const { return m_GroupedFiles; } -mitk::DICOMDatasetList +const mitk::DICOMDatasetList& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult -::GetUnsortedDatasets() +::GetUnsortedDatasets() const { return m_UnsortedFiles; } +mitk::IOVolumeSplitReason::ConstPointer +mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult::GetSplitReason() const +{ + return m_SplitReason; +} + +mitk::IOVolumeSplitReason::Pointer +mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult::GetSplitReason() +{ + return m_SplitReason; +} + bool mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::ContainsGantryTilt() { return m_TiltInfo.IsRegularGantryTilt(); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToSortedBlock(DICOMDatasetAccess* dataset) { m_GroupedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFileToUnsortedBlock(DICOMDatasetAccess* dataset) { m_UnsortedFiles.push_back( dataset ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::AddFilesToUnsortedBlock(const DICOMDatasetList& datasets) { m_UnsortedFiles.insert( m_UnsortedFiles.end(), datasets.begin(), datasets.end() ); } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SetFirstFilenameOfBlock(const std::string& filename) { m_FirstFilenameOfBlock = filename; } std::string mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetFirstFilenameOfBlock() const { return m_FirstFilenameOfBlock; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::SetLastFilenameOfBlock(const std::string& filename) { m_LastFilenameOfBlock = filename; } std::string mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetLastFilenameOfBlock() const { return m_LastFilenameOfBlock; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::FlagGantryTilt(const GantryTiltInformation& tiltInfo) { m_TiltInfo = tiltInfo; } const mitk::GantryTiltInformation& mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::GetTiltInfo() const { return m_TiltInfo; } void mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult ::UndoPrematureGrouping() { assert( !m_GroupedFiles.empty() ); m_UnsortedFiles.insert( m_UnsortedFiles.begin(), m_GroupedFiles.back() ); m_GroupedFiles.pop_back(); m_TiltInfo = GantryTiltInformation(); } // ------------------------ end helper class mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter() :DICOMDatasetSorter() ,m_AcceptTilt(false) ,m_ToleratedOriginOffset(0.3) ,m_ToleratedOriginOffsetIsAbsolute(false) ,m_AcceptTwoSlicesGroups(true) { } mitk::EquiDistantBlocksSorter ::EquiDistantBlocksSorter(const EquiDistantBlocksSorter& other ) :DICOMDatasetSorter(other) ,m_AcceptTilt(other.m_AcceptTilt) ,m_ToleratedOriginOffset(other.m_ToleratedOriginOffset) ,m_ToleratedOriginOffsetIsAbsolute(other.m_ToleratedOriginOffsetIsAbsolute) ,m_AcceptTwoSlicesGroups(other.m_AcceptTwoSlicesGroups) { } mitk::EquiDistantBlocksSorter ::~EquiDistantBlocksSorter() { } bool mitk::EquiDistantBlocksSorter ::operator==(const DICOMDatasetSorter& other) const { if (const auto* otherSelf = dynamic_cast(&other)) { return this->m_AcceptTilt == otherSelf->m_AcceptTilt && this->m_ToleratedOriginOffsetIsAbsolute == otherSelf->m_ToleratedOriginOffsetIsAbsolute && this->m_AcceptTwoSlicesGroups == otherSelf->m_AcceptTwoSlicesGroups && (fabs(this->m_ToleratedOriginOffset - otherSelf->m_ToleratedOriginOffset) < eps); } else { return false; } } void mitk::EquiDistantBlocksSorter ::PrintConfiguration(std::ostream& os, const std::string& indent) const { std::stringstream ts; if (!m_ToleratedOriginOffsetIsAbsolute) { ts << "adaptive"; } else { ts << m_ToleratedOriginOffset << "mm"; } os << indent << "Sort into blocks of equidistant, well-aligned (tolerance " << ts.str() << ") slices " << (m_AcceptTilt ? "(accepting a gantry tilt)" : "") << std::endl; } void mitk::EquiDistantBlocksSorter ::SetAcceptTilt(bool accept) { m_AcceptTilt = accept; } bool mitk::EquiDistantBlocksSorter ::GetAcceptTilt() const { return m_AcceptTilt; } void mitk::EquiDistantBlocksSorter ::SetAcceptTwoSlicesGroups(bool accept) { m_AcceptTwoSlicesGroups = accept; } bool mitk::EquiDistantBlocksSorter ::GetAcceptTwoSlicesGroups() const { return m_AcceptTwoSlicesGroups; } mitk::EquiDistantBlocksSorter& mitk::EquiDistantBlocksSorter ::operator=(const EquiDistantBlocksSorter& other) { if (this != &other) { DICOMDatasetSorter::operator=(other); m_AcceptTilt = other.m_AcceptTilt; m_ToleratedOriginOffset = other.m_ToleratedOriginOffset; m_ToleratedOriginOffsetIsAbsolute = other.m_ToleratedOriginOffsetIsAbsolute; m_AcceptTwoSlicesGroups = other.m_AcceptTwoSlicesGroups; } return *this; } mitk::DICOMTagList mitk::EquiDistantBlocksSorter ::GetTagsOfInterest() { DICOMTagList tags; tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient tags.push_back( DICOMTag(0x0018, 0x1120) ); // GantryDetectorTilt return tags; } void mitk::EquiDistantBlocksSorter ::Sort() { DICOMDatasetList remainingInput = GetInput(); // copy - typedef std::list OutputListType; - OutputListType outputs; - m_SliceGroupingResults.clear(); while (!remainingInput.empty()) // repeat until all files are grouped somehow { - SliceGroupingAnalysisResult regularBlock = this->AnalyzeFileForITKImageSeriesReaderSpacingAssumption( remainingInput, m_AcceptTilt ); + auto regularBlock = this->AnalyzeFileForITKImageSeriesReaderSpacingAssumption( remainingInput, m_AcceptTilt ); #ifdef MBILOG_ENABLE_DEBUG DICOMDatasetList inBlock = regularBlock.GetBlockDatasets(); DICOMDatasetList laterBlock = regularBlock.GetUnsortedDatasets(); MITK_DEBUG << "Result: sorted 3D group with " << inBlock.size() << " files"; for (DICOMDatasetList::const_iterator diter = inBlock.cbegin(); diter != inBlock.cend(); ++diter) MITK_DEBUG << " IN " << (*diter)->GetFilenameIfAvailable(); for (DICOMDatasetList::const_iterator diter = laterBlock.cbegin(); diter != laterBlock.cend(); ++diter) MITK_DEBUG << " OUT " << (*diter)->GetFilenameIfAvailable(); #endif // MBILOG_ENABLE_DEBUG - outputs.push_back( regularBlock.GetBlockDatasets() ); + remainingInput = regularBlock->GetUnsortedDatasets(); + + if (remainingInput.empty() && !m_SliceGroupingResults.empty() && m_SliceGroupingResults.back()->GetSplitReason()->HasReason(IOVolumeSplitReason::ReasonType::OverlappingSlices)) + { + //if all inputs are processed and there is already a preceding grouping result that has overlapping as split reason, add also overlapping as split reason for the current block + regularBlock->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); + } + m_SliceGroupingResults.push_back( regularBlock ); - remainingInput = regularBlock.GetUnsortedDatasets(); } - unsigned int numberOfOutputs = outputs.size(); + unsigned int numberOfOutputs = m_SliceGroupingResults.size(); this->SetNumberOfOutputs(numberOfOutputs); unsigned int outputIndex(0); - for (auto oIter = outputs.cbegin(); - oIter != outputs.cend(); + for (auto oIter = m_SliceGroupingResults.cbegin(); + oIter != m_SliceGroupingResults.cend(); ++outputIndex, ++oIter) { - this->SetOutput(outputIndex, *oIter); + this->SetOutput(outputIndex, (*oIter)->GetBlockDatasets(), (*oIter)->GetSplitReason()); } } void mitk::EquiDistantBlocksSorter ::SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistance) { m_ToleratedOriginOffset = fractionOfInterSliceDistance; m_ToleratedOriginOffsetIsAbsolute = false; if (m_ToleratedOriginOffset < 0.0) { MITK_WARN << "Call SetToleratedOriginOffsetToAdaptive() only with positive numbers between 0.0 and 1.0, read documentation!"; } if (m_ToleratedOriginOffset > 0.5) { MITK_WARN << "EquiDistantBlocksSorter is now accepting large errors, take care of measurements, they could appear at imprecise locations!"; } } void mitk::EquiDistantBlocksSorter ::SetToleratedOriginOffset(double millimeters) { m_ToleratedOriginOffset = millimeters; m_ToleratedOriginOffsetIsAbsolute = true; if (m_ToleratedOriginOffset < 0.0) { MITK_WARN << "Negative tolerance set to SetToleratedOriginOffset()!"; } } double mitk::EquiDistantBlocksSorter ::GetToleratedOriginOffset() const { return m_ToleratedOriginOffset; } bool mitk::EquiDistantBlocksSorter ::IsToleratedOriginOffsetAbsolute() const { return m_ToleratedOriginOffsetIsAbsolute; } std::string mitk::EquiDistantBlocksSorter ::ConstCharStarToString(const char* s) { return s ? std::string(s) : std::string(); } -mitk::EquiDistantBlocksSorter::SliceGroupingAnalysisResult +std::shared_ptr mitk::EquiDistantBlocksSorter ::AnalyzeFileForITKImageSeriesReaderSpacingAssumption( const DICOMDatasetList& datasets, bool groupImagesWithGantryTilt) { - // result.first = files that fit ITK's assumption - // result.second = files that do not fit, should be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption() again - SliceGroupingAnalysisResult result; + auto result = std::make_shared(); const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient) const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation Vector3D fromFirstToSecondOrigin; fromFirstToSecondOrigin.Fill(0.0); bool fromFirstToSecondOriginInitialized(false); Point3D thisOrigin; thisOrigin.Fill(0.0f); Point3D lastOrigin; lastOrigin.Fill(0.0f); Point3D lastDifferentOrigin; lastDifferentOrigin.Fill(0.0f); bool lastOriginInitialized(false); + int missingSlicesCount = 0; MITK_DEBUG << "--------------------------------------------------------------------------------"; MITK_DEBUG << "Analyzing " << datasets.size() << " files for z-spacing assumption of ITK's ImageSeriesReader (group tilted: " << groupImagesWithGantryTilt << ")"; unsigned int fileIndex(0); double toleratedOriginError(0.005); // default: max. 1/10mm error when measurement crosses 20 slices in z direction (too strict? we don't know better) for (auto dsIter = datasets.cbegin(); dsIter != datasets.cend(); ++dsIter, ++fileIndex) { bool fileFitsIntoPattern(false); std::string thisOriginString; // Read tag value into point3D. PLEASE replace this by appropriate GDCM code if you figure out how to do that thisOriginString = (*dsIter)->GetTagValueAsString(tagImagePositionPatient).value; if (thisOriginString.empty()) { // don't let such files be in a common group. Everything without position information will be loaded as a single slice: // with standard DICOM files this can happen to: CR, DX, SC MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis (no position information)"; // we already have one occupying this position - if ( result.GetBlockDatasets().empty() ) // nothing WITH position information yet + if ( result->GetBlockDatasets().empty() ) // nothing WITH position information yet { // ==> this is a group of its own, stop processing, come back later - result.AddFileToSortedBlock( *dsIter ); + result->AddFileToSortedBlock( *dsIter ); DICOMDatasetList remainingFiles; remainingFiles.insert( remainingFiles.end(), dsIter+1, datasets.end() ); - result.AddFilesToUnsortedBlock( remainingFiles ); - + result->AddFilesToUnsortedBlock( remainingFiles ); + if (!remainingFiles.empty()) + //if there are remaining files add a split reason + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::ImagePostionMissing); fileFitsIntoPattern = false; break; // no files anymore } else { // ==> this does not match, consider later - result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis fileFitsIntoPattern = false; continue; // next file } } bool ignoredConversionError(-42); // hard to get here, no graceful way to react thisOrigin = DICOMStringToPoint3D( thisOriginString, ignoredConversionError ); MITK_DEBUG << " " << fileIndex << " " << (*dsIter)->GetFilenameIfAvailable() << " at " /* << thisOriginString */ << "(" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; if ( lastOriginInitialized && (thisOrigin == lastOrigin) ) { MITK_DEBUG << " ==> Sort away " << *dsIter << " for separate time step"; // we already have one occupying this position - result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); fileFitsIntoPattern = false; } else { if (!fromFirstToSecondOriginInitialized && lastOriginInitialized) // calculate vector as soon as possible when we get a new position { fromFirstToSecondOrigin = thisOrigin - lastDifferentOrigin; fromFirstToSecondOriginInitialized = true; // classic mode without tolerance! if (!m_ToleratedOriginOffsetIsAbsolute) { MITK_DEBUG << "Distance of two slices: " << fromFirstToSecondOrigin.GetNorm() << "mm"; toleratedOriginError = fromFirstToSecondOrigin.GetNorm() * 0.3; // a third of the slice distance // (less than half, which would mean that a slice is displayed where another slice should actually be) } else { toleratedOriginError = m_ToleratedOriginOffset; } MITK_DEBUG << "Accepting errors in actual versus expected origin up to " << toleratedOriginError << "mm"; // Here we calculate if this slice and the previous one are well aligned, // i.e. we test if the previous origin is on a line through the current // origin, directed into the normal direction of the current slice. // If this is NOT the case, then we have a data set with a TILTED GANTRY geometry, // which cannot be simply loaded into a single mitk::Image at the moment. // For this case, we flag this finding in the result and DicomSeriesReader // can correct for that later. Vector3D right; right.Fill(0.0); Vector3D up; right.Fill(0.0); // might be down as well, but it is just a name at this point std::string orientationValue = (*dsIter)->GetTagValueAsString( tagImageOrientation ).value; DICOMStringToOrientationVectors( orientationValue, right, up, ignoredConversionError ); GantryTiltInformation tiltInfo( lastDifferentOrigin, thisOrigin, right, up, 1 ); if ( tiltInfo.IsSheared() ) { /* optimistic approach, accepting gantry tilt: save file for later, check all further files */ // at this point we have TWO slices analyzed! if they are the only two files, we still split, because there is no third to verify our tilting assumption. // later with a third being available, we must check if the initial tilting vector is still valid. if yes, continue. - // if NO, we need to split the already sorted part (result.first) and the currently analyzed file (*dsIter) + // if NO, we need to split the already sorted part (result->first) and the currently analyzed file (*dsIter) // tell apart gantry tilt from overall skewedness // sort out irregularly sheared slices, that IS NOT tilting if ( groupImagesWithGantryTilt && tiltInfo.IsRegularGantryTilt() ) { assert(!datasets.empty()); - result.FlagGantryTilt(tiltInfo); - result.AddFileToSortedBlock( *dsIter ); // this file is good for current block - result.SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); - result.SetLastFilenameOfBlock( datasets.back()->GetFilenameIfAvailable() ); + result->FlagGantryTilt(tiltInfo); + result->AddFileToSortedBlock( *dsIter ); // this file is good for current block + result->SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() ); + result->SetLastFilenameOfBlock( datasets.back()->GetFilenameIfAvailable() ); fileFitsIntoPattern = true; } else // caller does not want tilt compensation OR shearing is more complicated than tilt { - result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::GantryTiltDifference); fileFitsIntoPattern = false; } } else // not sheared { - result.AddFileToSortedBlock( *dsIter ); // this file is good for current block + result->AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } else if (fromFirstToSecondOriginInitialized) // we already know the offset between slices { Point3D assumedOrigin = lastDifferentOrigin + fromFirstToSecondOrigin; Vector3D originError = assumedOrigin - thisOrigin; double norm = originError.GetNorm(); if (norm > toleratedOriginError) { MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = " << norm << ", allowed " << toleratedOriginError << ")."; MITK_DEBUG << " Expected position (" << assumedOrigin[0] << "," << assumedOrigin[1] << "," << assumedOrigin[2] << "), got position (" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")"; MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis"; // At this point we know we deviated from the expectation of ITK's ImageSeriesReader // We split the input file list at this point, i.e. all files up to this one (excluding it) // are returned as group 1, the remaining files (including the faulty one) are group 2 /* Optimistic approach: check if any of the remaining slices fits in */ - result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + result->AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis + + const auto fromLastToThisOriginDistance = (lastDifferentOrigin - thisOrigin).GetNorm(); + const auto fromFirstToSecondOriginDistance = fromFirstToSecondOrigin.GetNorm(); + + auto currentMissCount = static_cast(std::round((fromLastToThisOriginDistance / fromFirstToSecondOriginDistance)-1)); + if (missingSlicesCount == 0 || missingSlicesCount > currentMissCount) + { + if (missingSlicesCount != 0 && currentMissCount == 0) + { + result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::MissingSlices); + } + + missingSlicesCount = currentMissCount; + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency, std::to_string(fromLastToThisOriginDistance)); + } + + if (missingSlicesCount < 0) + { + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); + } + else if (missingSlicesCount > 0 && !result->GetSplitReason()->HasReason(IOVolumeSplitReason::ReasonType::OverlappingSlices)) + { + //If the missing slice count is positive, but no overlapping was flagged, add the missing slice reason. + //We only do it if overlapping was not flagged, to avoid false positives, that could be triggered by slices + //of the overlapping volume. + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::MissingSlices, std::to_string(missingSlicesCount)); + } + fileFitsIntoPattern = false; } else { - result.AddFileToSortedBlock( *dsIter ); // this file is good for current block + result->AddFileToSortedBlock( *dsIter ); // this file is good for current block + result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::SliceDistanceInconsistency); + result->GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::MissingSlices); fileFitsIntoPattern = true; + missingSlicesCount = 0; } } else // this should be the very first slice { - result.AddFileToSortedBlock( *dsIter ); // this file is good for current block + result->AddFileToSortedBlock( *dsIter ); // this file is good for current block fileFitsIntoPattern = true; } } // record current origin for reference in later iterations if ( !lastOriginInitialized || ( fileFitsIntoPattern && (thisOrigin != lastOrigin) ) ) { lastDifferentOrigin = thisOrigin; } lastOrigin = thisOrigin; lastOriginInitialized = true; } - if ( result.ContainsGantryTilt() ) + if ( result->ContainsGantryTilt() ) { // check here how many files were grouped. // IF it was only two files AND we assume tiltedness (e.g. save "distance") // THEN we would want to also split the two previous files (simple) because // we don't have any reason to assume they belong together // Above behavior can be configured via m_AcceptTwoSlicesGroups, the default being "do accept" - if ( result.GetBlockDatasets().size() == 2 && !m_AcceptTwoSlicesGroups ) + if ( result->GetBlockDatasets().size() == 2 && !m_AcceptTwoSlicesGroups ) { - result.UndoPrematureGrouping(); + result->UndoPrematureGrouping(); + result->GetSplitReason()->AddReason(IOVolumeSplitReason::ReasonType::GantryTiltDifference); } } // update tilt info to get maximum precision // earlier, tilt was only calculated from first and second slice. // now that we know the whole range, we can re-calculate using the very first and last slice - if ( result.ContainsGantryTilt() && result.GetBlockDatasets().size() > 1 ) + if ( result->ContainsGantryTilt() && result->GetBlockDatasets().size() > 1 ) { try { - DICOMDatasetList datasets = result.GetBlockDatasets(); + DICOMDatasetList datasets = result->GetBlockDatasets(); DICOMDatasetAccess* firstDataset = datasets.front(); DICOMDatasetAccess* lastDataset = datasets.back(); unsigned int numberOfSlicesApart = datasets.size() - 1; std::string orientationString = firstDataset->GetTagValueAsString( tagImageOrientation ).value; std::string firstOriginString = firstDataset->GetTagValueAsString( tagImagePositionPatient ).value; std::string lastOriginString = lastDataset->GetTagValueAsString( tagImagePositionPatient ).value; - result.FlagGantryTilt( GantryTiltInformation::MakeFromTagValues( firstOriginString, lastOriginString, orientationString, numberOfSlicesApart )); + result->FlagGantryTilt( GantryTiltInformation::MakeFromTagValues( firstOriginString, lastOriginString, orientationString, numberOfSlicesApart )); } catch (...) { // just do not flag anything, we are ok } } return result; } diff --git a/Modules/DICOM/src/mitkThreeDnTDICOMSeriesReader.cpp b/Modules/DICOM/src/mitkThreeDnTDICOMSeriesReader.cpp index 599719fd36..484d0f2442 100644 --- a/Modules/DICOM/src/mitkThreeDnTDICOMSeriesReader.cpp +++ b/Modules/DICOM/src/mitkThreeDnTDICOMSeriesReader.cpp @@ -1,295 +1,308 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkThreeDnTDICOMSeriesReader.h" #include "mitkITKDICOMSeriesReaderHelper.h" mitk::ThreeDnTDICOMSeriesReader ::ThreeDnTDICOMSeriesReader(unsigned int decimalPlacesForOrientation) :DICOMITKSeriesGDCMReader(decimalPlacesForOrientation) ,m_Group3DandT(m_DefaultGroup3DandT), m_OnlyCondenseSameSeries(m_DefaultOnlyCondenseSameSeries) { } mitk::ThreeDnTDICOMSeriesReader ::ThreeDnTDICOMSeriesReader(const ThreeDnTDICOMSeriesReader& other ) :DICOMITKSeriesGDCMReader(other) ,m_Group3DandT(m_DefaultGroup3DandT), m_OnlyCondenseSameSeries(m_DefaultOnlyCondenseSameSeries) { } mitk::ThreeDnTDICOMSeriesReader ::~ThreeDnTDICOMSeriesReader() { } mitk::ThreeDnTDICOMSeriesReader& mitk::ThreeDnTDICOMSeriesReader ::operator=(const ThreeDnTDICOMSeriesReader& other) { if (this != &other) { DICOMITKSeriesGDCMReader::operator=(other); this->m_Group3DandT = other.m_Group3DandT; } return *this; } bool mitk::ThreeDnTDICOMSeriesReader ::operator==(const DICOMFileReader& other) const { if (const auto* otherSelf = dynamic_cast(&other)) { return DICOMITKSeriesGDCMReader::operator==(other) && this->m_Group3DandT == otherSelf->m_Group3DandT; } else { return false; } } void mitk::ThreeDnTDICOMSeriesReader ::SetGroup3DandT(bool on) { m_Group3DandT = on; } bool mitk::ThreeDnTDICOMSeriesReader ::GetGroup3DandT() const { return m_Group3DandT; } /** Helper function to make the code in mitk::ThreeDnTDICOMSeriesReader ::Condense3DBlocks(SortingBlockList& resultOf3DGrouping) more readable.*/ bool BlockShouldBeCondensed(bool onlyCondenseSameSeries, unsigned int currentBlockNumberOfSlices, unsigned int otherBlockNumberOfSlices, const mitk::DICOMDatasetFinding& currentBlockFirstOrigin, const mitk::DICOMDatasetFinding& currentBlockLastOrigin, const mitk::DICOMDatasetFinding& otherBlockFirstOrigin, const mitk::DICOMDatasetFinding& otherBlockLastOrigin, const mitk::DICOMDatasetFinding& currentBlockSeriesInstanceUID, const mitk::DICOMDatasetFinding& otherBlockSeriesInstanceUID) { if (otherBlockNumberOfSlices != currentBlockNumberOfSlices) return false; //don't condense blocks that have unequal slice count if (!otherBlockFirstOrigin.isValid || !otherBlockLastOrigin.isValid) return false; //don't condense blocks that have invalid origins if (!currentBlockFirstOrigin.isValid || !currentBlockLastOrigin.isValid) return false; //don't condense blocks that have invalid origins const bool sameSeries = otherBlockSeriesInstanceUID.isValid && currentBlockSeriesInstanceUID.isValid && otherBlockSeriesInstanceUID.value == currentBlockSeriesInstanceUID.value; if (onlyCondenseSameSeries && !sameSeries) return false; //don't condense blocks if it is only allowed to condense same series and series are not defined or not equal. if (otherBlockFirstOrigin.value != currentBlockFirstOrigin.value) return false; //don't condense blocks that have unequal first origins if (otherBlockLastOrigin.value != currentBlockLastOrigin.value) return false; //don't condense blocks that have unequal last origins return true; } mitk::DICOMITKSeriesGDCMReader::SortingBlockList mitk::ThreeDnTDICOMSeriesReader ::Condense3DBlocks(SortingBlockList& resultOf3DGrouping) { if (!m_Group3DandT) { return resultOf3DGrouping; // don't work if nobody asks us to } SortingBlockList remainingBlocks = resultOf3DGrouping; SortingBlockList non3DnTBlocks; SortingBlockList true3DnTBlocks; std::vector true3DnTBlocksTimeStepCount; // we should describe our need for this tag as needed via a function // (however, we currently know that the superclass will always need this tag) const DICOMTag tagImagePositionPatient(0x0020, 0x0032); const DICOMTag tagSeriesInstaceUID(0x0020, 0x000e); while (!remainingBlocks.empty()) { // new block to fill up - const DICOMDatasetAccessingImageFrameList& firstBlock = remainingBlocks.front(); + const DICOMDatasetAccessingImageFrameList& firstBlock = remainingBlocks.front().first; DICOMDatasetAccessingImageFrameList current3DnTBlock = firstBlock; + auto currentSplitReason = remainingBlocks.front().second; + int current3DnTBlockNumberOfTimeSteps = 1; // get block characteristics of first block const unsigned int currentBlockNumberOfSlices = firstBlock.size(); const auto currentBlockFirstOrigin = firstBlock.front()->GetTagValueAsString( tagImagePositionPatient ); const auto currentBlockLastOrigin = firstBlock.back()->GetTagValueAsString( tagImagePositionPatient ); const auto currentBlockSeriesInstanceUID = firstBlock.back()->GetTagValueAsString(tagSeriesInstaceUID); remainingBlocks.erase( remainingBlocks.begin() ); // compare all other blocks against the first one for (auto otherBlockIter = remainingBlocks.begin(); otherBlockIter != remainingBlocks.cend(); /*++otherBlockIter*/) // <-- inside loop { // get block characteristics from first block - const DICOMDatasetAccessingImageFrameList otherBlock = *otherBlockIter; + const DICOMDatasetAccessingImageFrameList otherBlock = otherBlockIter->first; const unsigned int otherBlockNumberOfSlices = otherBlock.size(); const auto otherBlockFirstOrigin = otherBlock.front()->GetTagValueAsString( tagImagePositionPatient ); const auto otherBlockLastOrigin = otherBlock.back()->GetTagValueAsString( tagImagePositionPatient ); const auto otherBlockSeriesInstanceUID = otherBlock.back()->GetTagValueAsString(tagSeriesInstaceUID); // add matching blocks to current3DnTBlock // keep other blocks for later if ( BlockShouldBeCondensed(m_OnlyCondenseSameSeries, currentBlockNumberOfSlices, otherBlockNumberOfSlices, currentBlockFirstOrigin, currentBlockLastOrigin, otherBlockFirstOrigin, otherBlockLastOrigin, currentBlockSeriesInstanceUID, otherBlockSeriesInstanceUID)) { // matching block ++current3DnTBlockNumberOfTimeSteps; current3DnTBlock.insert( current3DnTBlock.end(), otherBlock.begin(), otherBlock.end() ); // append + //also merge split reasons + currentSplitReason = currentSplitReason->ExtendReason(otherBlockIter->second); + // remove this block from remainingBlocks otherBlockIter = remainingBlocks.erase(otherBlockIter); // make sure iterator otherBlockIter is valid afterwards } else { ++otherBlockIter; } } // in any case, we now know all about the first block of our list ... // ... and we either call it 3D o 3D+t if (current3DnTBlockNumberOfTimeSteps > 1) { - true3DnTBlocks.push_back(current3DnTBlock); + true3DnTBlocks.emplace_back(current3DnTBlock,currentSplitReason); true3DnTBlocksTimeStepCount.push_back(current3DnTBlockNumberOfTimeSteps); } else { - non3DnTBlocks.push_back(current3DnTBlock); + non3DnTBlocks.emplace_back(current3DnTBlock, currentSplitReason); } } // create output for real 3D+t blocks (other outputs will be created by superclass) // set 3D+t flag on output block this->SetNumberOfOutputs( true3DnTBlocks.size() ); unsigned int o = 0; for (auto blockIter = true3DnTBlocks.cbegin(); blockIter != true3DnTBlocks.cend(); ++o, ++blockIter) { // bad copy&paste code from DICOMITKSeriesGDCMReader, should be handled in a better way - DICOMDatasetAccessingImageFrameList gdcmFrameInfoList = *blockIter; + const auto& gdcmFrameInfoList = blockIter->first; assert(!gdcmFrameInfoList.empty()); // reverse frames if necessary // update tilt information from absolute last sorting const DICOMDatasetList datasetList = ConvertToDICOMDatasetList( gdcmFrameInfoList ); m_NormalDirectionConsistencySorter->SetInput( datasetList ); m_NormalDirectionConsistencySorter->Sort(); const DICOMDatasetAccessingImageFrameList sortedGdcmInfoFrameList = ConvertToDICOMDatasetAccessingImageFrameList( m_NormalDirectionConsistencySorter->GetOutput(0) ); const GantryTiltInformation& tiltInfo = m_NormalDirectionConsistencySorter->GetTiltInformation(); // set frame list for current block const DICOMImageFrameList frameList = ConvertToDICOMImageFrameList( sortedGdcmInfoFrameList ); assert(!frameList.empty()); DICOMImageBlockDescriptor block; block.SetTagCache( this->GetTagCache() ); // important: this must be before SetImageFrameList(), because SetImageFrameList will trigger reading of lots of interesting tags! block.SetAdditionalTagsOfInterest(GetAdditionalTagsOfInterest()); block.SetTagLookupTableToPropertyFunctor(GetTagLookupTableToPropertyFunctor()); block.SetImageFrameList( frameList ); block.SetTiltInformation( tiltInfo ); + block.SetSplitReason(blockIter->second->Clone()); + + if (true3DnTBlocks.size() == 1 && non3DnTBlocks.empty()) + { + //if we have condensed everything into just on 3DnT block, we can remove the overlap reason, + //because no real overlap is existent any more. + block.GetSplitReason()->RemoveReason(IOVolumeSplitReason::ReasonType::OverlappingSlices); + } block.SetFlag("3D+t", true); block.SetIntProperty("timesteps", true3DnTBlocksTimeStepCount[o]); MITK_DEBUG << "Found " << true3DnTBlocksTimeStepCount[o] << " timesteps"; this->SetOutput( o, block ); } return non3DnTBlocks; } bool mitk::ThreeDnTDICOMSeriesReader ::LoadImages() { bool success = true; unsigned int numberOfOutputs = this->GetNumberOfOutputs(); for (unsigned int o = 0; o < numberOfOutputs; ++o) { const DICOMImageBlockDescriptor& block = this->InternalGetOutput(o); if (block.GetFlag("3D+t", false)) { success &= this->LoadMitkImageForOutput(o); } else { success &= DICOMITKSeriesGDCMReader::LoadMitkImageForOutput(o); // let superclass handle non-3D+t } } return success; } bool mitk::ThreeDnTDICOMSeriesReader ::LoadMitkImageForImageBlockDescriptor(DICOMImageBlockDescriptor& block) const { PushLocale(); const DICOMImageFrameList& frames = block.GetImageFrameList(); const GantryTiltInformation tiltInfo = block.GetTiltInformation(); const bool hasTilt = tiltInfo.IsRegularGantryTilt(); const int numberOfTimesteps = block.GetNumberOfTimeSteps(); if (numberOfTimesteps == 1) { return DICOMITKSeriesGDCMReader::LoadMitkImageForImageBlockDescriptor(block); } const int numberOfFramesPerTimestep = block.GetNumberOfFramesPerTimeStep(); ITKDICOMSeriesReaderHelper::StringContainerList filenamesPerTimestep; for (int timeStep = 0; timeStepFilename ); } filenamesPerTimestep.push_back( filenamesOfThisTimeStep ); } mitk::ITKDICOMSeriesReaderHelper helper; mitk::Image::Pointer mitkImage = helper.Load3DnT( filenamesPerTimestep, m_FixTiltByShearing && hasTilt, tiltInfo ); block.SetMitkImage( mitkImage ); PopLocale(); return true; } diff --git a/Modules/DICOMTesting/src/mitkTestDICOMLoading.cpp b/Modules/DICOMTesting/src/mitkTestDICOMLoading.cpp index 62c86a2614..329ce3d39e 100644 --- a/Modules/DICOMTesting/src/mitkTestDICOMLoading.cpp +++ b/Modules/DICOMTesting/src/mitkTestDICOMLoading.cpp @@ -1,609 +1,616 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ //#define MBILOG_ENABLE_DEBUG #include "mitkTestDICOMLoading.h" #include "mitkDICOMIOMetaInformationPropertyConstants.h" +#include "mitkIOMetaInformationPropertyConstants.h" #include "mitkDICOMProperty.h" #include "mitkArbitraryTimeGeometry.h" #include #include #include #include "itksys/SystemTools.hxx" mitk::TestDICOMLoading::TestDICOMLoading() :m_PreviousCLocale(nullptr) { } void mitk::TestDICOMLoading::SetDefaultLocale() { // remember old locale only once if (m_PreviousCLocale == nullptr) { m_PreviousCLocale = setlocale(LC_NUMERIC, nullptr); // set to "C" setlocale(LC_NUMERIC, "C"); m_PreviousCppLocale = std::cin.getloc(); std::locale l( "C" ); std::cin.imbue(l); std::cout.imbue(l); } } void mitk::TestDICOMLoading::ResetUserLocale() { if (m_PreviousCLocale) { setlocale(LC_NUMERIC, m_PreviousCLocale); std::cin.imbue(m_PreviousCppLocale); std::cout.imbue(m_PreviousCppLocale); m_PreviousCLocale = nullptr; } } mitk::TestDICOMLoading::ImageList mitk::TestDICOMLoading ::LoadFiles( const StringList& files ) { for (auto iter = files.begin(); iter != files.end(); ++iter) { MITK_DEBUG << "File " << *iter; } ImageList result; ClassicDICOMSeriesReader::Pointer reader = this->BuildDICOMReader(); reader->SetTagLookupTableToPropertyFunctor(mitk::GetDICOMPropertyForDICOMValuesFunctor); reader->SetInputFiles( files ); reader->AnalyzeInputFiles(); reader->PrintOutputs(std::cout,true); reader->LoadImages(); unsigned int numberOfImages = reader->GetNumberOfOutputs(); for (unsigned imageIndex = 0; imageIndex < numberOfImages; ++imageIndex) { const DICOMImageBlockDescriptor& block = reader->GetOutput(imageIndex); result.push_back( block.GetMitkImage() ); } return result; } mitk::ClassicDICOMSeriesReader::Pointer mitk::TestDICOMLoading ::BuildDICOMReader() { ClassicDICOMSeriesReader::Pointer reader = ClassicDICOMSeriesReader::New(); reader->SetFixTiltByShearing(true); return reader; } mitk::Image::Pointer mitk::TestDICOMLoading ::DecorateVerifyCachedImage( const StringList& files, mitk::DICOMTagCache* tagCache, mitk::Image::Pointer cachedImage ) { DICOMImageBlockDescriptor block; DICOMImageFrameList framelist; for (auto iter = files.begin(); iter != files.end(); ++iter) { framelist.push_back( DICOMImageFrameInfo::New(*iter) ); } block.SetImageFrameList( framelist ); block.SetTagCache( tagCache ); block.SetMitkImage( cachedImage ); // this should/will create a propertylist describing the image slices return block.GetMitkImage(); } mitk::Image::Pointer mitk::TestDICOMLoading ::DecorateVerifyCachedImage( const StringList& files, mitk::Image::Pointer cachedImage ) { ClassicDICOMSeriesReader::Pointer reader = this->BuildDICOMReader(); reader->SetTagLookupTableToPropertyFunctor(mitk::GetDICOMPropertyForDICOMValuesFunctor); reader->SetInputFiles( files ); reader->AnalyzeInputFiles(); // This just creates a "tag cache and a nice DICOMImageBlockDescriptor. // Both of these could also be produced in a different way. The only // important thing is, that the DICOMImageBlockDescriptor knows a // tag-cache object when PropertyDecorateCachedMitkImageForImageBlockDescriptor // is called. if ( reader->GetNumberOfOutputs() != 1 ) { MITK_ERROR << "Reader produce " << reader->GetNumberOfOutputs() << " images instead of 1 expected.."; return nullptr; } DICOMImageBlockDescriptor block = reader->GetOutput(0); // creates a block copy block.SetMitkImage( cachedImage ); // this should/will create a propertylist describing the image slices return block.GetMitkImage(); } std::string mitk::TestDICOMLoading::ComponentTypeToString(itk::IOComponentEnum type) { if (type == itk::IOComponentEnum::UCHAR) return "UCHAR"; else if (type == itk::IOComponentEnum::CHAR) return "CHAR"; else if (type == itk::IOComponentEnum::USHORT) return "USHORT"; else if (type == itk::IOComponentEnum::SHORT) return "SHORT"; else if (type == itk::IOComponentEnum::UINT) return "UINT"; else if (type == itk::IOComponentEnum::INT) return "INT"; else if (type == itk::IOComponentEnum::ULONG) return "ULONG"; else if (type == itk::IOComponentEnum::LONG) return "LONG"; else if (type == itk::IOComponentEnum::FLOAT) return "FLOAT"; else if (type == itk::IOComponentEnum::DOUBLE) return "DOUBLE"; else return "UNKNOWN"; } // add a line to stringstream result (see DumpImageInformation #define DumpLine(field, data) DumpILine(0, field, data) // add an indented(!) line to stringstream result (see DumpImageInformation #define DumpILine(indent, field, data) \ { \ std::string DumpLine_INDENT; DumpLine_INDENT.resize(indent, ' ' ); \ result << DumpLine_INDENT << field << ": " << data << "\n"; \ } std::string mitk::TestDICOMLoading::DumpImageInformation( const Image* image ) { std::stringstream result; if (image == nullptr) return result.str(); SetDefaultLocale(); // basic image data DumpLine( "Pixeltype", ComponentTypeToString(image->GetPixelType().GetComponentType()) ); DumpLine( "BitsPerPixel", image->GetPixelType().GetBpe() ); DumpLine( "Dimension", image->GetDimension() ); result << "Dimensions: "; for (unsigned int dim = 0; dim < image->GetDimension(); ++dim) result << image->GetDimension(dim) << " "; result << "\n"; // geometry data result << "Geometry: \n"; const TimeGeometry* timeGeometry = image->GetTimeGeometry(); BaseGeometry* geometry = timeGeometry->GetGeometryForTimeStep(0); if (geometry) { AffineTransform3D* transform = geometry->GetIndexToWorldTransform(); if (transform) { result << " " << "Matrix: "; const AffineTransform3D::MatrixType& matrix = transform->GetMatrix(); for (unsigned int i = 0; i < 3; ++i) for (unsigned int j = 0; j < 3; ++j) result << matrix[i][j] << " "; result << "\n"; result << " " << "Offset: "; const AffineTransform3D::OutputVectorType& offset = transform->GetOffset(); for (unsigned int i = 0; i < 3; ++i) result << offset[i] << " "; result << "\n"; result << " " << "Center: "; const AffineTransform3D::InputPointType& center = transform->GetCenter(); for (unsigned int i = 0; i < 3; ++i) result << center[i] << " "; result << "\n"; result << " " << "Translation: "; const AffineTransform3D::OutputVectorType& translation = transform->GetTranslation(); for (unsigned int i = 0; i < 3; ++i) result << translation[i] << " "; result << "\n"; result << " " << "Scale: "; const double* scale = transform->GetScale(); for (unsigned int i = 0; i < 3; ++i) result << scale[i] << " "; result << "\n"; result << " " << "Origin: "; const Point3D& origin = geometry->GetOrigin(); for (unsigned int i = 0; i < 3; ++i) result << origin[i] << " "; result << "\n"; result << " " << "Spacing: "; const Vector3D& spacing = geometry->GetSpacing(); for (unsigned int i = 0; i < 3; ++i) result << spacing[i] << " "; result << "\n"; result << " " << "TimeBounds: "; /////////////////////////////////////// // Workaround T27883. See https://phabricator.mitk.org/T27883#219473 for more details. // This workaround should be removed as soon as T28262 is solved! TimeBounds timeBounds = timeGeometry->GetTimeBounds(); auto atg = dynamic_cast(timeGeometry); if (atg && atg->HasCollapsedFinalTimeStep()) { timeBounds[1] = timeBounds[1] - 1.; } //Original code: //const TimeBounds timeBounds = timeGeometry->GetTimeBounds(); // // End of workaround for T27883 ////////////////////////////////////// for (unsigned int i = 0; i < 2; ++i) result << timeBounds[i] << " "; result << "\n"; } } // io dicom meta information AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_CONFIGURATION(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_FILES(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_GANTRY_TILT_CORRECTED(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_IMPLEMENTATION_LEVEL_STRING(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_PIXEL_SPACING_INTERPRETATION_STRING(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_3D_plus_t(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_DCMTK(), image, result); AddPropertyToDump(mitk::DICOMIOMetaInformationPropertyConstants::READER_GDCM(), image, result); + AddPropertyToDump(mitk::IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON(), image, result); ResetUserLocale(); return result.str(); } void mitk::TestDICOMLoading::AddPropertyToDump(const mitk::PropertyKeyPath& key, const mitk::Image* image, std::stringstream& result) { auto propKey = mitk::PropertyKeyPathToPropertyName(key); auto prop = image->GetProperty(propKey.c_str()); if (prop.IsNotNull()) { auto value = prop->GetValueAsString(); auto dicomProp = dynamic_cast< mitk::DICOMProperty*>(prop.GetPointer()); if (dicomProp != nullptr) { auto strippedProp = dicomProp->Clone(); if (key == mitk::DICOMIOMetaInformationPropertyConstants::READER_FILES()) {//strip dicom file information from path to ensure generalized dump files auto timePoints = strippedProp->GetAvailableTimeSteps(); for (auto timePoint : timePoints) { auto slices = strippedProp->GetAvailableSlices(timePoint); for (auto slice : slices) { auto value = strippedProp->GetValue(timePoint, slice); value = itksys::SystemTools::GetFilenameName(value); strippedProp->SetValue(timePoint, slice, value); } } } value = mitk::PropertyPersistenceSerialization::serializeTemporoSpatialStringPropertyToJSON(strippedProp); } result << propKey << ": " << value << "\n"; } } std::string mitk::TestDICOMLoading::trim(const std::string& pString, const std::string& pWhitespace) { const size_t beginStr = pString.find_first_not_of(pWhitespace); if (beginStr == std::string::npos) { // no content return ""; } const size_t endStr = pString.find_last_not_of(pWhitespace); const size_t range = endStr - beginStr + 1; return pString.substr(beginStr, range); } std::string mitk::TestDICOMLoading::reduce(const std::string& pString, const std::string& pFill, const std::string& pWhitespace) { // trim first std::string result(trim(pString, pWhitespace)); // replace sub ranges size_t beginSpace = result.find_first_of(pWhitespace); while (beginSpace != std::string::npos) { const size_t endSpace = result.find_first_not_of(pWhitespace, beginSpace); const size_t range = endSpace - beginSpace; result.replace(beginSpace, range, pFill); const size_t newStart = beginSpace + pFill.length(); beginSpace = result.find_first_of(pWhitespace, newStart); } return result; } bool mitk::TestDICOMLoading::CompareSpacedValueFields( const std::string& reference, const std::string& test, double /*eps*/ ) { bool result(true); // tokenize string, compare each token, if possible by float comparison std::stringstream referenceStream(reduce(reference)); std::stringstream testStream(reduce(test)); std::string refToken; std::string testToken; while ( std::getline( referenceStream, refToken, ' ' ) && std::getline ( testStream, testToken, ' ' ) ) { float refNumber; float testNumber; if ( this->StringToNumber(refToken, refNumber) ) { if ( this->StringToNumber(testToken, testNumber) ) { // print-out compared tokens if DEBUG output allowed MITK_DEBUG << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; bool old_result = result; result &= ( std::abs(refNumber - testNumber) < 0.0001f /*mitk::eps*/ ); // log the token/number which causes the test to fail if( old_result != result) { MITK_ERROR << std::setprecision(16) << "Reference Token '" << refToken << "'" << " value " << refNumber << ", test Token '" << testToken << "'" << " value " << testNumber; MITK_ERROR << "[FALSE] - difference: " << std::setprecision(16) << std::abs(refNumber - testNumber) << " EPS: " << 0.0001f; //mitk::eps; } } else { MITK_ERROR << refNumber << " cannot be compared to '" << testToken << "'"; } } else { MITK_DEBUG << "Token '" << refToken << "'" << " handled as string"; result &= refToken == testToken; } } if ( std::getline( referenceStream, refToken, ' ' ) ) { MITK_ERROR << "Reference string still had values when test string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } else if ( std::getline( testStream, testToken, ' ' ) ) { MITK_ERROR << "Test string still had values when reference string was already parsed: ref '" << reference << "', test '" << test << "'"; result = false; } return result; } bool mitk::TestDICOMLoading::CompareJSON(const std::string& reference, const std::string& test) { try { auto jReference = nlohmann::json::parse(reference); auto jTest = nlohmann::json::parse(test); return jReference == jTest; } catch (const nlohmann::json::exception& e) { MITK_ERROR << e.what(); return false; } } bool mitk::TestDICOMLoading::CompareImageInformationDumps( const std::string& referenceDump, const std::string& testDump ) { KeyValueMap reference = ParseDump(referenceDump); KeyValueMap test = ParseDump(testDump); bool testResult(true); // verify all expected values for (KeyValueMap::const_iterator refIter = reference.begin(); refIter != reference.end(); ++refIter) { const std::string& refKey = refIter->first; const std::string& refValue = refIter->second; if ( test.find(refKey) != test.end() ) { const std::string& testValue = test[refKey]; if (refKey == mitk::PropertyKeyPathToPropertyName(mitk::DICOMIOMetaInformationPropertyConstants::READER_DCMTK())) { //check dcmtk version always against the current version of the system bool thisTestResult = testValue == std::string(" ") + PACKAGE_VERSION; testResult &= thisTestResult; MITK_DEBUG << refKey << ": '" << PACKAGE_VERSION << "' == '" << testValue << "' ? " << (thisTestResult ? "YES" : "NO"); } else if (refKey == mitk::PropertyKeyPathToPropertyName(mitk::DICOMIOMetaInformationPropertyConstants::READER_GDCM())) {//check gdcm version always against the current version of the system bool thisTestResult = testValue == std::string(" ") + gdcm::Version::GetVersion(); testResult &= thisTestResult; MITK_DEBUG << refKey << ": '" << gdcm::Version::GetVersion() << "' == '" << testValue << "' ? " << (thisTestResult ? "YES" : "NO"); } else if (refKey == mitk::PropertyKeyPathToPropertyName(mitk::DICOMIOMetaInformationPropertyConstants::READER_FILES())) { bool thisTestResult = CompareJSON(refValue, testValue); testResult &= thisTestResult; } + else if (refKey == mitk::PropertyKeyPathToPropertyName(mitk::IOMetaInformationPropertyConstants::VOLUME_SPLIT_REASON())) + { + bool thisTestResult = CompareJSON(refValue, testValue); + testResult &= thisTestResult; + } else { bool thisTestResult = CompareSpacedValueFields(refValue, testValue); testResult &= thisTestResult; MITK_DEBUG << refKey << ": '" << refValue << "' == '" << testValue << "' ? " << (thisTestResult ? "YES" : "NO"); } } else { MITK_ERROR << "Reference dump contains a key'" << refKey << "' (value '" << refValue << "')." ; MITK_ERROR << "This key is expected to be generated for tests (but was not). Most probably you need to update your test data."; return false; } } // now check test dump does not contain any additional keys for (KeyValueMap::const_iterator testIter = test.begin(); testIter != test.end(); ++testIter) { const std::string& key = testIter->first; const std::string& value = testIter->second; if (key == mitk::PropertyKeyPathToPropertyName(mitk::DICOMIOMetaInformationPropertyConstants::READER_DCMTK())) {//check dcmtk version always against the current version of the system bool thisTestResult = value == std::string(" ")+PACKAGE_VERSION; testResult &= thisTestResult; MITK_DEBUG << key << ": '" << PACKAGE_VERSION << "' == '" << value << "' ? " << (thisTestResult ? "YES" : "NO"); } else if (key == mitk::PropertyKeyPathToPropertyName(mitk::DICOMIOMetaInformationPropertyConstants::READER_GDCM())) {//check gdcm version always against the current version of the system bool thisTestResult = value == std::string(" ") + gdcm::Version::GetVersion(); testResult &= thisTestResult; MITK_DEBUG << key << ": '" << gdcm::Version::GetVersion() << "' == '" << value << "' ? " << (thisTestResult ? "YES" : "NO"); } else if ( reference.find(key) == reference.end() ) { MITK_ERROR << "Test dump contains an unexpected key'" << key << "' (value '" << value << "')." ; MITK_ERROR << "This key is not expected. Most probably you need to update your test data."; return false; } } return testResult; } mitk::TestDICOMLoading::KeyValueMap mitk::TestDICOMLoading::ParseDump( const std::string& dump ) { KeyValueMap parsedResult; std::string shredder(dump); std::stack surroundingKeys; std::stack expectedIndents; expectedIndents.push(0); while (true) { std::string::size_type newLinePos = shredder.find( '\n' ); if (newLinePos == std::string::npos || newLinePos == 0) break; std::string line = shredder.substr( 0, newLinePos ); shredder = shredder.erase( 0, newLinePos+1 ); std::string::size_type keyPosition = line.find_first_not_of( ' ' ); std::string::size_type colonPosition = line.find( ':' ); std::string key = line.substr(keyPosition, colonPosition - keyPosition); std::string::size_type firstSpacePosition = key.find_first_of(" "); if (firstSpacePosition != std::string::npos) { key.erase(firstSpacePosition); } if ( keyPosition > expectedIndents.top() ) { // more indent than before expectedIndents.push(keyPosition); } else { if (!surroundingKeys.empty()) { surroundingKeys.pop(); // last of same length } while (expectedIndents.top() != keyPosition) { expectedIndents.pop(); if (!surroundingKeys.empty()) { surroundingKeys.pop(); } }; // unwind until current indent is found } if (!surroundingKeys.empty()) { key = surroundingKeys.top() + "." + key; // construct current key name } surroundingKeys.push(key); // this is the new embracing key std::string value = line.substr(colonPosition+1); MITK_DEBUG << " Key: '" << key << "' value '" << value << "'" ; parsedResult[key] = value; // store parsing result } return parsedResult; } diff --git a/Modules/DICOMTesting/test/CMakeLists.txt b/Modules/DICOMTesting/test/CMakeLists.txt index 5912e42626..204d9c04a5 100644 --- a/Modules/DICOMTesting/test/CMakeLists.txt +++ b/Modules/DICOMTesting/test/CMakeLists.txt @@ -1,89 +1,101 @@ MITK_CREATE_MODULE_TESTS() - +############################################################### +# Test group 1 # verify minimum expectations: # files are recognized as DICOM # loading files results in a given number of images mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoFiles mitkDICOMTestingSanityTest 0) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_CTImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-ct.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_MRImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-mr.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_SCImage mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc.dcm) mitkAddCustomModuleTest(mitkDICOMTestingSanityTest_NoImagePositionPatient mitkDICOMTestingSanityTest 1 ${MITK_DATA_DIR}/spacing-ok-sc-no2032.dcm) +############################################################### +# Test group 2 # verifies that the loader can also be used to just scan for tags and provide them in mitk::Properties (parameter preLoadedVolume) mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Slice mitkDICOMPreloadedVolumeTest ${MITK_DATA_DIR}/spacing-ok-ct.dcm) +############################################################### +# Test group 3 +# verifies that the construction of the volumes (splitting, sorting, etc. of slices are done as expected) for different configurations set(VERIFY_DUMP_CMD ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/VerifyDICOMMitkImageDump) +# Alternative definition of VERIFY_DUMP_CMD +# Comment in to use the DumpDICOMMitkImage app to use the tests to produce new reference dumps +#set(VERIFY_DUMP_CMD ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DumpDICOMMitkImage) + set(CT_ABDOMEN_DIR ${MITK_DATA_DIR}/TinyCTAbdomen_DICOMReader) set(MR_HEART_DIR ${MITK_DATA_DIR}/3D+t-Heart) set(CT_TILT_HEAD_DIR ${MITK_DATA_DIR}/TiltHead) set(CT_TILT_DIR ${MITK_DATA_DIR}/TiltedData) set(SOP_CLASSES_DIR ${MITK_DATA_DIR}/DICOMReader) # these variables could be passed as parameters to a generic test creation function set(TESTS_DIR Tests) set(INPUT_LISTNAME input) set(EXPECTED_DUMP expected.dump) -function(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) +function(AddDicomTestsFromDataRepository CURRENT_DATASET_NAME CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) # find all test input lists file(GLOB_RECURSE allInputs ${CURRENT_DATASET_DIR}/${TESTS_DIR}/*/${INPUT_LISTNAME}) function(expectFileExists filename) if(NOT EXISTS ${filename}) message(SEND_ERROR "Test case expected file ${filename} which does not exist! Please fix your CMake code or file layout.") endif(NOT EXISTS ${filename}) endfunction(expectFileExists) foreach(input ${allInputs}) # extract only the name of the directory of this very test case string(REGEX REPLACE ".*${TESTS_DIR}/([^/]+)/.*" "\\1" input ${input}) set(inputfilelist "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${INPUT_LISTNAME}") set(expecteddump "${CURRENT_DATASET_DIR}/${TESTS_DIR}/${input}/${EXPECTED_DUMP}") - set(test_name "DICOM_Load_${input}") + set(test_name "DICOM_Load_${CURRENT_DATASET_NAME}_${input}") #message(STATUS "DICOM loading test case '${input}'") expectFileExists(${inputfilelist}) expectFileExists(${expecteddump}) # TODO provide error messages if input not valid set(dicomFiles) # clear list # read list of file names from file "input" file(STRINGS ${inputfilelist} rawDicomFiles) foreach(raw ${rawDicomFiles}) # prepend each file with an abosolute path set(fileWithFullPath ${CURRENT_DATASET_DIR}/${raw}) list(APPEND dicomFiles ${fileWithFullPath}) endforeach(raw ${rawDicomFiles}) #message(STATUS " Load ${dicomFiles}") add_test(${test_name} ${VERIFY_DUMP_CMD} ${expecteddump} ${dicomFiles}) mitkFunctionGetLibrarySearchPaths(MITK_RUNTIME_PATH_RELEASE release RELEASE) mitkFunctionGetLibrarySearchPaths(MITK_RUNTIME_PATH_DEBUG debug DEBUG) set(test_env_path ${MITK_RUNTIME_PATH_RELEASE} ${MITK_RUNTIME_PATH_DEBUG} $ENV{PATH}) list(REMOVE_DUPLICATES test_env_path) string (REGEX REPLACE "\;" "\\\;" test_env_path "${test_env_path}") set_property(TEST ${test_name} PROPERTY ENVIRONMENT "PATH=${test_env_path}" APPEND) set_property(TEST ${test_name} PROPERTY SKIP_RETURN_CODE 77) endforeach(input allInputs) endfunction(AddDicomTestsFromDataRepository CURRENT_DATASET_DIR TESTS_DIR INPUT_LISTNAME EXPECTED_DUMP) -AddDicomTestsFromDataRepository(${CT_ABDOMEN_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${CT_TILT_HEAD_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${CT_TILT_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${MR_HEART_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDicomTestsFromDataRepository(TinyCTAbdomen_DICOMReader ${CT_ABDOMEN_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDicomTestsFromDataRepository(TiltHead ${CT_TILT_HEAD_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDicomTestsFromDataRepository(TiltedData ${CT_TILT_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDicomTestsFromDataRepository(3D+t-Heart ${MR_HEART_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) -AddDicomTestsFromDataRepository(${SOP_CLASSES_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +AddDicomTestsFromDataRepository(DICOMReader ${SOP_CLASSES_DIR} ${TESTS_DIR} ${INPUT_LISTNAME} ${EXPECTED_DUMP}) +############################################################### +# Test group 4 # use one more realistic series for testing property lists file(GLOB_RECURSE abdomenImages ${CT_ABDOMEN_DIR}/14?) # this is just one small volume mitkAddCustomModuleTest(mitkDICOMPreloadedVolumeTest_Abdomen mitkDICOMPreloadedVolumeTest ${abdomenImages}) diff --git a/Modules/MatchPointRegistration/cmdapps/MatchImage.cpp b/Modules/MatchPointRegistration/cmdapps/MatchImage.cpp index 7e32fa9b6a..9bf126b717 100644 --- a/Modules/MatchPointRegistration/cmdapps/MatchImage.cpp +++ b/Modules/MatchPointRegistration/cmdapps/MatchImage.cpp @@ -1,482 +1,482 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "mitkCommandLineParser.h" #include #include #include #include #include #include #include // MatchPoint #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct Settings { std::string movingFileName = ""; std::string targetFileName = ""; std::string outFileName = ""; std::string algFileName = ""; std::string parameters = ""; }; void SetupParser(mitkCommandLineParser& parser) { parser.setTitle("Match Image"); parser.setCategory("Registration Tools"); parser.setDescription(""); parser.setContributor("MIC, German Cancer Research Center (DKFZ)"); parser.setArgumentPrefix("--", "-"); // Add command line argument names parser.beginGroup("Required I/O parameters"); parser.addArgument( "moving", "m", mitkCommandLineParser::File, "Moving image files", "Path to the data that should be registered into the target space.", us::Any(), false, false, false, mitkCommandLineParser::Input); parser.addArgument( "target", "t", mitkCommandLineParser::File, "Tareget image files", "Path to the data that should be the target data on which the moving data should be registered.", us::Any(), false, false, false, mitkCommandLineParser::Input); parser.addArgument( "algorithm", "a", mitkCommandLineParser::File, "Registration algorithm", "Path to the registration algorithm that should be used for registration.", us::Any(), false, false, false, mitkCommandLineParser::Input); parser.addArgument("output", "o", mitkCommandLineParser::File, "Output file path", "Path to the generated registration.", us::Any(), false, false, false, mitkCommandLineParser::Output); parser.endGroup(); parser.beginGroup("Optional parameters"); parser.addArgument( "parameters", "p", mitkCommandLineParser::String, "Parameters", "Json string containing a json object that contains the parameters that should be passed to the algorithm as key value pairs."); parser.addArgument("help", "h", mitkCommandLineParser::Bool, "Help:", "Show this help text"); parser.endGroup(); } bool ConfigureApplicationSettings(std::map parsedArgs, Settings& settings) { try { if (parsedArgs.size() == 0) return false; settings.movingFileName = us::any_cast(parsedArgs["moving"]); settings.targetFileName = us::any_cast(parsedArgs["target"]); settings.outFileName = us::any_cast(parsedArgs["output"]); settings.algFileName = us::any_cast(parsedArgs["algorithm"]); if (parsedArgs.count("parameters") > 0) { settings.parameters = us::any_cast(parsedArgs["parameters"]); } } catch (...) { return false; } return true; } map::deployment::RegistrationAlgorithmBasePointer loadAlgorithm(const Settings& settings) { map::deployment::RegistrationAlgorithmBasePointer spAlgorithmBase = nullptr; std::cout << std::endl << "Load registration algorithm..." << std::endl; map::deployment::DLLHandle::Pointer spHandle = nullptr; spHandle = map::deployment::openDeploymentDLL(settings.algFileName); if (spHandle.IsNull()) { mapDefaultExceptionStaticMacro(<< "Cannot open deployed registration algorithm file."); } std::cout << "... library opened..." << std::endl; std::cout << "Algorithm information: " << std::endl; spHandle->getAlgorithmUID().Print(std::cout, 2); std::cout << std::endl; //Now load the algorithm from DLL spAlgorithmBase = map::deployment::getRegistrationAlgorithm(spHandle); if (spAlgorithmBase.IsNotNull()) { std::cout << "... done" << std::endl << std::endl; } else { mapDefaultExceptionStaticMacro(<< "Cannot create algorithm instance"); } return spAlgorithmBase; }; mitk::Image::Pointer ExtractFirstFrame(const mitk::Image* dynamicImage) { mitk::ImageTimeSelector::Pointer imageTimeSelector = mitk::ImageTimeSelector::New(); imageTimeSelector->SetInput(dynamicImage); imageTimeSelector->SetTimeNr(0); imageTimeSelector->UpdateLargestPossibleRegion(); return imageTimeSelector->GetOutput(); } template map::core::MetaPropertyBase::Pointer CheckCastAndSetProp(const nlohmann::json& value) { map::core::MetaPropertyBase::Pointer prop; try { const auto castedValue = value.get(); prop = map::core::MetaProperty::New(castedValue).GetPointer(); } catch (const std::exception& e) { MITK_ERROR << "Cannot convert value \"" << value << "\" into type: " << typeid(TValueType).name() << ". Details: " << e.what(); } catch (...) { - MITK_ERROR << "Unkown error. Cannot convert value \"" << value << "\" into type: " << typeid(TValueType).name(); + MITK_ERROR << "Unknown error. Cannot convert value \"" << value << "\" into type: " << typeid(TValueType).name(); } return prop; }; template map::core::MetaPropertyBase::Pointer CheckCastAndSetItkArrayProp(const nlohmann::json& valueSequence) { using ArrayType = ::itk::Array; ArrayType castedValue; map::core::MetaPropertyBase::Pointer prop; try { castedValue.SetSize(valueSequence.size()); typename ::itk::Array::SizeValueType index = 0; for (const auto& element : valueSequence) { const auto castedElement = element.template get(); castedValue[index] = castedElement; } prop = map::core::MetaProperty<::itk::Array>::New(castedValue).GetPointer(); } catch (const std::exception& e) { MITK_ERROR << "Cannot convert value \"" << valueSequence << "\" into type: " << typeid(ArrayType).name() << ". Details: " << e.what(); } catch (...) { - MITK_ERROR << "Unkown error. Cannot convert value \"" << valueSequence << "\" into type: " << typeid(ArrayType).name(); + MITK_ERROR << "Unknown error. Cannot convert value \"" << valueSequence << "\" into type: " << typeid(ArrayType).name(); } return prop; }; ::map::core::MetaPropertyBase::Pointer WrapIntoMetaProperty(const ::map::algorithm::MetaPropertyInfo* pInfo, const nlohmann::json& value) { map::core::MetaPropertyBase::Pointer metaProp; if (pInfo == nullptr) { return metaProp; } if (pInfo->getTypeInfo() == typeid(int)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(unsigned int)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(long)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(unsigned long)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(float)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(double)) { metaProp = CheckCastAndSetProp(value); } else if (pInfo->getTypeInfo() == typeid(::itk::Array)) { metaProp = CheckCastAndSetItkArrayProp< double >(value); } else if (pInfo->getTypeInfo() == typeid(bool)) { metaProp = CheckCastAndSetProp< bool >(value); } else if (pInfo->getTypeInfo() == typeid(::map::core::String)) { metaProp = map::core::MetaProperty::New(value).GetPointer(); } return metaProp; }; void OnMapAlgorithmEvent(::itk::Object*, const itk::EventObject& event, void*) { const map::events::AlgorithmEvent* pAlgEvent = dynamic_cast(&event); const map::events::AlgorithmWrapperEvent* pWrapEvent = dynamic_cast(&event); const map::events::InitializingAlgorithmEvent* pInitEvent = dynamic_cast(&event); const map::events::StartingAlgorithmEvent* pStartEvent = dynamic_cast(&event); const map::events::StoppingAlgorithmEvent* pStoppingEvent = dynamic_cast(&event); const map::events::StoppedAlgorithmEvent* pStoppedEvent = dynamic_cast(&event); const map::events::FinalizingAlgorithmEvent* pFinalizingEvent = dynamic_cast(&event); const map::events::FinalizedAlgorithmEvent* pFinalizedEvent = dynamic_cast(&event); if (pInitEvent) { std::cout <<"Initializing algorithm ..." << std::endl; } else if (pStartEvent) { std::cout <<"Starting algorithm ..." << std::endl; } else if (pStoppingEvent) { std::cout <<"Stopping algorithm ..." << std::endl; } else if (pStoppedEvent) { std::cout <<"Stopped algorithm ..." << std::endl; if (!pStoppedEvent->getComment().empty()) { std::cout <<"Stopping condition: " << pStoppedEvent->getComment() << std::endl; } } else if (pFinalizingEvent) { std::cout <<"Finalizing algorithm and results ..." << std::endl; } else if (pFinalizedEvent) { std::cout <<"Finalized algorithm ..." << std::endl; } else if (pAlgEvent && !pWrapEvent) { std::cout << pAlgEvent->getComment() << std::endl; } } int main(int argc, char* argv[]) { std::cout << "MitkMatchImage - Generic light weight image registration tool based on MatchPoint." << std::endl; Settings settings; mitkCommandLineParser parser; SetupParser(parser); const std::map& parsedArgs = parser.parseArguments(argc, argv); if (!ConfigureApplicationSettings(parsedArgs, settings)) { MITK_ERROR << "Command line arguments are invalid. To see the correct usage please call with -h or --help to show the help information."; return EXIT_FAILURE; }; // Show a help message if (parsedArgs.count("help") || parsedArgs.count("h")) { std::cout << parser.helpText(); return EXIT_SUCCESS; } std::cout << std::endl << "*******************************************" << std::endl; std::cout << "Moving file: " << settings.movingFileName << std::endl; std::cout << "Target file: " << settings.targetFileName << std::endl; std::cout << "Output file: " << settings.outFileName << std::endl; std::cout << "Algorithm location: " << settings.algFileName << std::endl; try { auto algorithm = loadAlgorithm(settings); auto command = ::itk::CStyleCommand::New(); command->SetCallback(OnMapAlgorithmEvent); algorithm->AddObserver(::map::events::AlgorithmEvent(), command); auto metaPropInterface = dynamic_cast(algorithm.GetPointer()); if (!settings.parameters.empty()) { if (nullptr == metaPropInterface) { MITK_WARN << "loaded algorithm does not support custom parameterization. Passed user parameters are ignored."; } else { nlohmann::json paramMap; std::string parseError = ""; try { paramMap = nlohmann::json::parse(settings.parameters); } catch (const std::exception& e) { parseError = e.what(); } if (!parseError.empty()) { mitkThrow() << "Cannot parametrize algorithm. Passed JSON parameter string seems to be invalid. Passed string: \"" << settings.parameters << "\". Error details: " << parseError; } std::cout << "Configuring algorithm with user specified parameters ..." << std::endl; for (const auto& [key, val] : paramMap.items()) { const auto info = metaPropInterface->getPropertyInfo(key); if (info.IsNotNull()) { if (info->isWritable()) { std::cout << "Set meta property: " << key << " = " << val << std::endl; ::map::core::MetaPropertyBase::Pointer prop = WrapIntoMetaProperty(info, val); if (prop.IsNull()) { mitkThrow() << "Error. Cannot set specified meta property. Type conversion is not supported or value cannot be converted into type. Property name: " << info->getName() << "; property type: " << info->getTypeName(); } else { metaPropInterface->setProperty(key, prop); } } else { mitkThrow() << "Cannot parametrize algorithm. A passed parameter is not writable for the algorithm. Violating parameter: \"" << key << "\"."; } } else { auto knownProps = metaPropInterface->getPropertyInfos(); std::ostringstream knownPropsNameString; for (const auto& knownProp : knownProps) { knownPropsNameString << knownProp->getName() << "; "; } - mitkThrow() << "Cannot parametrize algorithm. A parameter is unkown to algorithm. Unkown passed parameter: \"" << key << "\". Known parameters: " << knownPropsNameString.str(); + mitkThrow() << "Cannot parametrize algorithm. A parameter is unkown to algorithm. Unknown passed parameter: \"" << key << "\". Known parameters: " << knownPropsNameString.str(); } } } } std::cout << "Load moving data..." << std::endl; auto movingImage = mitk::IOUtil::Load(settings.movingFileName); if (movingImage.IsNull()) { MITK_ERROR << "Cannot load moving image."; return EXIT_FAILURE; } if (movingImage->GetTimeSteps() > 1) { movingImage = mitk::SelectImageByTimeStep(movingImage, 0)->Clone(); //we have to clone because SelectImageByTimeStep //only generates as new view of the data and we //are overwriting the only smartpointer to the source. std::cout << "Moving image has multiple time steps. Use first time step for registration." << std::endl; } std::cout << "Load target data..." << std::endl; auto targetImage = mitk::IOUtil::Load(settings.targetFileName); if (targetImage.IsNull()) { MITK_ERROR << "Cannot load target image."; return EXIT_FAILURE; } if (targetImage->GetTimeSteps() > 1) { targetImage = mitk::SelectImageByTimeStep(targetImage, 0)->Clone(); //we have to clone because SelectImageByTimeStep //only generates as new view of the data and we //are overwriting the only smartpointer to the source. std::cout << "Target image has multiple time steps. Use first time step for registration." << std::endl; } std::cout << "Start registration...." << std::endl; mitk::MAPAlgorithmHelper helper(algorithm); helper.SetData(movingImage, targetImage); ::itk::StdStreamLogOutput::Pointer spStreamLogOutput = ::itk::StdStreamLogOutput::New(); spStreamLogOutput->SetStream(std::cout); map::core::Logbook::addAdditionalLogOutput(spStreamLogOutput); auto registration = helper.GetRegistration(); // wrap the registration in a data node if (registration.IsNull()) { MITK_ERROR << "No valid registration generated"; return EXIT_FAILURE; } auto regWrapper = mitk::MAPRegistrationWrapper::New(registration); std::cout << "Store registration...." << std::endl; mitk::IOUtil::Save(regWrapper, settings.outFileName); } catch (const std::exception& e) { MITK_ERROR << e.what(); return EXIT_FAILURE; } catch (...) { MITK_ERROR << "Unexpected error encountered."; return EXIT_FAILURE; } return EXIT_SUCCESS; }