diff --git a/Modules/Core/CMakeLists.txt b/Modules/Core/CMakeLists.txt index a4e065c8c4..6f3655dbe9 100644 --- a/Modules/Core/CMakeLists.txt +++ b/Modules/Core/CMakeLists.txt @@ -1,74 +1,75 @@ set(TOOL_CPPS "") # temporary suppress warnings in the following files until image accessors are fully integrated. set_source_files_properties( src/DataManagement/mitkImage.cpp COMPILE_FLAGS -DMITK_NO_DEPRECATED_WARNINGS ) set_source_files_properties( src/Controllers/mitkSliceNavigationController.cpp COMPILE_FLAGS -DMITK_NO_DEPRECATED_WARNINGS ) mitk_create_module( INCLUDE_DIRS PUBLIC ${MITK_BINARY_DIR} PRIVATE src/Algorithms src/Controllers src/DataManagement src/Interactions src/IO src/Rendering DEPENDS PUBLIC mbilog CppMicroServices PACKAGE_DEPENDS PUBLIC Boost ITK|IOImageBase+SpatialObjects+Statistics #ITK|Statistics+Transform - VTK|FiltersTexture+FiltersParallel+ImagingStencil+ImagingMath+InteractionStyle+RenderingOpenGL2+RenderingVolumeOpenGL2+RenderingFreeType+RenderingLabel+InteractionWidgets+IOGeometry+IOXML + VTK|FiltersTexture+FiltersParallel+ImagingStencil+ImagingMath+InteractionStyle+RenderingOpenGL2+RenderingVolumeOpenGL2+RenderingFreeType+RenderingLabel+InteractionWidgets+IOGeometry+IOImage+IOXML PRIVATE ITK|IOBioRad+IOBMP+IOBruker+IOCSV+IOGDCM+IOGE+IOGIPL+IOHDF5+IOIPL+IOJPEG+IOJPEG2000+IOLSM+IOMesh+IOMeta+IOMINC+IOMRC+IONIFTI+IONRRD+IOPNG+IOSiemens+IOSpatialObjects+IOStimulate+IOTIFF+IOTransformBase+IOTransformHDF5+IOTransformInsightLegacy+IOTransformMatlab+IOVTK+IOXML + Poco|Foundation nlohmann_json tinyxml2 ${optional_private_package_depends} # Do not automatically create CppMicroServices initialization code. # Because the VTK "auto-init" functionality injects file-local static # initialization code in every cpp file which includes a VTK header, # static initialization order becomes an issue again. For the Mitk # core library, we need to ensure that the VTK static initialization stuff # happens before the CppMicroServices initialization, since the latter # might already use VTK code which needs to access VTK object factories. # Hence, CppMicroServices initialization code is placed manually within # the mitkCoreActivator.cpp file. NO_INIT ) if(NOT TARGET ${MODULE_TARGET}) message(SEND_ERROR "Core target ${MODULE_TARGET} does not exist") endif() function(_itk_create_factory_register_manager) # In MITK_ITK_Config.cmake, we do *not* include ITK_USE_FILE, which # prevents multiple registrations/unregistrations of ITK IO factories # during library loading/unloading (of MITK libraries). However, we need # "one" place where the IO factories are registered at # least once. This could be the application executable, but every executable would # need to take care of that itself. Instead, we allow the auto registration in the # Mitk Core library. set(NO_DIRECTORY_SCOPED_ITK_COMPILE_DEFINITION 1) find_package(ITK) include(${ITK_USE_FILE}) if(NOT ITK_NO_IO_FACTORY_REGISTER_MANAGER) # We manually add the define which will be of target scope. MITK # patches ITK_USE_FILE to remove the directory scoped compile # definition since it would be propagated to other targets in the # same directory scope but these targets might want to *not* # use the ITK factory manager stuff. target_compile_definitions(${MODULE_TARGET} PRIVATE ITK_IO_FACTORY_REGISTER_MANAGER) endif() endfunction() _itk_create_factory_register_manager() if(BUILD_TESTING) add_subdirectory(TestingHelper) add_subdirectory(test) endif() diff --git a/Modules/Core/files.cmake b/Modules/Core/files.cmake index eb40ccde9d..cff9510c3c 100644 --- a/Modules/Core/files.cmake +++ b/Modules/Core/files.cmake @@ -1,327 +1,328 @@ 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/mitkUndoController.cpp Controllers/mitkVerboseLimitedLinearUndo.cpp Controllers/mitkVtkLayerController.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/mitkIPropertyExtensions.cpp DataManagement/mitkIPropertyFilters.cpp DataManagement/mitkIPropertyOwner.cpp DataManagement/mitkIPropertyPersistence.cpp DataManagement/mitkIPropertyProvider.cpp DataManagement/mitkLandmarkProjectorBasedCurvedGeometry.cpp DataManagement/mitkLandmarkProjector.cpp DataManagement/mitkLevelWindow.cpp DataManagement/mitkLevelWindowManager.cpp DataManagement/mitkLevelWindowPreset.cpp DataManagement/mitkLevelWindowProperty.cpp DataManagement/mitkLine.cpp DataManagement/mitkLookupTable.cpp DataManagement/mitkLookupTableProperty.cpp DataManagement/mitkLookupTables.cpp # specializations of GenericLookupTable 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/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/mitkVector.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/mitkImageGenerator.cpp IO/mitkImageVtkLegacyIO.cpp IO/mitkImageVtkXmlIO.cpp IO/mitkIMimeTypeProvider.cpp IO/mitkIOConstants.cpp IO/mitkIOMimeTypes.cpp IO/mitkIOUtil.cpp IO/mitkItkImageIO.cpp IO/mitkItkLoggingAdapter.cpp IO/mitkLegacyFileReaderService.cpp IO/mitkLegacyFileWriterService.cpp IO/mitkLocaleSwitch.cpp IO/mitkLog.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/mitkVideoRecorder.h b/Modules/Core/include/mitkVideoRecorder.h new file mode 100644 index 0000000000..ee93447afb --- /dev/null +++ b/Modules/Core/include/mitkVideoRecorder.h @@ -0,0 +1,86 @@ +/*============================================================================ + +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 mitkVideoRecorder_h +#define mitkVideoRecorder_h + +#include +#include +#include + +#include + +namespace mitk +{ + /** \brief Record the contents of a render window as video using FFmpeg as external command-line application. + * + * Before recording, set the render window, the path to FFmpeg, the path to the video output file, + * its format/codec, and frame rate. + * + * Most settings have decent defaults, e.g., the royalty-free and open VP9 video codec in a WebM container as + * output format and a frame rate of 30 frames per second. + * + * If not set explicitly, the FFmpeg path and output format are queried from the preferences, if available. + * + * Call StartRecording() to begin a recording session, record each frame with RecordFrame(), and end the recording + * session with a call to StopRecording(). StopRecording() is a blocking call that may take a long time to return + * since it calls FFmpeg to encode the recorded frames into a video. Consider calling it from a separate thread. + * + * The VideoRecorder throws an Exception on any error. It is advised to use it within a try/catch block. + */ + class MITKCORE_EXPORT VideoRecorder + { + public: + enum class OutputFormat + { + WebM_VP9, + MP4_H264 + }; + + /** \brief Get the file extension corresponding to the specified video output format. + * + * \return A file extension string like ".webm" or ".mp4". + */ + static std::string GetFileExtension(OutputFormat format); + + VideoRecorder(); + ~VideoRecorder(); + + VideoRecorder(const VideoRecorder&) = delete; + VideoRecorder& operator=(const VideoRecorder&) = delete; + + std::filesystem::path GetFFmpegPath() const; + void SetFFmpegPath(const std::filesystem::path& path); + + std::filesystem::path GetOutputPath() const; + void SetOutputPath(const std::filesystem::path& path); + + OutputFormat GetOutputFormat() const; + void SetOutputFormat(OutputFormat format); + + std::string GetRenderWindowName() const; + void SetRenderWindowName(const std::string& renderWindowName); + + int GetFrameRate() const; + void SetFrameRate(unsigned int fps); + + void StartRecording(); + void RecordFrame() const; + int StopRecording(); + + private: + class Impl; + std::unique_ptr m_Impl; + }; +} + +#endif diff --git a/Modules/Core/src/Rendering/mitkVideoRecorder.cpp b/Modules/Core/src/Rendering/mitkVideoRecorder.cpp new file mode 100644 index 0000000000..f30ea29140 --- /dev/null +++ b/Modules/Core/src/Rendering/mitkVideoRecorder.cpp @@ -0,0 +1,360 @@ +/*============================================================================ + +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 +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +std::string mitk::VideoRecorder::GetFileExtension(OutputFormat format) +{ + switch (format) + { + case OutputFormat::WebM_VP9: + return ".webm"; + + case OutputFormat::MP4_H264: + return ".mp4"; + + default: + break; + } + + mitkThrow() << "Unknown output format for video recording."; +} + +namespace +{ + mitk::IPreferences* GetPreferences() + { + auto* preferencesService = mitk::CoreServices::GetPreferencesService(); + return preferencesService->GetSystemPreferences()->Node("org.mitk.views.moviemaker"); + } + + class RecordingSession + { + public: + RecordingSession(vtkRenderWindow* renderWindow, mitk::VideoRecorder::OutputFormat format) + : m_FrameDir(mitk::IOUtil::CreateTemporaryDirectory("MITK_RecordingSession_XXXXXX")), + m_NumberOfFrames(0) + { + m_WindowToImageFilter->SetInput(renderWindow); + + if (mitk::VideoRecorder::OutputFormat::MP4_H264 == format) + { + // H.264 only supports image dimensions that are a multiple of 2. Resize if necessary. + + auto* size = renderWindow->GetActualSize(); + + if (size[0] & 1 || size[1] & 1) + { + m_ImageResize->SetInputConnection(m_WindowToImageFilter->GetOutputPort()); + m_ImageResize->SetOutputDimensions(size[0] & ~1, size[1] & ~1, -1); + m_ImageResize->SetInterpolate(0); + m_ImageResize->BorderOn(); + + m_ImageWriter->SetInputConnection(m_ImageResize->GetOutputPort()); + return; + } + } + + m_ImageWriter->SetInputConnection(m_WindowToImageFilter->GetOutputPort()); + } + + ~RecordingSession() + { + std::error_code errorCode; + std::filesystem::remove_all(m_FrameDir, errorCode); + } + + RecordingSession(const RecordingSession&) = delete; + RecordingSession& operator=(const RecordingSession&) = delete; + + std::filesystem::path GetFrameDir() const + { + return m_FrameDir; + } + + void RecordFrame() + { + m_WindowToImageFilter->Modified(); + + std::stringstream frameFilename; + frameFilename << std::setw(6) << std::setfill('0') << m_NumberOfFrames << ".png"; + const auto framePath = m_FrameDir / frameFilename.str(); + + m_ImageWriter->SetFileName(framePath.string().c_str()); + m_ImageWriter->Write(); + + ++m_NumberOfFrames; + } + + private: + std::filesystem::path m_FrameDir; + unsigned int m_NumberOfFrames; + vtkNew m_WindowToImageFilter; + vtkNew m_ImageResize; + vtkNew m_ImageWriter; + }; +} + +namespace mitk +{ + class VideoRecorder::Impl + { + public: + Impl() + : m_FrameRate(30) + { + } + + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + std::filesystem::path GetFFmpegPath() const + { + if (m_FFmpegPath) + return m_FFmpegPath.value(); + + auto* preferences = GetPreferences(); + + if (nullptr != preferences) + { + auto ffmpegPath = preferences->Get("ffmpeg", ""); + + if (!ffmpegPath.empty()) + return ffmpegPath; + } + + return std::filesystem::path(); + } + + void SetFFmpegPath(const std::filesystem::path& path) + { + m_FFmpegPath = path; + } + + std::filesystem::path GetOutputPath() const + { + return m_OutputPath; + } + + void SetOutputPath(const std::filesystem::path& path) + { + m_OutputPath = path; + } + + mitk::VideoRecorder::OutputFormat GetOutputFormat() const + { + if (m_OutputFormat) + return m_OutputFormat.value(); + + auto* preferences = GetPreferences(); + + if (nullptr != preferences) + return static_cast(preferences->GetInt("format", 0)); + + return OutputFormat::WebM_VP9; + } + + void SetOutputFormat(OutputFormat format) + { + m_OutputFormat = format; + } + + std::string GetRenderWindowName() const + { + return m_RenderWindowName; + } + + void SetRenderWindowName(const std::string& renderWindowName) + { + m_RenderWindowName = renderWindowName; + } + + int GetFrameRate() const + { + return m_FrameRate; + } + + void SetFrameRate(unsigned int fps) + { + m_FrameRate = fps; + } + + bool OnAir() const + { + return nullptr != m_RecordingSession.get(); + } + + void StartRecording() + { + if (this->OnAir()) + mitkThrow() << "Recording session already running."; + + auto renderWindowName = this->GetRenderWindowName(); + + if (renderWindowName.empty()) + mitkThrow() << "No render window specified for recording."; + + auto* renderWindow = BaseRenderer::GetRenderWindowByName(renderWindowName); + + if (nullptr == renderWindow) + mitkThrow() << "\"" << renderWindowName << "\" references unknown render window for recording."; + + m_RecordingSession = std::make_unique(renderWindow, this->GetOutputFormat()); + } + + void RecordFrame() + { + if (!this->OnAir()) + mitkThrow() << "Cannot record frame. No recording session running."; + + m_RecordingSession->RecordFrame(); + } + + int StopRecording() + { + if (!this->OnAir()) + mitkThrow() << "No recording session running."; + + auto ffmpegPath = this->GetFFmpegPath(); + + if (ffmpegPath.empty()) + mitkThrow() << "Path to FFmpeg not set."; + + if (this->GetOutputPath().empty()) + mitkThrow() << "Path to output video file not set."; + + bool vp9 = OutputFormat::WebM_VP9 == this->GetOutputFormat(); + + Poco::Process::Args args = { + "-y", + "-r", std::to_string(this->GetFrameRate()), + "-i", "%6d.png", + "-c:v", vp9 ? "libvpx-vp9" : "libx264", + "-crf", vp9 ? "31" : "23", + "-pix_fmt", "yuv420p", + "-b:v", "0", + this->GetOutputPath().string() + }; + + auto processHandle = Poco::Process::launch(ffmpegPath.string(), args, m_RecordingSession->GetFrameDir().string()); + auto exitCode = processHandle.wait(); + + m_RecordingSession = nullptr; + + return exitCode; + } + + private: + std::optional m_FFmpegPath; + std::filesystem::path m_OutputPath; + std::optional m_OutputFormat; + std::string m_RenderWindowName; + unsigned int m_FrameRate; + std::unique_ptr m_RecordingSession; + }; +} + +mitk::VideoRecorder::VideoRecorder() + : m_Impl(std::make_unique()) +{ +} + +mitk::VideoRecorder::~VideoRecorder() +{ +} + +std::filesystem::path mitk::VideoRecorder::GetFFmpegPath() const +{ + return m_Impl->GetFFmpegPath(); +} + +void mitk::VideoRecorder::SetFFmpegPath(const std::filesystem::path& path) +{ + m_Impl->SetFFmpegPath(path); +} + +std::filesystem::path mitk::VideoRecorder::GetOutputPath() const +{ + return m_Impl->GetOutputPath(); +} + +void mitk::VideoRecorder::SetOutputPath(const std::filesystem::path& path) +{ + m_Impl->SetOutputPath(path); +} + +mitk::VideoRecorder::OutputFormat mitk::VideoRecorder::GetOutputFormat() const +{ + return m_Impl->GetOutputFormat(); +} + +void mitk::VideoRecorder::SetOutputFormat(OutputFormat format) +{ + m_Impl->SetOutputFormat(format); +} + +std::string mitk::VideoRecorder::GetRenderWindowName() const +{ + return m_Impl->GetRenderWindowName(); +} + +void mitk::VideoRecorder::SetRenderWindowName(const std::string& renderWindowName) +{ + m_Impl->SetRenderWindowName(renderWindowName); +} + +int mitk::VideoRecorder::GetFrameRate() const +{ + return m_Impl->GetFrameRate(); +} + +void mitk::VideoRecorder::SetFrameRate(unsigned int fps) +{ + m_Impl->SetFrameRate(fps); +} + +void mitk::VideoRecorder::StartRecording() +{ + m_Impl->StartRecording(); +} + +void mitk::VideoRecorder::RecordFrame() const +{ + m_Impl->RecordFrame(); +} + +int mitk::VideoRecorder::StopRecording() +{ + return m_Impl->StopRecording(); +} diff --git a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.cpp b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.cpp index cf26cce7b0..c9d0fb7efa 100644 --- a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.cpp +++ b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.cpp @@ -1,183 +1,129 @@ /*============================================================================ 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 #include #include "QmitkExternalProgramsPreferencePage.h" namespace { mitk::IPreferences* GetPreferences() { auto* preferencesService = mitk::CoreServices::GetPreferencesService(); return preferencesService->GetSystemPreferences()->Node("org.mitk.gui.qt.ext.externalprograms"); } } QmitkExternalProgramsPreferencePage::QmitkExternalProgramsPreferencePage() : m_Ui(new Ui::QmitkExternalProgramsPreferencePage), m_Control(nullptr), - m_FFmpegProcess(nullptr), m_GnuplotProcess(nullptr) { } QmitkExternalProgramsPreferencePage::~QmitkExternalProgramsPreferencePage() { } void QmitkExternalProgramsPreferencePage::CreateQtControl(QWidget* parent) { m_Control = new QWidget(parent); - m_FFmpegProcess = new QProcess(m_Control); m_GnuplotProcess = new QProcess(m_Control); m_Ui->setupUi(m_Control); - connect(m_FFmpegProcess, SIGNAL(error(QProcess::ProcessError)), this, SLOT(OnFFmpegProcessError(QProcess::ProcessError))); - connect(m_FFmpegProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(OnFFmpegProcessFinished(int, QProcess::ExitStatus))); - connect(m_Ui->ffmpegButton, SIGNAL(clicked()), this, SLOT(OnFFmpegButtonClicked())); - connect(m_GnuplotProcess, SIGNAL(error(QProcess::ProcessError)), this, SLOT(OnGnuplotProcessError(QProcess::ProcessError))); connect(m_GnuplotProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(OnGnuplotProcessFinished(int, QProcess::ExitStatus))); connect(m_Ui->gnuplotButton, SIGNAL(clicked()), this, SLOT(OnGnuplotButtonClicked())); this->Update(); } -void QmitkExternalProgramsPreferencePage::OnFFmpegButtonClicked() -{ - QString filter = "ffmpeg executable "; - -#if defined(WIN32) - filter += "(ffmpeg.exe)"; -#else - filter += "(ffmpeg)"; -#endif - - QString ffmpegPath = QFileDialog::getOpenFileName(m_Control, "FFmpeg", "", filter); - - if (!ffmpegPath.isEmpty()) - { - m_FFmpegPath = ffmpegPath; - m_FFmpegProcess->start(ffmpegPath, QStringList() << "-version", QProcess::ReadOnly); - } -} - -void QmitkExternalProgramsPreferencePage::OnFFmpegProcessError(QProcess::ProcessError) -{ - m_FFmpegPath.clear(); - m_Ui->ffmpegLineEdit->clear(); -} - -void QmitkExternalProgramsPreferencePage::OnFFmpegProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - if (exitStatus == QProcess::NormalExit && exitCode == 0) - { - QString output = QTextCodec::codecForName("UTF-8")->toUnicode(m_FFmpegProcess->readAllStandardOutput()); - - if (output.startsWith("ffmpeg")) - { - m_Ui->ffmpegLineEdit->setText(m_FFmpegPath); - return; - } - } - - m_FFmpegPath.clear(); - m_Ui->ffmpegLineEdit->clear(); -} - void QmitkExternalProgramsPreferencePage::OnGnuplotButtonClicked() { QString filter = "gnuplot executable "; #if defined(WIN32) filter += "(gnuplot.exe)"; #else filter += "(gnuplot)"; #endif QString gnuplotPath = QFileDialog::getOpenFileName(m_Control, "Gnuplot", "", filter); if (!gnuplotPath.isEmpty()) { m_GnuplotPath = gnuplotPath; m_GnuplotProcess->start(gnuplotPath, QStringList() << "--version", QProcess::ReadOnly); } } void QmitkExternalProgramsPreferencePage::OnGnuplotProcessError(QProcess::ProcessError) { m_GnuplotPath.clear(); m_Ui->gnuplotLineEdit->clear(); } void QmitkExternalProgramsPreferencePage::OnGnuplotProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::NormalExit && exitCode == 0) { QString output = QTextCodec::codecForName("UTF-8")->toUnicode(m_GnuplotProcess->readAllStandardOutput()); if (output.startsWith("gnuplot")) { m_Ui->gnuplotLineEdit->setText(m_GnuplotPath); return; } } m_GnuplotPath.clear(); m_Ui->gnuplotLineEdit->clear(); } QWidget* QmitkExternalProgramsPreferencePage::GetQtControl() const { return m_Control; } void QmitkExternalProgramsPreferencePage::Init(berry::IWorkbench::Pointer) { } void QmitkExternalProgramsPreferencePage::PerformCancel() { } bool QmitkExternalProgramsPreferencePage::PerformOk() { auto* prefs = GetPreferences(); - prefs->Put("ffmpeg", m_FFmpegPath.toStdString()); prefs->Put("gnuplot", m_GnuplotPath.toStdString()); return true; } void QmitkExternalProgramsPreferencePage::Update() { auto* prefs = GetPreferences(); - m_FFmpegPath = QString::fromStdString(prefs->Get("ffmpeg", "")); - - if (!m_FFmpegPath.isEmpty()) - m_FFmpegProcess->start(m_FFmpegPath, QStringList() << "-version", QProcess::ReadOnly); - m_GnuplotPath = QString::fromStdString(prefs->Get("gnuplot", "")); if (!m_GnuplotPath.isEmpty()) m_GnuplotProcess->start(m_GnuplotPath, QStringList() << "--version", QProcess::ReadOnly); } diff --git a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h index a27574642b..2457e3b42f 100644 --- a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h +++ b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h @@ -1,61 +1,54 @@ /*============================================================================ 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 QmitkExternalProgramsPreferencePage_h #define QmitkExternalProgramsPreferencePage_h #include #include #include namespace Ui { class QmitkExternalProgramsPreferencePage; } class QmitkExternalProgramsPreferencePage : public QObject, public berry::IQtPreferencePage { Q_OBJECT Q_INTERFACES(berry::IPreferencePage) public: QmitkExternalProgramsPreferencePage(); ~QmitkExternalProgramsPreferencePage() override; void CreateQtControl(QWidget* parent) override; QWidget* GetQtControl() const override; void Init(berry::IWorkbench::Pointer) override; void PerformCancel() override; bool PerformOk() override; void Update() override; private slots: - void OnFFmpegButtonClicked(); - void OnFFmpegProcessError(QProcess::ProcessError error); - void OnFFmpegProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - void OnGnuplotButtonClicked(); void OnGnuplotProcessError(QProcess::ProcessError error); void OnGnuplotProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); private: QScopedPointer m_Ui; QWidget* m_Control; - QProcess* m_FFmpegProcess; - QString m_FFmpegPath; - QProcess* m_GnuplotProcess; QString m_GnuplotPath; }; #endif diff --git a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.ui b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.ui index cc527fdf25..69f491a9e4 100644 --- a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.ui +++ b/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.ui @@ -1,92 +1,52 @@ QmitkExternalProgramsPreferencePage 0 0 400 - 300 + 152 External Programs - - - - - - - FFmpeg: - - - Qt::PlainText - - - gnuplotButton - - - - - - - true - - - - - - - ... - - - - - - - gnuplot: - - - Qt::PlainText - - - gnuplotButton - - - - + + + + + gnuplot: + + + Qt::PlainText + + + gnuplotButton + + + + + + true - + ... - - - - Qt::Vertical - - - - 20 - 248 - - - - diff --git a/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker.dox b/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker.dox index 474d73a10f..e36ba450a7 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker.dox +++ b/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker.dox @@ -1,64 +1,67 @@ /** \page org_mitk_views_moviemaker The Movie Maker View \imageMacro{moviemakericon_black.svg,"Icon of the Movie Maker Plugin.",2.00} \tableofcontents \section org_mitk_views_moviemakerOverview Overview The Movie Maker View allows you to create basic animations of your scene and to record them to video files. -Individual animations are arranged in a timeline and can be played back sequential or in parallel. +Individual animations are arranged in a timeline and can be played back sequentially or in parallel. -The Movie Maker View uses the external FFmpeg command line utility to write compressed video files. +The Movie Maker View uses the external FFmpeg command-line application to write compressed video files. -You have to manually install FFmpeg and set the corresponding path in "External Programs" in the MITK Workbench Preferences (Ctrl+P) in order to record your movies to video files. +You must install FFmpeg and set its corresponding path in the "Movie Maker" preferences (Ctrl+P) to be able to record your movies to video files. -\imageMacro{QmitkMovieMaker_Preferences.png,"The External Programs preferences page.",12.00} +\imageMacro{QmitkMovieMaker_Preferences.png,"The Movie Maker preferences page.",12.00} \section org_mitk_views_moviemakerUsage Usage \imageMacro{QmitkMovieMaker_MovieMakerView.png,"The Movie Maker View.",16.00} -To create a movie you have to add an animation to the timeline by clicking the "Add animation" button. +To create a movie, add an animation to the timeline by clicking the "Add animation" button. You can choose between the available types of animations, e.g., Orbit or Slice. -The timeline surrounding bottons allow you to arrange, remove, or add further animations to your movie. +The bottons sourroundng the timeline allow you to arrange, remove, or add further animations to your movie. -Each animation can be set to either begin with the previous animation, i.e., run in parallel, or to start after the previous animation, i.e., run sequential. +Each animation can be set to either begin with the previous animation, i.e., run in parallel, or to start after the previous animation, i.e., run sequentially. In combination with delays, rather complex animation arrangements are possible. To set animation specific parameters, select the corresponding animation in the timeline first. -You can play back, pause and stop your movie with the according controls at the bottom of the Movie Maker View. -Click the "Record" button to finally record your movie to a video file with the specified number of frames per second. -You have to choose the render window which you want to record. +You can play back, pause and stop your movie with the controls at the bottom of the Movie Maker View. +Click the "Record" button to finally record your movie to a video file with the specified frame rate resp. number of frames per second. +Choose the render window that you want to record. + +By default, movies are recorded into *.webm files using the open and royalty-free VP9 video codec. +You can switch to the widespread *.mp4 file format using the non-free H.264 video codec in the Movie Maker preferences. \subsection org_mitk_views_moviemakerOrbitUsage Orbit Animation The Orbit animation rotates the camera in the 3D window around the scene. Align the camera directly in the 3D window and enter the number of degrees for the orbitting. If you are planning to have a specific view in the middle of your movie you can play the movie and pause it at the specific frame of interest. Adjust the camera in the 3D window and restart the animation. \imageMacro{QmitkMovieMaker_Orbit.png,"The Orbit animation.",12.00} \subsection org_mitk_views_moviemakerSliceUsage Slice Animation The Slice animation slices through an image. You can choose the image plane (axial, sagittal, or coronal), as well as the start and end points of the slicing. -Use the image navigator in the bottom left of the Workbench to get an idea of the desired values. -Check "Reverse" in order to slice from the higher slice number to the lower slice number. +Look at the Image Navigator View to get an idea of the desired values. +Toggle "Reverse" to slice from the higher slice number to the lower slice number. \imageMacro{QmitkMovieMaker_Slice.png,"The Slice animation.",12.00} \subsection org_mitk_views_moviemakerTimeUsage Time Animation The Time animation steps through the individual time steps of the current scene. You can specify the range of the animated time steps. -Use the image navigator in the bottom left of the Workbench to get an idea of the desired values. -Check "Reverse" in order to step from later time steps to previous time steps. +Look at the Image Navigator View to get an idea of the desired values. +Toggle "Reverse" to step from later time steps to previous time steps. \imageMacro{QmitkMovieMaker_Time.gif,"The Time animation.",12.00} */ diff --git a/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker_Preferences.png b/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker_Preferences.png index 5324b909fa..4acebf94d7 100644 Binary files a/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker_Preferences.png and b/Plugins/org.mitk.gui.qt.moviemaker/documentation/UserManual/QmitkMovieMaker_Preferences.png differ diff --git a/Plugins/org.mitk.gui.qt.moviemaker/files.cmake b/Plugins/org.mitk.gui.qt.moviemaker/files.cmake index 16a46cc729..3b175ebe33 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/files.cmake +++ b/Plugins/org.mitk.gui.qt.moviemaker/files.cmake @@ -1,55 +1,58 @@ set(SRC_CPP_FILES ) set(INTERNAL_CPP_FILES QmitkAnimationItem.cpp QmitkAnimationItemDelegate.cpp QmitkAnimationWidget.cpp + QmitkMovieMakerPreferencePage.cpp QmitkMovieMakerView.cpp QmitkOrbitAnimationItem.cpp QmitkOrbitAnimationWidget.cpp QmitkSliceAnimationItem.cpp QmitkSliceAnimationWidget.cpp QmitkTimeSliceAnimationItem.cpp QmitkTimeSliceAnimationWidget.cpp mitkMovieMakerPluginActivator.cpp QmitkScreenshotMaker.cpp ) set(UI_FILES + src/internal/QmitkMovieMakerPreferencePage.ui src/internal/QmitkMovieMakerView.ui src/internal/QmitkOrbitAnimationWidget.ui src/internal/QmitkSliceAnimationWidget.ui src/internal/QmitkTimeSliceAnimationWidget.ui src/internal/QmitkScreenshotMakerControls.ui ) set(MOC_H_FILES src/internal/mitkMovieMakerPluginActivator.h src/internal/QmitkAnimationItemDelegate.h src/internal/QmitkAnimationWidget.h + src/internal/QmitkMovieMakerPreferencePage.h src/internal/QmitkMovieMakerView.h src/internal/QmitkOrbitAnimationWidget.h src/internal/QmitkSliceAnimationWidget.h src/internal/QmitkTimeSliceAnimationWidget.h src/internal/QmitkScreenshotMaker.h ) set(CACHED_RESOURCE_FILES resources/video-camera.svg resources/camera.svg plugin.xml ) set(QRC_FILES resources/QmitkMovieMaker.qrc ) foreach(file ${SRC_CPP_FILES}) set(CPP_FILES ${CPP_FILES} src/${file}) endforeach(file ${SRC_CPP_FILES}) foreach(file ${INTERNAL_CPP_FILES}) set(CPP_FILES ${CPP_FILES} src/internal/${file}) endforeach(file ${INTERNAL_CPP_FILES}) diff --git a/Plugins/org.mitk.gui.qt.moviemaker/plugin.xml b/Plugins/org.mitk.gui.qt.moviemaker/plugin.xml index df7bbc34a0..8900ec38a3 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/plugin.xml +++ b/Plugins/org.mitk.gui.qt.moviemaker/plugin.xml @@ -1,48 +1,52 @@ Take movies of your data + + + + Take screenshots of your data diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.cpp b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.cpp new file mode 100644 index 0000000000..5a598806e4 --- /dev/null +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.cpp @@ -0,0 +1,142 @@ +/*============================================================================ + +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 "QmitkMovieMakerPreferencePage.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace +{ + mitk::IPreferences* GetPreferences() + { + auto* preferencesService = mitk::CoreServices::GetPreferencesService(); + return preferencesService->GetSystemPreferences()->Node("org.mitk.views.moviemaker"); + } +} + +QmitkMovieMakerPreferencePage::QmitkMovieMakerPreferencePage() + : m_Ui(new Ui::QmitkMovieMakerPreferencePage), + m_Control(nullptr), + m_FFmpegProcess(nullptr) +{ +} + +QmitkMovieMakerPreferencePage::~QmitkMovieMakerPreferencePage() +{ +} + +void QmitkMovieMakerPreferencePage::Init(berry::IWorkbench::Pointer) +{ +} + +void QmitkMovieMakerPreferencePage::CreateQtControl(QWidget* parent) +{ + using Self = QmitkMovieMakerPreferencePage; + + m_Control = new QWidget(parent); + + m_Ui->setupUi(m_Control); + + m_Ui->formatButtonGroup->setId(m_Ui->vp9Button, static_cast(mitk::VideoRecorder::OutputFormat::WebM_VP9)); + m_Ui->formatButtonGroup->setId(m_Ui->h264Button, static_cast(mitk::VideoRecorder::OutputFormat::MP4_H264)); + + connect(m_Ui->ffmpegButton, &QToolButton::clicked, this, &Self::OnFFmpegButtonClicked); + + m_FFmpegProcess = new QProcess(m_Control); + + connect(m_FFmpegProcess, qOverload(&QProcess::error), this, &Self::OnFFmpegProcessError); + connect(m_FFmpegProcess, qOverload(&QProcess::finished), this, &Self::OnFFmpegProcessFinished); + + this->Update(); +} + +QWidget* QmitkMovieMakerPreferencePage::GetQtControl() const +{ + return m_Control; +} + +bool QmitkMovieMakerPreferencePage::PerformOk() +{ + auto* prefs = GetPreferences(); + + prefs->Put("ffmpeg", m_Ui->ffmpegLineEdit->text().toStdString()); + prefs->PutInt("format", m_Ui->formatButtonGroup->checkedId()); + + return true; +} + +void QmitkMovieMakerPreferencePage::PerformCancel() +{ +} + +void QmitkMovieMakerPreferencePage::Update() +{ + auto* prefs = GetPreferences(); + + m_FFmpegPath = QString::fromStdString(prefs->Get("ffmpeg", "")); + + if (!m_FFmpegPath.isEmpty()) + m_FFmpegProcess->start(m_FFmpegPath, QStringList() << "-version", QProcess::ReadOnly); + + m_Ui->formatButtonGroup->button(prefs->GetInt("format", 0))->setChecked(true); +} + +void QmitkMovieMakerPreferencePage::OnFFmpegButtonClicked() +{ + QString filter = "FFmpeg executable "; + +#if defined(WIN32) + filter += "(ffmpeg.exe)"; +#else + filter += "(ffmpeg)"; +#endif + + auto ffmpegPath = QFileDialog::getOpenFileName(m_Control, "FFmpeg", "", filter); + + if (!ffmpegPath.isEmpty()) + { + m_FFmpegPath = ffmpegPath; + m_FFmpegProcess->start(ffmpegPath, QStringList() << "-version", QProcess::ReadOnly); + } +} + +void QmitkMovieMakerPreferencePage::OnFFmpegProcessError(QProcess::ProcessError) +{ + m_FFmpegPath.clear(); + m_Ui->ffmpegLineEdit->clear(); +} + +void QmitkMovieMakerPreferencePage::OnFFmpegProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitStatus == QProcess::NormalExit && exitCode == 0) + { + auto ffmpegOutput = QTextCodec::codecForName("UTF-8")->toUnicode(m_FFmpegProcess->readAllStandardOutput()); + + if (ffmpegOutput.startsWith("ffmpeg")) + { + m_Ui->ffmpegLineEdit->setText(m_FFmpegPath); + return; + } + } + + m_FFmpegPath.clear(); + m_Ui->ffmpegLineEdit->clear(); +} diff --git a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.h similarity index 58% copy from Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h copy to Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.h index a27574642b..da91081012 100644 --- a/Plugins/org.mitk.gui.qt.ext/src/internal/QmitkExternalProgramsPreferencePage.h +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.h @@ -1,61 +1,56 @@ /*============================================================================ 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 QmitkExternalProgramsPreferencePage_h -#define QmitkExternalProgramsPreferencePage_h +#ifndef QmitkMovieMakerPreferencePage_h +#define QmitkMovieMakerPreferencePage_h #include #include -#include +#include + +class QWidget; namespace Ui { - class QmitkExternalProgramsPreferencePage; + class QmitkMovieMakerPreferencePage; } -class QmitkExternalProgramsPreferencePage : public QObject, public berry::IQtPreferencePage +class QmitkMovieMakerPreferencePage : public QObject, public berry::IQtPreferencePage { Q_OBJECT Q_INTERFACES(berry::IPreferencePage) public: - QmitkExternalProgramsPreferencePage(); - ~QmitkExternalProgramsPreferencePage() override; + QmitkMovieMakerPreferencePage(); + ~QmitkMovieMakerPreferencePage() override; + void Init(berry::IWorkbench::Pointer workbench) override; void CreateQtControl(QWidget* parent) override; QWidget* GetQtControl() const override; - void Init(berry::IWorkbench::Pointer) override; - void PerformCancel() override; bool PerformOk() override; + void PerformCancel() override; void Update() override; private slots: void OnFFmpegButtonClicked(); void OnFFmpegProcessError(QProcess::ProcessError error); void OnFFmpegProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - void OnGnuplotButtonClicked(); - void OnGnuplotProcessError(QProcess::ProcessError error); - void OnGnuplotProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - private: - QScopedPointer m_Ui; + Ui::QmitkMovieMakerPreferencePage* m_Ui; QWidget* m_Control; QProcess* m_FFmpegProcess; QString m_FFmpegPath; - - QProcess* m_GnuplotProcess; - QString m_GnuplotPath; }; #endif diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.ui b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.ui new file mode 100644 index 0000000000..514b9e8285 --- /dev/null +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerPreferencePage.ui @@ -0,0 +1,80 @@ + + + QmitkMovieMakerPreferencePage + + + + 0 + 0 + 440 + 133 + + + + + + + FFmpeg path: + + + + + + + + + true + + + + + + + ... + + + + + + + + + Video output format: + + + + + + + + + VP9 (*.webm) + + + true + + + formatButtonGroup + + + + + + + H.264 (*.mp4) + + + formatButtonGroup + + + + + + + + + + + + + diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.cpp b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.cpp index 8bb3f309f6..4b97be0319 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.cpp +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.cpp @@ -1,715 +1,698 @@ /*============================================================================ 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 "QmitkMovieMakerView.h" #include #include "QmitkAnimationItemDelegate.h" #include "QmitkOrbitAnimationItem.h" #include "QmitkOrbitAnimationWidget.h" #include "QmitkSliceAnimationItem.h" #include "QmitkSliceAnimationWidget.h" #include "QmitkTimeSliceAnimationItem.h" #include "QmitkTimeSliceAnimationWidget.h" -#include -#include -#include -#include +#include #include #include #include -#include +#include #include #include -#include - -#include -#include namespace { - class TemporaryDirectory - { - public: - TemporaryDirectory() - { - try - { - m_Path = mitk::IOUtil::CreateTemporaryDirectory("MITK_MovieMaker_XXXXXX"); - } - catch (...) - { - } - } - - ~TemporaryDirectory() - { - try - { - std::filesystem::remove_all(m_Path); - } - catch (...) - { - } - } - - bool IsValid() const - { - return !m_Path.empty() && std::filesystem::is_directory(m_Path); - } - - std::filesystem::path GetPath() const - { - return m_Path; - } - - TemporaryDirectory(const TemporaryDirectory&) = delete; - TemporaryDirectory& operator=(const TemporaryDirectory&) = delete; - - private: - std::filesystem::path m_Path; - }; - QmitkAnimationItem* CreateDefaultAnimation(const QString& widgetKey) { if (widgetKey == "Orbit") return new QmitkOrbitAnimationItem; if (widgetKey == "Slice") return new QmitkSliceAnimationItem; if (widgetKey == "Time") return new QmitkTimeSliceAnimationItem; return nullptr; } - QString GetFFmpegPath() + class EncodingThread : public QThread { - auto* preferences = mitk::CoreServices::GetPreferencesService()->GetSystemPreferences()->Node("/org.mitk.gui.qt.ext.externalprograms"); + public: + EncodingThread(mitk::VideoRecorder* videoRecorder, QObject* parent = nullptr) + : QThread(parent), + m_VideoRecorder(videoRecorder) + { + } - return preferences != nullptr - ? QString::fromStdString(preferences->Get("ffmpeg", "")) - : QString(); - } + ~EncodingThread() override = default; + + private: + void run() override + { + m_VideoRecorder->StopRecording(); + } + + mitk::VideoRecorder* m_VideoRecorder; + }; } const std::string QmitkMovieMakerView::VIEW_ID = "org.mitk.views.moviemaker"; QmitkMovieMakerView::QmitkMovieMakerView() : m_Parent(nullptr), m_Ui(new Ui::QmitkMovieMakerView), m_AnimationModel(nullptr), m_AddAnimationMenu(nullptr), m_RecordMenu(nullptr), m_Timer(nullptr), m_TotalDuration(0.0), m_NumFrames(0), m_CurrentFrame(0) { } QmitkMovieMakerView::~QmitkMovieMakerView() { } void QmitkMovieMakerView::CreateQtPartControl(QWidget* parent) { m_Parent = parent; m_Ui->setupUi(parent); this->InitializeAnimationWidgets(); this->InitializeAnimationTreeViewWidgets(); this->InitializePlaybackAndRecordWidgets(); this->InitializeTimer(parent); m_Ui->animationWidgetGroupBox->setVisible(false); } void QmitkMovieMakerView::InitializeAnimationWidgets() { m_AnimationWidgets["Orbit"] = new QmitkOrbitAnimationWidget; m_AnimationWidgets["Slice"] = new QmitkSliceAnimationWidget; m_AnimationWidgets["Time"] = new QmitkTimeSliceAnimationWidget; for (const auto& widget : m_AnimationWidgets) { if (nullptr != widget.second) { widget.second->setVisible(false); m_Ui->animationWidgetGroupBoxLayout->addWidget(widget.second); } } this->ConnectAnimationWidgets(); } void QmitkMovieMakerView::InitializeAnimationTreeViewWidgets() { this->InitializeAnimationModel(); this->InitializeAddAnimationMenu(); this->ConnectAnimationTreeViewWidgets(); } void QmitkMovieMakerView::InitializePlaybackAndRecordWidgets() { this->InitializeRecordMenu(); this->ConnectPlaybackAndRecordWidgets(); + this->InitializeRecordingProgress(); } void QmitkMovieMakerView::InitializeAnimationModel() { m_AnimationModel = new QStandardItemModel(m_Ui->animationTreeView); m_AnimationModel->setHorizontalHeaderLabels(QStringList() << "Animation" << "Timeline"); m_Ui->animationTreeView->setModel(m_AnimationModel); m_Ui->animationTreeView->setItemDelegate(new QmitkAnimationItemDelegate(m_Ui->animationTreeView)); } void QmitkMovieMakerView::InitializeAddAnimationMenu() { m_AddAnimationMenu = new QMenu(m_Ui->addAnimationButton); for(const auto& widget : m_AnimationWidgets) m_AddAnimationMenu->addAction(widget.first); } void QmitkMovieMakerView::InitializeRecordMenu() { std::array, 4> renderWindows = { std::make_pair(QStringLiteral("Axial"), QStringLiteral("stdmulti.widget0")), std::make_pair(QStringLiteral("Sagittal"), QStringLiteral("stdmulti.widget1")), std::make_pair(QStringLiteral("Coronal"), QStringLiteral("stdmulti.widget2")), std::make_pair(QStringLiteral("3D"), QStringLiteral("stdmulti.widget3")) }; m_RecordMenu = new QMenu(m_Ui->recordButton); for(const auto& renderWindow : renderWindows) { auto* action = new QAction(m_RecordMenu); action->setText(renderWindow.first); action->setData(renderWindow.second); m_RecordMenu->addAction(action); } } +void QmitkMovieMakerView::InitializeRecordingProgress() +{ + m_Ui->recordingLabel->setEnabled(true); + m_Ui->recordingLabel->setVisible(false); + + m_Ui->recordingProgressBar->setEnabled(true); + m_Ui->recordingProgressBar->setValue(0); + m_Ui->recordingProgressBar->setVisible(false); + + m_Ui->encodingLabel->setEnabled(false); + m_Ui->encodingLabel->setVisible(false); + + m_Ui->encodingProgressBar->setEnabled(false); + m_Ui->encodingProgressBar->setMaximum(1); + m_Ui->encodingProgressBar->setVisible(false); +} + void QmitkMovieMakerView::InitializeTimer(QWidget* parent) { m_Timer = new QTimer(parent); this->OnFPSSpinBoxValueChanged(m_Ui->fpsSpinBox->value()); this->ConnectTimer(); } void QmitkMovieMakerView::ConnectAnimationTreeViewWidgets() { connect(m_AnimationModel, &QStandardItemModel::rowsInserted, this, &QmitkMovieMakerView::OnAnimationTreeViewRowsInserted); connect(m_AnimationModel, &QStandardItemModel::rowsRemoved, this, &QmitkMovieMakerView::OnAnimationTreeViewRowsRemoved); connect(m_Ui->animationTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QmitkMovieMakerView::OnAnimationTreeViewSelectionChanged); connect(m_Ui->moveAnimationUpButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnMoveAnimationUpButtonClicked); connect(m_Ui->moveAnimationDownButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnMoveAnimationDownButtonClicked); connect(m_Ui->addAnimationButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnAddAnimationButtonClicked); connect(m_Ui->removeAnimationButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnRemoveAnimationButtonClicked); } void QmitkMovieMakerView::ConnectAnimationWidgets() { connect(m_Ui->startComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(OnStartComboBoxCurrentIndexChanged(int))); connect(m_Ui->durationSpinBox, SIGNAL(valueChanged(double)), this, SLOT(OnDurationSpinBoxValueChanged(double))); connect(m_Ui->delaySpinBox, SIGNAL(valueChanged(double)), this, SLOT(OnDelaySpinBoxValueChanged(double))); } void QmitkMovieMakerView::ConnectPlaybackAndRecordWidgets() { connect(m_Ui->playButton, &QToolButton::toggled, this, &QmitkMovieMakerView::OnPlayButtonToggled); connect(m_Ui->stopButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnStopButtonClicked); connect(m_Ui->recordButton, &QToolButton::clicked, this, &QmitkMovieMakerView::OnRecordButtonClicked); connect(m_Ui->fpsSpinBox, SIGNAL(valueChanged(int)), this, SLOT(OnFPSSpinBoxValueChanged(int))); } void QmitkMovieMakerView::ConnectTimer() { connect(m_Timer, &QTimer::timeout, this, &QmitkMovieMakerView::OnTimerTimeout); } void QmitkMovieMakerView::SetFocus() { m_Ui->addAnimationButton->setFocus(); } void QmitkMovieMakerView::RenderWindowPartActivated(mitk::IRenderWindowPart* renderWindowPart) { auto multiWidgetEditor = dynamic_cast(renderWindowPart); bool isMxN = nullptr != multiWidgetEditor && multiWidgetEditor->GetClassName() == "QmitkMxNMultiWidgetEditor"; m_Parent->setDisabled(isMxN); } void QmitkMovieMakerView::RenderWindowPartDeactivated(mitk::IRenderWindowPart* /*renderWindowPart*/) { } void QmitkMovieMakerView::RenderWindowPartInputChanged(mitk::IRenderWindowPart* /*renderWindowPart*/) { } void QmitkMovieMakerView::OnMoveAnimationUpButtonClicked() { const QItemSelection selection = m_Ui->animationTreeView->selectionModel()->selection(); if (!selection.isEmpty()) { const int selectedRow = selection[0].top(); if (selectedRow > 0) m_AnimationModel->insertRow(selectedRow - 1, m_AnimationModel->takeRow(selectedRow)); } this->CalculateTotalDuration(); } void QmitkMovieMakerView::OnMoveAnimationDownButtonClicked() { const QItemSelection selection = m_Ui->animationTreeView->selectionModel()->selection(); if (!selection.isEmpty()) { const int rowCount = m_AnimationModel->rowCount(); const int selectedRow = selection[0].top(); if (selectedRow < rowCount - 1) m_AnimationModel->insertRow(selectedRow + 1, m_AnimationModel->takeRow(selectedRow)); } this->CalculateTotalDuration(); } void QmitkMovieMakerView::OnAddAnimationButtonClicked() { auto action = m_AddAnimationMenu->exec(QCursor::pos()); if (nullptr != action) { const auto key = action->text(); m_AnimationModel->appendRow(QList() << new QStandardItem(key) << CreateDefaultAnimation(key)); m_Ui->playbackAndRecordingGroupBox->setEnabled(true); } } void QmitkMovieMakerView::OnPlayButtonToggled(bool checked) { if (checked) { m_Ui->playButton->setIcon(QIcon(":/org_mitk_icons/icons/tango/scalable/actions/media-playback-pause.svg")); m_Ui->playButton->repaint(); m_Timer->start(static_cast(1000.0 / m_Ui->fpsSpinBox->value())); } else { m_Timer->stop(); m_Ui->playButton->setIcon(QIcon(":/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg")); m_Ui->playButton->repaint(); } } void QmitkMovieMakerView::OnStopButtonClicked() { m_Ui->playButton->setChecked(false); m_Ui->stopButton->setEnabled(false); m_CurrentFrame = 0; this->RenderCurrentFrame(); } void QmitkMovieMakerView::OnRecordButtonClicked() { if (0 == m_NumFrames || 0.0 == m_TotalDuration) return; - const QString ffmpegPath = GetFFmpegPath(); + m_VideoRecorder = std::make_unique(); - if (ffmpegPath.isEmpty()) + if (m_VideoRecorder->GetFFmpegPath().empty()) { QMessageBox::information(nullptr, "Movie Maker", "

Set path to FFmpeg (ffmpeg.org) in preferences " - "(Window -> Preferences... (Ctrl+P) -> External Programs) " - "to be able to record your movies to video files.

"); + "(Window -> Preferences... (Ctrl+P) -> Movie Maker) to be able to record your " + "movies to video files.

"); return; } auto action = m_RecordMenu->exec(QCursor::pos()); if (nullptr == action) return; - auto renderWindow = mitk::BaseRenderer::GetRenderWindowByName(action->data().toString().toStdString()); + m_VideoRecorder->SetRenderWindowName(action->data().toString().toStdString()); + m_VideoRecorder->SetFrameRate(static_cast(m_Ui->fpsSpinBox->value())); + + auto fileExt = QString::fromStdString(mitk::VideoRecorder::GetFileExtension(m_VideoRecorder->GetOutputFormat())); + QString outputPath = QFileDialog::getSaveFileName(nullptr, "Specify a filename", "", "Movie (*" + fileExt + ")"); - if (nullptr == renderWindow) + if (outputPath.isEmpty()) return; - QString saveFileName = QFileDialog::getSaveFileName(nullptr, "Specify a filename", "", "Movie (*.webm)"); + if(!outputPath.endsWith(fileExt)) + outputPath += fileExt; - if (saveFileName.isEmpty()) - return; + m_VideoRecorder->SetOutputPath(outputPath.toStdString()); - if(!saveFileName.endsWith(".webm")) - saveFileName += ".webm"; + m_Ui->recordButton->setEnabled(false); - try - { - // Create a temporary directory and write all frames as PNGs into this directory. - // Call FFmpeg to create a video from these PNG images. - // Delete the temporary directory afterwards. + m_Ui->recordingProgressBar->setMaximum(m_NumFrames); - TemporaryDirectory tempDir; + m_Ui->recordingLabel->setVisible(true); + m_Ui->recordingProgressBar->setVisible(true); - if (!tempDir.IsValid()) - return; + m_Ui->encodingLabel->setVisible(true); + m_Ui->encodingProgressBar->setVisible(true); - auto windowToImage = vtkSmartPointer::New(); - windowToImage->SetInput(renderWindow); + EncodingThread* encodingThread = new EncodingThread(m_VideoRecorder.get(), m_Parent); + connect(encodingThread, &EncodingThread::finished, this, &QmitkMovieMakerView::OnEncodingFinished); - auto imageWriter = vtkSmartPointer::New(); - imageWriter->SetInputConnection(windowToImage->GetOutputPort()); + try + { + m_VideoRecorder->StartRecording(); for (m_CurrentFrame = 0; m_CurrentFrame < m_NumFrames; ++m_CurrentFrame) { - this->RenderCurrentFrame(); - windowToImage->Modified(); - - std::stringstream stream; - stream << std::setw(8) << std::setfill('0') << m_CurrentFrame << ".png"; - auto path = tempDir.GetPath() / stream.str(); + m_Ui->recordingProgressBar->setValue(m_CurrentFrame + 1); - imageWriter->SetFileName(path.string().c_str()); - imageWriter->Write(); + this->RenderCurrentFrame(); + m_VideoRecorder->RecordFrame(); } - QProcess ffmpeg; + m_Ui->encodingProgressBar->setMaximum(0); - ffmpeg.setWorkingDirectory(QString::fromStdString(tempDir.GetPath().string())); - ffmpeg.start(ffmpegPath, QStringList() - << "-y" // Override already existing files - << "-r" << QString::number(m_Ui->fpsSpinBox->value()) // Framerate - << "-i" << "%8d.png" // Input images - << "-c:v" << "libvpx-vp9" // VP9 codec - << "-crf" << QString::number(18) // Quality (constant rate factor) - << "-b:v" << QString::number(0) // Must be 0 for constant quality - << saveFileName); // Output video + m_Ui->encodingLabel->setEnabled(true); + m_Ui->encodingProgressBar->setEnabled(true); - if (ffmpeg.waitForStarted()) - ffmpeg.waitForFinished(); + encodingThread->start(); } catch (const mitk::Exception& exception) { + if (encodingThread->isRunning()) + encodingThread->terminate(); + QMessageBox::critical(nullptr, "Movie Maker", exception.GetDescription()); + + m_VideoRecorder = nullptr; + this->OnEncodingFinished(); } +} + +void QmitkMovieMakerView::OnEncodingFinished() +{ + m_VideoRecorder = nullptr; + + this->InitializeRecordingProgress(); m_CurrentFrame = 0; this->RenderCurrentFrame(); + + m_Ui->recordButton->setEnabled(true); } void QmitkMovieMakerView::OnRemoveAnimationButtonClicked() { const QItemSelection selection = m_Ui->animationTreeView->selectionModel()->selection(); if (!selection.isEmpty()) m_AnimationModel->removeRow(selection[0].top()); } void QmitkMovieMakerView::OnAnimationTreeViewRowsInserted(const QModelIndex& parent, int start, int) { this->CalculateTotalDuration(); m_Ui->animationTreeView->selectionModel()->select( m_AnimationModel->index(start, 0, parent), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } void QmitkMovieMakerView::OnAnimationTreeViewRowsRemoved(const QModelIndex&, int, int) { this->CalculateTotalDuration(); this->UpdateWidgets(); } void QmitkMovieMakerView::OnAnimationTreeViewSelectionChanged(const QItemSelection&, const QItemSelection&) { this->UpdateWidgets(); } void QmitkMovieMakerView::OnStartComboBoxCurrentIndexChanged(int index) { QmitkAnimationItem* item = this->GetSelectedAnimationItem(); if (item != nullptr) { item->SetStartWithPrevious(index); this->RedrawTimeline(); this->CalculateTotalDuration(); } } void QmitkMovieMakerView::OnDurationSpinBoxValueChanged(double value) { QmitkAnimationItem* item = this->GetSelectedAnimationItem(); if (item != nullptr) { item->SetDuration(value); this->RedrawTimeline(); this->CalculateTotalDuration(); } } void QmitkMovieMakerView::OnDelaySpinBoxValueChanged(double value) { QmitkAnimationItem* item = this->GetSelectedAnimationItem(); if (item != nullptr) { item->SetDelay(value); this->RedrawTimeline(); this->CalculateTotalDuration(); } } void QmitkMovieMakerView::OnFPSSpinBoxValueChanged(int value) { this->CalculateTotalDuration(); m_Timer->setInterval(static_cast(1000.0 / value)); } void QmitkMovieMakerView::OnTimerTimeout() { this->RenderCurrentFrame(); m_CurrentFrame = std::min(m_NumFrames, m_CurrentFrame + 1); if (m_CurrentFrame >= m_NumFrames) { m_Ui->playButton->setChecked(false); m_CurrentFrame = 0; this->RenderCurrentFrame(); } m_Ui->stopButton->setEnabled(m_CurrentFrame != 0); } void QmitkMovieMakerView::RenderCurrentFrame() { const double deltaT = m_TotalDuration / (m_NumFrames - 1); const auto activeAnimations = this->GetActiveAnimations(m_CurrentFrame * deltaT); for (const auto& animation : activeAnimations) { const auto nextActiveAnimations = this->GetActiveAnimations((m_CurrentFrame + 1) * deltaT); bool lastFrameForAnimation = true; for (const auto& nextAnimation : nextActiveAnimations) { if (nextAnimation.first == animation.first) { lastFrameForAnimation = false; break; } } animation.first->Animate(!lastFrameForAnimation ? animation.second : 1.0); } mitk::RenderingManager::GetInstance()->ForceImmediateUpdateAll(); } void QmitkMovieMakerView::UpdateWidgets() { const QItemSelection selection = m_Ui->animationTreeView->selectionModel()->selection(); if (selection.isEmpty()) { m_Ui->moveAnimationUpButton->setEnabled(false); m_Ui->moveAnimationDownButton->setEnabled(false); m_Ui->removeAnimationButton->setEnabled(false); m_Ui->playbackAndRecordingGroupBox->setEnabled(false); this->HideCurrentAnimationWidget(); } else { const int rowCount = m_AnimationModel->rowCount(); const int selectedRow = selection[0].top(); m_Ui->moveAnimationUpButton->setEnabled(rowCount > 1 && selectedRow != 0); m_Ui->moveAnimationDownButton->setEnabled(rowCount > 1 && selectedRow < rowCount - 1); m_Ui->removeAnimationButton->setEnabled(true); m_Ui->playbackAndRecordingGroupBox->setEnabled(true); this->ShowAnimationWidget(dynamic_cast(m_AnimationModel->item(selectedRow, 1))); } this->UpdateAnimationWidgets(); } void QmitkMovieMakerView::UpdateAnimationWidgets() { QmitkAnimationItem* item = this->GetSelectedAnimationItem(); if (item != nullptr) { m_Ui->startComboBox->setCurrentIndex(item->GetStartWithPrevious()); m_Ui->durationSpinBox->setValue(item->GetDuration()); m_Ui->delaySpinBox->setValue(item->GetDelay()); m_Ui->animationGroupBox->setEnabled(true); } else { m_Ui->animationGroupBox->setEnabled(false); } } void QmitkMovieMakerView::HideCurrentAnimationWidget() { if (m_Ui->animationWidgetGroupBox->isVisible()) { m_Ui->animationWidgetGroupBox->setVisible(false); int numWidgets = m_Ui->animationWidgetGroupBoxLayout->count(); for (int i = 0; i < numWidgets; ++i) m_Ui->animationWidgetGroupBoxLayout->itemAt(i)->widget()->setVisible(false); } } void QmitkMovieMakerView::ShowAnimationWidget(QmitkAnimationItem* animationItem) { this->HideCurrentAnimationWidget(); if (animationItem == nullptr) return; const QString widgetKey = animationItem->GetWidgetKey(); auto animationWidgetIter = m_AnimationWidgets.find(widgetKey); auto animationWidget = m_AnimationWidgets.end() != animationWidgetIter ? animationWidgetIter->second : nullptr; if (nullptr != animationWidget) { m_Ui->animationWidgetGroupBox->setTitle(widgetKey); animationWidget->SetAnimationItem(animationItem); animationWidget->setVisible(true); } m_Ui->animationWidgetGroupBox->setVisible(animationWidget != nullptr); } void QmitkMovieMakerView::RedrawTimeline() { if (m_AnimationModel->rowCount() > 1) { m_Ui->animationTreeView->dataChanged( m_AnimationModel->index(0, 1), m_AnimationModel->index(m_AnimationModel->rowCount() - 1, 1)); } } QmitkAnimationItem* QmitkMovieMakerView::GetSelectedAnimationItem() const { const QItemSelection selection = m_Ui->animationTreeView->selectionModel()->selection(); return !selection.isEmpty() ? dynamic_cast(m_AnimationModel->item(selection[0].top(), 1)) : nullptr; } void QmitkMovieMakerView::CalculateTotalDuration() { const int rowCount = m_AnimationModel->rowCount(); double totalDuration = 0.0; double previousStart = 0.0; for (int i = 0; i < rowCount; ++i) { auto item = dynamic_cast(m_AnimationModel->item(i, 1)); if (nullptr == item) continue; if (item->GetStartWithPrevious()) { totalDuration = std::max(totalDuration, previousStart + item->GetDelay() + item->GetDuration()); } else { previousStart = totalDuration; totalDuration += item->GetDelay() + item->GetDuration(); } } m_TotalDuration = totalDuration; m_NumFrames = static_cast(totalDuration * m_Ui->fpsSpinBox->value()); } std::vector> QmitkMovieMakerView::GetActiveAnimations(double t) const { const int rowCount = m_AnimationModel->rowCount(); std::vector> activeAnimations; double totalDuration = 0.0; double previousStart = 0.0; for (int i = 0; i < rowCount; ++i) { QmitkAnimationItem* item = dynamic_cast(m_AnimationModel->item(i, 1)); if (item == nullptr) continue; if (item->GetDuration() > 0.0) { double start = item->GetStartWithPrevious() ? previousStart + item->GetDelay() : totalDuration + item->GetDelay(); if (start <= t && t <= start + item->GetDuration()) activeAnimations.emplace_back(item, (t - start) / item->GetDuration()); } if (item->GetStartWithPrevious()) { totalDuration = std::max(totalDuration, previousStart + item->GetDelay() + item->GetDuration()); } else { previousStart = totalDuration; totalDuration += item->GetDelay() + item->GetDuration(); } } return activeAnimations; } diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.h b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.h index cbf3795b2f..666ddfde1a 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.h +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.h @@ -1,101 +1,110 @@ /*============================================================================ 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 QmitkMovieMakerView_h #define QmitkMovieMakerView_h #include #include +#include #include #include class QmitkAnimationItem; class QmitkAnimationWidget; class QMenu; class QStandardItemModel; class QTimer; +namespace mitk +{ + class VideoRecorder; +} + namespace Ui { class QmitkMovieMakerView; } class QmitkMovieMakerView : public QmitkAbstractView, public mitk::IRenderWindowPartListener { Q_OBJECT public: static const std::string VIEW_ID; QmitkMovieMakerView(); ~QmitkMovieMakerView() override; void CreateQtPartControl(QWidget* parent) override; void SetFocus() override; void RenderWindowPartActivated(mitk::IRenderWindowPart* renderWindowPart) override; void RenderWindowPartDeactivated(mitk::IRenderWindowPart* renderWindowPart) override; void RenderWindowPartInputChanged(mitk::IRenderWindowPart* renderWindowPart) override; private slots: void OnMoveAnimationUpButtonClicked(); void OnMoveAnimationDownButtonClicked(); void OnAddAnimationButtonClicked(); void OnRemoveAnimationButtonClicked(); void OnAnimationTreeViewRowsInserted(const QModelIndex& parent, int start, int end); void OnAnimationTreeViewRowsRemoved(const QModelIndex& parent, int start, int end); void OnAnimationTreeViewSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); void OnStartComboBoxCurrentIndexChanged(int index); void OnDurationSpinBoxValueChanged(double value); void OnDelaySpinBoxValueChanged(double value); void OnPlayButtonToggled(bool checked); void OnStopButtonClicked(); void OnRecordButtonClicked(); + void OnEncodingFinished(); void OnFPSSpinBoxValueChanged(int value); void OnTimerTimeout(); private: void InitializeAnimationWidgets(); void InitializeAnimationTreeViewWidgets(); void InitializeAnimationModel(); void InitializeAddAnimationMenu(); void InitializePlaybackAndRecordWidgets(); void InitializeRecordMenu(); + void InitializeRecordingProgress(); void InitializeTimer(QWidget* parent); void ConnectAnimationTreeViewWidgets(); void ConnectAnimationWidgets(); void ConnectPlaybackAndRecordWidgets(); void ConnectTimer(); void RenderCurrentFrame(); void UpdateWidgets(); void UpdateAnimationWidgets(); void HideCurrentAnimationWidget(); void ShowAnimationWidget(QmitkAnimationItem* animationItem); void RedrawTimeline(); void CalculateTotalDuration(); QmitkAnimationItem* GetSelectedAnimationItem() const; std::vector> GetActiveAnimations(double t) const; QWidget* m_Parent; Ui::QmitkMovieMakerView* m_Ui; QStandardItemModel* m_AnimationModel; std::map m_AnimationWidgets; QMenu* m_AddAnimationMenu; QMenu* m_RecordMenu; QTimer* m_Timer; double m_TotalDuration; int m_NumFrames; int m_CurrentFrame; + std::unique_ptr m_VideoRecorder; }; #endif diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.ui b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.ui index ccbd70bb83..d834787a0a 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.ui +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/QmitkMovieMakerView.ui @@ -1,507 +1,558 @@ QmitkMovieMakerView true 0 0 - 320 - 640 + 305 + 644 Movie Maker false Move animation up :/org_mitk_icons/icons/tango/scalable/actions/go-up.svg:/org_mitk_icons/icons/tango/scalable/actions/go-up.svg 24 24 true false Move animation down :/org_mitk_icons/icons/tango/scalable/actions/go-down.svg:/org_mitk_icons/icons/tango/scalable/actions/go-down.svg 24 24 true Add animation :/org_mitk_icons/icons/tango/scalable/actions/list-add.svg:/org_mitk_icons/icons/tango/scalable/actions/list-add.svg 24 24 true false Remove animation :/org_mitk_icons/icons/tango/scalable/actions/list-remove.svg:/org_mitk_icons/icons/tango/scalable/actions/list-remove.svg 24 24 true 0 0 0 100 16777215 100 false true 80 false Animation 0 0 24 24 24 24 :/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg true 0 0 Start: startComboBox 0 0 After previous With previous 0 0 24 24 24 24 :/QmitkMovieMakerView/duration.svg true 0 0 Duration: durationSpinBox 0 0 s 100.000000000000000 0.500000000000000 2.000000000000000 0 0 24 24 24 24 :/QmitkMovieMakerView/delay.svg true 0 0 Delay: delaySpinBox 0 0 s 100.000000000000000 0.500000000000000 false Playback && Recording - + - - - Play - - - - :/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg:/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg - - - - 24 - 24 - - - - true - - - true - - - - - - - false - - - Stop - - - - :/org_mitk_icons/icons/tango/scalable/actions/media-playback-stop.svg:/org_mitk_icons/icons/tango/scalable/actions/media-playback-stop.svg - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Record - - - - :/org_mitk_icons/icons/tango/scalable/actions/media-record.svg:/org_mitk_icons/icons/tango/scalable/actions/media-record.svg - - - - 24 - 24 - - - - true - - + + + + + Play + + + + :/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg:/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg + + + + 24 + 24 + + + + true + + + true + + + + + + + false + + + Stop + + + + :/org_mitk_icons/icons/tango/scalable/actions/media-playback-stop.svg:/org_mitk_icons/icons/tango/scalable/actions/media-playback-stop.svg + + + + 24 + 24 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Record + + + + :/org_mitk_icons/icons/tango/scalable/actions/media-record.svg:/org_mitk_icons/icons/tango/scalable/actions/media-record.svg + + + + 24 + 24 + + + + true + + + + + + + Frames per second + + + FPS + + + 1 + + + 120 + + + 30 + + + + - - - Frames per second - - - FPS - - - 1 - - - 120 - - - 30 - - + + + + + Recording: + + + + + + + 0 + + + Qt::AlignCenter + + + true + + + %v / %m Frames + + + + + + + Encoding: + + + + + + + 1 + + + 0 + + + false + + + + Qt::Vertical 20 227 animationTreeView moveAnimationUpButton moveAnimationDownButton addAnimationButton removeAnimationButton startComboBox durationSpinBox delaySpinBox playButton stopButton recordButton fpsSpinBox diff --git a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/mitkMovieMakerPluginActivator.cpp b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/mitkMovieMakerPluginActivator.cpp index 0178c16e04..2e8dabab1a 100644 --- a/Plugins/org.mitk.gui.qt.moviemaker/src/internal/mitkMovieMakerPluginActivator.cpp +++ b/Plugins/org.mitk.gui.qt.moviemaker/src/internal/mitkMovieMakerPluginActivator.cpp @@ -1,33 +1,35 @@ /*============================================================================ 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 "mitkMovieMakerPluginActivator.h" +#include "QmitkMovieMakerPreferencePage.h" #include "QmitkMovieMakerView.h" #include "QmitkScreenshotMaker.h" #include US_INITIALIZE_MODULE namespace mitk { void MovieMakerPluginActivator::start(ctkPluginContext* context) { + BERRY_REGISTER_EXTENSION_CLASS(QmitkMovieMakerPreferencePage, context) BERRY_REGISTER_EXTENSION_CLASS(QmitkMovieMakerView, context) BERRY_REGISTER_EXTENSION_CLASS(QmitkScreenshotMaker, context) } void MovieMakerPluginActivator::stop(ctkPluginContext* context) { Q_UNUSED(context) } }