diff --git a/Modules/Segmentation/Interactions/mitkSegmentAnythingPythonService.cpp b/Modules/Segmentation/Interactions/mitkSegmentAnythingPythonService.cpp index 591ff73a08..5eb4388188 100644 --- a/Modules/Segmentation/Interactions/mitkSegmentAnythingPythonService.cpp +++ b/Modules/Segmentation/Interactions/mitkSegmentAnythingPythonService.cpp @@ -1,244 +1,251 @@ /*============================================================================ 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 "mitkSegmentAnythingPythonService.h" #include "mitkIOUtil.h" #include #include #include #include #include #include "mitkImageAccessByItk.h" using namespace std::chrono_literals; namespace mitk { const std::string SIGNALCONSTANTS::READY = "READY"; const std::string SIGNALCONSTANTS::KILL = "KILL"; const std::string SIGNALCONSTANTS::OFF = "OFF"; const std::string SIGNALCONSTANTS::CUDA_OUT_OF_MEMORY_ERROR = "CudaOutOfMemoryError"; SegmentAnythingPythonService::Status SegmentAnythingPythonService::CurrentStatus = SegmentAnythingPythonService::Status::OFF; } mitk::SegmentAnythingPythonService::SegmentAnythingPythonService( std::string workingDir, std::string modelType, std::string checkPointPath, unsigned int gpuId) : m_PythonPath(workingDir), m_ModelType(modelType), m_CheckpointPath(checkPointPath), m_GpuId(gpuId) { this->CreateTempDirs(PARENT_TEMP_DIR_PATTERN); } mitk::SegmentAnythingPythonService::~SegmentAnythingPythonService() { if (CurrentStatus == Status::READY) { this->StopAsyncProcess(); } CurrentStatus = Status::OFF; std::filesystem::remove_all(this->GetMitkTempDir()); } void mitk::SegmentAnythingPythonService::onPythonProcessEvent(itk::Object*, const itk::EventObject &e, void*) { std::string testCOUT,testCERR; const auto *pEvent = dynamic_cast(&e); if (pEvent) { testCOUT = testCOUT + pEvent->GetOutput(); testCOUT.erase(std::find_if(testCOUT.rbegin(), testCOUT.rend(), [](unsigned char ch) { return !std::isspace(ch);}).base(), testCOUT.end()); // remove trailing whitespaces, if any if (SIGNALCONSTANTS::READY == testCOUT) { CurrentStatus = Status::READY; } if (SIGNALCONSTANTS::KILL == testCOUT) { CurrentStatus = Status::KILLED; } if (SIGNALCONSTANTS::CUDA_OUT_OF_MEMORY_ERROR == testCOUT) { CurrentStatus = Status::CUDAError; } MITK_INFO << testCOUT; } const auto *pErrEvent = dynamic_cast(&e); if (pErrEvent) { testCERR = testCERR + pErrEvent->GetOutput(); MITK_ERROR << testCERR; } } void mitk::SegmentAnythingPythonService::StopAsyncProcess() { std::stringstream controlStream; controlStream << SIGNALCONSTANTS::KILL; this->WriteControlFile(controlStream); m_Future.get(); } void mitk::SegmentAnythingPythonService::StartAsyncProcess() { if (nullptr != m_DaemonExec) { this->StopAsyncProcess(); } if (this->GetMitkTempDir().empty()) { this->CreateTempDirs(PARENT_TEMP_DIR_PATTERN); } std::stringstream controlStream; controlStream << SIGNALCONSTANTS::READY; this->WriteControlFile(controlStream); m_DaemonExec = ProcessExecutor::New(); itk::CStyleCommand::Pointer spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(&mitk::SegmentAnythingPythonService::onPythonProcessEvent); m_DaemonExec->AddObserver(ExternalProcessOutputEvent(), spCommand); m_Future = std::async(std::launch::async, &mitk::SegmentAnythingPythonService::start_python_daemon, this); } void mitk::SegmentAnythingPythonService::TransferPointsToProcess(std::stringstream &triggerCSV) { this->CheckStatus(); std::string triggerFilePath = m_InDir + IOUtil::GetDirectorySeparator() + TRIGGER_FILENAME; std::ofstream csvfile; csvfile.open(triggerFilePath, std::ofstream::out | std::ofstream::trunc); csvfile << triggerCSV.rdbuf(); csvfile.close(); } void mitk::SegmentAnythingPythonService::WriteControlFile(std::stringstream &statusStream) { std::string controlFilePath = m_InDir + IOUtil::GetDirectorySeparator() + "control.txt"; std::ofstream controlFile; controlFile.open(controlFilePath, std::ofstream::out | std::ofstream::trunc); controlFile << statusStream.rdbuf(); controlFile.close(); } void mitk::SegmentAnythingPythonService::start_python_daemon() { ProcessExecutor::ArgumentListType args; std::string command = "python"; args.push_back("-u"); args.push_back(SAM_PYTHON_FILE_NAME); args.push_back("--input-folder"); args.push_back(m_InDir); args.push_back("--output-folder"); args.push_back(m_OutDir); args.push_back("--trigger-file"); args.push_back(TRIGGER_FILENAME); args.push_back("--model-type"); args.push_back(m_ModelType); args.push_back("--checkpoint"); args.push_back(m_CheckpointPath); args.push_back("--device"); if (m_GpuId == -1) { args.push_back("cpu"); } else { args.push_back("cuda"); std::string cudaEnv = "CUDA_VISIBLE_DEVICES=" + std::to_string(m_GpuId); itksys::SystemTools::PutEnv(cudaEnv.c_str()); } try { std::stringstream logStream; for (const auto &arg : args) logStream << arg << " "; logStream << m_PythonPath; MITK_INFO << logStream.str(); m_DaemonExec->Execute(m_PythonPath, command, args); } catch (const mitk::Exception &e) { MITK_ERROR << e.GetDescription(); return; } MITK_INFO << "Python process ended."; } bool mitk::SegmentAnythingPythonService::CheckStatus() { switch (CurrentStatus) { case mitk::SegmentAnythingPythonService::Status::READY: return true; case mitk::SegmentAnythingPythonService::Status::CUDAError: mitkThrow() << "Error: Cuda Out of Memory. Change your model type in Preferences and Activate Segment Anything tool again."; case mitk::SegmentAnythingPythonService::Status::KILLED: mitkThrow() << "Error: Python process is already terminated. Cannot load requested segmentation. Activate Segment Anything tool again."; default: return false; } } void mitk::SegmentAnythingPythonService::CreateTempDirs(const std::string &dirPattern) { this->SetMitkTempDir(IOUtil::CreateTemporaryDirectory(dirPattern)); m_InDir = IOUtil::CreateTemporaryDirectory("sam-in-XXXXXX", m_MitkTempDir); m_OutDir = IOUtil::CreateTemporaryDirectory("sam-out-XXXXXX", m_MitkTempDir); } mitk::LabelSetImage::Pointer mitk::SegmentAnythingPythonService::RetrieveImageFromProcess() { std::string outputImagePath = m_OutDir + IOUtil::GetDirectorySeparator() + m_CurrentUId + ".nrrd"; while (!std::filesystem::exists(outputImagePath)) { this->CheckStatus(); std::this_thread::sleep_for(100ms); } LabelSetImage::Pointer outputBuffer = mitk::IOUtil::Load(outputImagePath); return outputBuffer; } void mitk::SegmentAnythingPythonService::TransferImageToProcess(const Image *inputAtTimeStep, std::string &UId) { std::string inputImagePath = m_InDir + IOUtil::GetDirectorySeparator() + UId + ".nrrd"; - AccessByItk_n(inputAtTimeStep, ITKWriter, (inputImagePath)); + if (inputAtTimeStep->GetPixelType().GetNumberOfComponents() < 2) + { + AccessByItk_n(inputAtTimeStep, ITKWriter, (inputImagePath)); + } + else + { + mitk::IOUtil::Save(inputAtTimeStep, inputImagePath); + } m_CurrentUId = UId; } template void mitk::SegmentAnythingPythonService::ITKWriter(const itk::Image *image, std::string& outputFilename) { typedef itk::Image ImageType; typedef itk::ImageFileWriter WriterType; typename WriterType::Pointer writer = WriterType::New(); writer->SetFileName(outputFilename); writer->SetInput(image); try { writer->Update(); } catch (const itk::ExceptionObject &error) { MITK_ERROR << "Error: " << error << std::endl; mitkThrow() << "Error: " << error; } } diff --git a/Modules/Segmentation/Interactions/mitkSegmentAnythingTool.cpp b/Modules/Segmentation/Interactions/mitkSegmentAnythingTool.cpp index 7740df7018..b022ef3132 100644 --- a/Modules/Segmentation/Interactions/mitkSegmentAnythingTool.cpp +++ b/Modules/Segmentation/Interactions/mitkSegmentAnythingTool.cpp @@ -1,404 +1,403 @@ /*============================================================================ 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 "mitkSegmentAnythingTool.h" #include #include #include #include "mitkInteractionPositionEvent.h" #include "mitkPointSetShapeProperty.h" #include "mitkProperties.h" #include "mitkToolManager.h" #include // us #include #include #include #include #include #include "mitkImageAccessByItk.h" using namespace std::chrono_literals; namespace mitk { MITK_TOOL_MACRO(MITKSEGMENTATION_EXPORT, SegmentAnythingTool, "SegmentAnythingTool"); } mitk::SegmentAnythingTool::SegmentAnythingTool() : SegWithPreviewTool(true, "PressMoveReleaseAndPointSetting") { this->ResetsToEmptyPreviewOn(); this->IsTimePointChangeAwareOff(); this->KeepActiveAfterAcceptOn(); } const char **mitk::SegmentAnythingTool::GetXPM() const { return nullptr; } const char *mitk::SegmentAnythingTool::GetName() const { return "Segment Anything"; } us::ModuleResource mitk::SegmentAnythingTool::GetIconResource() const { us::Module *module = us::GetModuleContext()->GetModule(); us::ModuleResource resource = module->GetResource("AI.svg"); return resource; } void mitk::SegmentAnythingTool::Activated() { Superclass::Activated(); m_PointSetPositive = mitk::PointSet::New(); m_PointSetNodePositive = mitk::DataNode::New(); m_PointSetNodePositive->SetData(m_PointSetPositive); m_PointSetNodePositive->SetName(std::string(this->GetName()) + "_PointSetPositive"); m_PointSetNodePositive->SetBoolProperty("helper object", true); m_PointSetNodePositive->SetColor(0.0, 1.0, 0.0); m_PointSetNodePositive->SetVisibility(true); m_PointSetNodePositive->SetProperty("Pointset.2D.shape", mitk::PointSetShapeProperty::New(mitk::PointSetShapeProperty::CIRCLE)); m_PointSetNodePositive->SetProperty("Pointset.2D.fill shape", mitk::BoolProperty::New(true)); this->GetDataStorage()->Add(m_PointSetNodePositive, this->GetToolManager()->GetWorkingData(0)); m_PointSetNegative = mitk::PointSet::New(); m_PointSetNodeNegative = mitk::DataNode::New(); m_PointSetNodeNegative->SetData(m_PointSetNegative); m_PointSetNodeNegative->SetName(std::string(this->GetName()) + "_PointSetNegative"); m_PointSetNodeNegative->SetBoolProperty("helper object", true); m_PointSetNodeNegative->SetColor(1.0, 0.0, 0.0); m_PointSetNodeNegative->SetVisibility(true); m_PointSetNodeNegative->SetProperty("Pointset.2D.shape", mitk::PointSetShapeProperty::New(mitk::PointSetShapeProperty::CIRCLE)); m_PointSetNodeNegative->SetProperty("Pointset.2D.fill shape", mitk::BoolProperty::New(true)); this->GetDataStorage()->Add(m_PointSetNodeNegative, this->GetToolManager()->GetWorkingData(0)); this->SetLabelTransferScope(LabelTransferScope::ActiveLabel); this->SetLabelTransferMode(LabelTransferMode::MapLabel); } void mitk::SegmentAnythingTool::Deactivated() { this->ClearSeeds(); GetDataStorage()->Remove(m_PointSetNodePositive); GetDataStorage()->Remove(m_PointSetNodeNegative); m_PointSetNodePositive = nullptr; m_PointSetNodeNegative = nullptr; m_PointSetPositive = nullptr; m_PointSetNegative = nullptr; m_PythonService.reset(); Superclass::Deactivated(); } void mitk::SegmentAnythingTool::ConnectActionsAndFunctions() { CONNECT_FUNCTION("ShiftSecondaryButtonPressed", OnAddNegativePoint); CONNECT_FUNCTION("ShiftPrimaryButtonPressed", OnAddPositivePoint); CONNECT_FUNCTION("DeletePoint", OnDelete); } void mitk::SegmentAnythingTool::InitSAMPythonProcess() { if (nullptr != m_PythonService) { m_PythonService.reset(); } m_PythonService = std::make_unique( this->GetPythonPath(), this->GetModelType(), this->GetCheckpointPath(), this->GetGpuId()); m_PythonService->StartAsyncProcess(); } bool mitk::SegmentAnythingTool::IsPythonReady() const { return m_PythonService->CheckStatus(); } void mitk::SegmentAnythingTool::OnAddNegativePoint(StateMachineAction *, InteractionEvent *interactionEvent) { if (!this->GetIsReady() || m_PointSetPositive->GetSize() == 0) { return; } if (!this->IsUpdating() && m_PointSetNegative.IsNotNull()) { const auto positionEvent = dynamic_cast(interactionEvent); if (positionEvent != nullptr) { m_PointSetNegative->InsertPoint(m_PointSetCount, positionEvent->GetPositionInWorld()); m_PointSetCount++; this->UpdatePreview(); } } } void mitk::SegmentAnythingTool::OnAddPositivePoint(StateMachineAction *, InteractionEvent *interactionEvent) { if (!this->GetIsReady()) { return; } m_IsGenerateEmbeddings = false; if ((nullptr == this->GetWorkingPlaneGeometry()) || !mitk::Equal(*(interactionEvent->GetSender()->GetCurrentWorldPlaneGeometry()), *(this->GetWorkingPlaneGeometry()))) { m_IsGenerateEmbeddings = true; this->ClearSeeds(); this->SetWorkingPlaneGeometry(interactionEvent->GetSender()->GetCurrentWorldPlaneGeometry()->Clone()); } if (!this->IsUpdating() && m_PointSetPositive.IsNotNull()) { const auto positionEvent = dynamic_cast(interactionEvent); if (positionEvent != nullptr) { m_PointSetPositive->InsertPoint(m_PointSetCount, positionEvent->GetPositionInWorld()); ++m_PointSetCount; this->UpdatePreview(); } } } void mitk::SegmentAnythingTool::OnDelete(StateMachineAction *, InteractionEvent *) { if (!this->IsUpdating() && m_PointSetPositive.IsNotNull()) { PointSet::Pointer removeSet = m_PointSetPositive; decltype(m_PointSetPositive->GetMaxId().Index()) maxId = 0; if (m_PointSetPositive->GetSize() > 0) { maxId = m_PointSetPositive->GetMaxId().Index(); } if (m_PointSetNegative->GetSize() > 0 && (maxId < m_PointSetNegative->GetMaxId().Index())) { removeSet = m_PointSetNegative; } removeSet->RemovePointAtEnd(0); --m_PointSetCount; this->UpdatePreview(); } } void mitk::SegmentAnythingTool::ClearPicks() { this->ClearSeeds(); this->UpdatePreview(); } bool mitk::SegmentAnythingTool::HasPicks() const { return this->m_PointSetPositive.IsNotNull() && this->m_PointSetPositive->GetSize() > 0; } void mitk::SegmentAnythingTool::ClearSeeds() { if (this->m_PointSetPositive.IsNotNull()) { m_PointSetCount -= m_PointSetPositive->GetSize(); this->m_PointSetPositive = mitk::PointSet::New(); // renew pointset this->m_PointSetNodePositive->SetData(this->m_PointSetPositive); } if (this->m_PointSetNegative.IsNotNull()) { m_PointSetCount -= m_PointSetNegative->GetSize(); this->m_PointSetNegative = mitk::PointSet::New(); // renew pointset this->m_PointSetNodeNegative->SetData(this->m_PointSetNegative); } } void mitk::SegmentAnythingTool::ConfirmCleanUp() { auto previewImage = this->GetPreviewSegmentation(); for (unsigned int timeStep = 0; timeStep < previewImage->GetTimeSteps(); ++timeStep) { this->ResetPreviewContentAtTimeStep(timeStep); } this->ClearSeeds(); RenderingManager::GetInstance()->RequestUpdateAll(); } template void mitk::SegmentAnythingTool::ITKWindowing(const itk::Image *inputImage, Image *mitkImage, ScalarType min, ScalarType max) { typedef itk::Image ImageType; typedef itk::IntensityWindowingImageFilter IntensityFilterType; typename IntensityFilterType::Pointer filter = IntensityFilterType::New(); filter->SetInput(inputImage); filter->SetWindowMinimum(min); filter->SetWindowMaximum(max); filter->SetOutputMinimum(min); filter->SetOutputMaximum(max); filter->Update(); mitkImage->SetVolume((void *)(filter->GetOutput()->GetPixelContainer()->GetBufferPointer())); } void mitk::SegmentAnythingTool::DoUpdatePreview(const Image *inputAtTimeStep, const Image * /*oldSegAtTimeStep*/, LabelSetImage *previewImage, TimeStepType timeStep) -{ +{ if (nullptr != previewImage && m_PointSetPositive.IsNotNull()) { - if (this->HasPicks() && nullptr != m_PythonService && this->IsImageAtTimeStepValid(inputAtTimeStep)) + if (this->HasPicks() && nullptr != m_PythonService) { mitk::LevelWindow levelWindow; this->GetToolManager()->GetReferenceData(0)->GetLevelWindow(levelWindow); std::string uniquePlaneID = GetHashForCurrentPlane(levelWindow); m_ProgressCommand->SetProgress(20); try { auto filteredImage = mitk::Image::New(); - filteredImage->Initialize(inputAtTimeStep); - AccessByItk_n(inputAtTimeStep, ITKWindowing, // apply level window filter - (filteredImage, levelWindow.GetLowerWindowBound(), levelWindow.GetUpperWindowBound())); + if (inputAtTimeStep->GetPixelType().GetNumberOfComponents() < 2) + { + filteredImage->Initialize(inputAtTimeStep); + AccessByItk_n(inputAtTimeStep, ITKWindowing, // apply level window filter + (filteredImage, levelWindow.GetLowerWindowBound(), levelWindow.GetUpperWindowBound())); + } + else + { + filteredImage = const_cast(inputAtTimeStep); + } m_ProgressCommand->SetProgress(50); this->EmitSAMStatusMessageEvent("Prompting Segment Anything Model..."); m_PythonService->TransferImageToProcess(filteredImage, uniquePlaneID); auto csvStream = this->GetPointsAsCSVString(filteredImage->GetGeometry()); m_ProgressCommand->SetProgress(100); m_PythonService->TransferPointsToProcess(csvStream); m_ProgressCommand->SetProgress(150); std::this_thread::sleep_for(100ms); mitk::LabelSetImage::Pointer outputBuffer = m_PythonService->RetrieveImageFromProcess(); m_ProgressCommand->SetProgress(180); mitk::SegTool2D::WriteSliceToVolume(previewImage, this->GetWorkingPlaneGeometry(), outputBuffer.GetPointer(), timeStep, false); this->SetSelectedLabels({MASK_VALUE}); this->EmitSAMStatusMessageEvent("Successfully generated segmentation."); } catch (const mitk::Exception &e) { this->EmitSAMStatusMessageEvent(e.GetDescription()); mitkThrow() << e.GetDescription(); } } else if (nullptr != this->GetWorkingPlaneGeometry()) { this->ResetPreviewContentAtTimeStep(timeStep); RenderingManager::GetInstance()->ForceImmediateUpdateAll(); } } } -bool mitk::SegmentAnythingTool::IsImageAtTimeStepValid(const Image *inputAtTimeStep) -{ - bool isValidDim0 = (inputAtTimeStep->GetDimension(0) > 1); - bool isValidDim1 = (inputAtTimeStep->GetDimension(1) > 1); - bool isValidDim2 = (inputAtTimeStep->GetDimension(2) > 1); - return (isValidDim0 || isValidDim1) && (isValidDim1 || isValidDim2) && (isValidDim0 || isValidDim2); -} - std::string mitk::SegmentAnythingTool::GetHashForCurrentPlane(mitk::LevelWindow &levelWindow) { mitk::Vector3D normal = this->GetWorkingPlaneGeometry()->GetNormal(); mitk::Point3D center = this->GetWorkingPlaneGeometry()->GetCenter(); std::stringstream hashstream; hashstream << std::setprecision(3) << std::fixed << normal[0]; //Consider only 3 digits after decimal hashstream << std::setprecision(3) << std::fixed << normal[1]; hashstream << std::setprecision(3) << std::fixed << normal[2]; hashstream << std::setprecision(3) << std::fixed << center[0]; hashstream << std::setprecision(3) << std::fixed << center[1]; hashstream << std::setprecision(3) << std::fixed << center[2]; hashstream << levelWindow.GetLowerWindowBound(); hashstream << levelWindow.GetUpperWindowBound(); size_t hashVal = std::hash{}(hashstream.str()); return std::to_string(hashVal); } std::stringstream mitk::SegmentAnythingTool::GetPointsAsCSVString(const mitk::BaseGeometry *baseGeometry) { MITK_INFO << "No.of points: " << m_PointSetPositive->GetSize(); std::stringstream pointsAndLabels; pointsAndLabels << "Point,Label\n"; mitk::PointSet::PointsIterator pointSetItPos = m_PointSetPositive->Begin(); mitk::PointSet::PointsIterator pointSetItNeg = m_PointSetNegative->Begin(); const char SPACE = ' '; while (pointSetItPos != m_PointSetPositive->End() || pointSetItNeg != m_PointSetNegative->End()) { if (pointSetItPos != m_PointSetPositive->End()) { mitk::Point3D point = pointSetItPos.Value(); if (baseGeometry->IsInside(point)) { Point2D p2D = Get2DIndicesfrom3DWorld(baseGeometry, point); pointsAndLabels << (int)p2D[0] << SPACE << (int)p2D[1] << ",1" << std::endl; } ++pointSetItPos; } if (pointSetItNeg != m_PointSetNegative->End()) { mitk::Point3D point = pointSetItNeg.Value(); if (baseGeometry->IsInside(point)) { Point2D p2D = Get2DIndicesfrom3DWorld(baseGeometry, point); pointsAndLabels << (int)p2D[0] << SPACE << (int)p2D[1] << ",0" << std::endl; } ++pointSetItNeg; } } return pointsAndLabels; } std::vector> mitk::SegmentAnythingTool::GetPointsAsVector( const mitk::BaseGeometry *baseGeometry) { std::vector> clickVec; clickVec.reserve(m_PointSetPositive->GetSize() + m_PointSetNegative->GetSize()); mitk::PointSet::PointsIterator pointSetItPos = m_PointSetPositive->Begin(); mitk::PointSet::PointsIterator pointSetItNeg = m_PointSetNegative->Begin(); while (pointSetItPos != m_PointSetPositive->End() || pointSetItNeg != m_PointSetNegative->End()) { if (pointSetItPos != m_PointSetPositive->End()) { mitk::Point3D point = pointSetItPos.Value(); if (baseGeometry->IsInside(point)) { Point2D p2D = Get2DIndicesfrom3DWorld(baseGeometry, point); clickVec.push_back(std::pair(p2D, "1")); } ++pointSetItPos; } if (pointSetItNeg != m_PointSetNegative->End()) { mitk::Point3D point = pointSetItNeg.Value(); if (baseGeometry->IsInside(point)) { Point2D p2D = Get2DIndicesfrom3DWorld(baseGeometry, point); clickVec.push_back(std::pair(p2D, "0")); } ++pointSetItNeg; } } return clickVec; } mitk::Point2D mitk::SegmentAnythingTool::Get2DIndicesfrom3DWorld(const mitk::BaseGeometry *baseGeometry, mitk::Point3D &point3d) { baseGeometry->WorldToIndex(point3d, point3d); MITK_INFO << point3d[0] << " " << point3d[1] << " " << point3d[2]; // remove Point2D point2D; point2D.SetElement(0, point3d[0]); point2D.SetElement(1, point3d[1]); return point2D; } void mitk::SegmentAnythingTool::EmitSAMStatusMessageEvent(const std::string& status) { SAMStatusMessageEvent.Send(status); }