From 61ba9d2e93e34be9586c579f1af169ba7ce331cb Mon Sep 17 00:00:00 2001 From: Giovanni Fattori Date: Wed, 7 Jan 2026 00:33:36 +0100 Subject: [PATCH] adding service worker structure for dicomscan skullremoval and volumepreparation --- CMakeLists.txt | 17 +- DicomScanService.cpp | 450 ++++++++++++++++++++++++++++++++++++++++ DicomScanService.h | 148 +++++++++++++ DicomScanWorker.cpp | 35 ++++ DicomScanWorker.h | 40 ++++ SkullRemovalService.cpp | 138 ++++++++++++ SkullRemovalService.h | 22 ++ SkullRemovalWorker.cpp | 44 ++++ SkullRemovalWorker.h | 26 +++ VolumePrepService.cpp | 12 ++ VolumePrepService.h | 10 + VolumePrepWorker.cpp | 31 +++ VolumePrepWorker.h | 26 +++ mainw.cpp | 11 +- mainw.h | 17 +- 15 files changed, 1014 insertions(+), 13 deletions(-) create mode 100644 DicomScanService.cpp create mode 100644 DicomScanService.h create mode 100644 DicomScanWorker.cpp create mode 100644 DicomScanWorker.h create mode 100644 SkullRemovalService.cpp create mode 100644 SkullRemovalService.h create mode 100644 SkullRemovalWorker.cpp create mode 100644 SkullRemovalWorker.h create mode 100644 VolumePrepService.cpp create mode 100644 VolumePrepService.h create mode 100644 VolumePrepWorker.cpp create mode 100644 VolumePrepWorker.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eee7ce1..0a1135d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,32 +62,39 @@ find_package(GDCM REQUIRED) # --- Sources --- set(SRCS + DicomScanService.cpp + DicomScanWorker.cpp + SkullRemovalService.cpp + SkullRemovalWorker.cpp + VolumePrepService.cpp + VolumePrepWorker.cpp main.cpp mainw.cpp ui_mainw.cpp dicomUtils.cpp gRen.cpp - gLoadPatient.cpp LocalizationService.cpp LocalizationWorker.cpp - gSkullRemoval.cpp ) set(HDR + DicomScanService.h + DicomScanWorker.h + SkullRemovalService.h + SkullRemovalWorker.h + VolumePrepService.h + VolumePrepWorker.h mainw.h ui_mainw.h connectITKVTK.h dicomUtils.h gPatientRTGeneralInfos.h gRen.h - gLoadPatient.h LocalizationService.h LocalizationWorker.h types.h types_qt.h - - gSkullRemoval.h itkQtAdaptor.h ) diff --git a/DicomScanService.cpp b/DicomScanService.cpp new file mode 100644 index 0000000..27aa5c6 --- /dev/null +++ b/DicomScanService.cpp @@ -0,0 +1,450 @@ +#include "DicomScanService.h" + + +/*_________________________________________________________- + +Patient loader for gLocalize. +no wrkDir behaviour. just load data +____________________________________________________________*/ + + +DicomScanService::gLoadPatient(){ + patientInfos = new gPatientRTGeneralInfos; + readRT=0; + m_rtIsocenter=0; + initialized=false; + virtualIso=false; + //cout<< "DicomScanService::gLoadPatient" <parse(p_loadDir); + + if(patientInfos->CTfiles .size()!=0){ + cout<< "this->load2VTK(patientInfos)" <rtIonPlanPath.isEmpty()){ + readRT = new RTPlan; + readRT->fillRTPlan(patientInfos->rtIonPlanPath.toLatin1().data() ); + m_rtIsocenter = new double[3]; + if (readRT->NumberOfBeams > 0){ + { + const auto& iso = readRT->Beams.at(0)->IsocenterPosition; + m_rtIsocenter[0] = iso[0]; + m_rtIsocenter[1] = iso[1]; + m_rtIsocenter[2] = iso[2]; + } + if (readRT->PatientOrientation.rfind("HFS", 0) == 0) + m_patientOrientation = SUPINE; + else if (readRT->PatientOrientation.rfind("HFP", 0) == 0) + m_patientOrientation = PRONE; + else m_patientOrientation = NOTDEFINED; + + if (callbacks.loadedRTIso) callbacks.loadedRTIso(m_rtIsocenter); + //for (int ii=0;iiNumberOfBeams; ii++){ + // cout<< readRT->Beams[ii]->IsocenterPosition[0]<<" "; + // cout<< readRT->Beams[ii]->IsocenterPosition[1]<<" "; + // cout<< readRT->Beams[ii]->IsocenterPosition[2]<<" "<NumberOfBeams <Beams.at(0)->IsocenterPosition[0] <Beams.at(2)->IsocenterPosition[0]<Beams.at(0)->IsocenterPosition[1] <Beams.at(2)->IsocenterPosition[1]<Beams.at(0)->IsocenterPosition[2] <Beams.at(2)->IsocenterPosition[2]<NumberOfBeams > 2 && + (readRT->Beams.at(0)->IsocenterPosition[0] != readRT->Beams.at(2)->IsocenterPosition[0] || + readRT->Beams.at(0)->IsocenterPosition[1] != readRT->Beams.at(2)->IsocenterPosition[1] || + readRT->Beams.at(0)->IsocenterPosition[2] != readRT->Beams.at(2)->IsocenterPosition[2]) + ) + virtualIso=true; + else + virtualIso=false; + //cout << "virtual Iso: "<loadDICOM(patientInfos->CTfiles); + this->connectToVTK(); +// this->changeRef(GOTSREF); + /* use DCM as default */ + this->changeRef(DCMREF); + //trueOffset_rot_prev=trueOffset_rot; + + + //this->load2VTK(patientInfos); + } else { + //DO NOTHING. THE folderisempty signal was already emitted. + } + cout<< " DicomScanService::load _ END" <SetDirection( /*imageDir*/directionToWCS ); + myImage->SetOrigin( trueOffset_rot ); + myImage->Update(); + + in->SetInput(myImage); + this->connectToVTK(); + this->actualizeOut(); + + /*calculate deltas for marker representation update*/ + if(initialized){ + if (callbacks.referenceChange) callbacks.referenceChange( + trueOffset_rot_prev[0]-trueOffset_rot[0], + trueOffset_rot_prev[1]-trueOffset_rot[1], + trueOffset_rot_prev[2]-trueOffset_rot[2]); + trueOffset_rot_prev=trueOffset_rot; + } else { + trueOffset_rot_prev=trueOffset_rot; + initialized=true; + } + +} + +void DicomScanService::loadDICOM(std::vector CTfilenames){ + + myImage = myImageType::New(); + rDICOM = itk::ImageSeriesReader::New(); + iGDCMimage = itk::GDCMImageIO::New(); + myDICOMseries = itk::GDCMSeriesFileNames::New(); + + rDICOM->SetImageIO(iGDCMimage); + rDICOM->SetFileNames(CTfilenames); + // rDICOM->SetReverseOrder(true); + try{ + rDICOM->Update(); + } + catch (itk::ExceptionObject &ex){ + std::cout << ex << std::endl; + return; // error! + } + + myImage=rDICOM->GetOutput(); + imageDir = myImage->GetDirection( ); + origin = myImage->GetOrigin( ); + sizeOfImage = myImage->GetLargestPossibleRegion().GetSize(); + +} + +void DicomScanService::connectToVTK(){ + + + out = vtkSmartPointer::New(); + + in = itk::VTKImageExport ::New(); + in->SetInput(myImage); + + out->SetUpdateInformationCallback(in->GetUpdateInformationCallback()); + out->SetPipelineModifiedCallback(in->GetPipelineModifiedCallback()); + out->SetWholeExtentCallback(in->GetWholeExtentCallback()); + out->SetSpacingCallback(in->GetSpacingCallback()); + out->SetOriginCallback(in->GetOriginCallback()); + out->SetScalarTypeCallback(in->GetScalarTypeCallback()); + out->SetNumberOfComponentsCallback(in->GetNumberOfComponentsCallback()); + out->SetPropagateUpdateExtentCallback(in->GetPropagateUpdateExtentCallback()); + out->SetUpdateDataCallback(in->GetUpdateDataCallback()); + out->SetDataExtentCallback(in->GetDataExtentCallback()); + out->SetBufferPointerCallback(in->GetBufferPointerCallback()); + out->SetCallbackUserData(in->GetCallbackUserData()); + +} + +void DicomScanService::actualizeOut(){ + + //in->Update(); + out->Update(); + //vol3D = vtkSmartPointer ::New(); + //vol3D->DeepCopy(out->GetOutput()); + //vol3D->Update(); + vol3D = out->GetOutput(); + cout<GetSpacing()[0] <<" "<GetSpacing()[1] <<" "<GetSpacing()[2] <clearInfo() " <clearInfo(); + + std::cout<<"parsing dir: "<c_str()).fileName().split(".").last() != QString("raw") ) + patientInfos->filenames.push_back( it->c_str() ); + } + patientInfos->filenames.size() == 0 ? result = PARSER_FOLDER_EMPTY : result = NOERRORS; + } else { + result=PARSER_FOLDER_NOTEXISTING; + }; + + cout << " patientInfos->filenames.size() "<filenames.size() <filenames.size()-1);i>-1;i--) + { + //cout << gCheckDICOMModalityToInt(patientInfos->filenames.at(i).c_str()) <SetFileName (patientInfos->filenames.at(i).c_str()); + + if( !fileReader->Read() ) { + cout<< "Error reading file: "<< patientInfos->filenames.at(i).data() <filenames.erase(patientInfos->filenames.begin()+i); + continue; + } + + //msStructRTIonPlan.SetFromFile( fileReader->GetFile() ); + //if( msStructRTIonPlan == gdcm::MediaStorage::RTIonPlanStorage ) + if( gCheckDICOMModalityToInt(patientInfos->filenames.at(i).c_str()) == RTPLAN ) + { + if(!patientInfos->rtIonPlanPath.isEmpty()) { +// result=PARSER_RTPLAN_NOTUNIQUE; + continue; + } else { + const gdcm::File &file = fileReader->GetFile(); + const gdcm::DataSet &ds = file.GetDataSet(); + //PATIENT NAME + strcpy( patientInfos->PatientName,gGetStringValueFromTag( gdcm::Tag(0x0010,0x0010), ds)); + // // // PATIENT ID For ex: DICOM (0010,0020) = 1933197 + strcpy( patientInfos->PatientID , gGetStringValueFromTag( gdcm::Tag(0x0010,0x0020), ds)); + // // // PATIENT AGE For ex: DICOM (0010,1010) = 031Y + strcpy( patientInfos->PatientAge , gGetStringValueFromTag( gdcm::Tag(0x0010,0x1010), ds)) ; + // // // PATIENT SEX For ex: DICOM (0010,0040) = M + strcpy( patientInfos->PatientSex, gGetStringValueFromTag( gdcm::Tag(0x0010,0x0040), ds) ); + // // // PATIENT BIRTHDATE For ex: DICOM (0010,0030) = 19680427 + strcpy( patientInfos->PatientBirthDate, gGetStringValueFromTag( gdcm::Tag(0x0010,0x0030), ds) ); + // // // SERIES NUMBER For ex: DICOM (0020,0011) = 902 + strcpy( patientInfos->SeriesNumber, gGetStringValueFromTag( gdcm::Tag(0x0020,0x0011), ds) ); + // // // SERIES DESCRIPTION For ex: DICOM (0008,103e) = SCOUT + strcpy( patientInfos->SeriesDescription , gGetStringValueFromTag( gdcm::Tag(0x0008,0x103e), ds) ); + // // // STUDY ID For ex: DICOM (0020,0010) = 37481 + strcpy( patientInfos->StudyID, gGetStringValueFromTag( gdcm::Tag(0x0020,0x0010), ds) ); + // // // STUDY DESCRIPTION For ex: DICOM (0008,1030) = BRAIN/C-SP/FACIAL + strcpy( patientInfos->StudyDescription, gGetStringValueFromTag( gdcm::Tag(0x0008,0x1030), ds) ); + + const gdcm::DataElement &gSetupSequence=ds.GetDataElement(gdcm::Tag(0x300a,0x0180)); + //LOOK FOR PATIENT ORIENTATION + gdcm::SmartPointer sqiPS = gSetupSequence.GetValueAsSQ(); + const gdcm::DataSet& gPatSetupNest = sqiPS->GetItem(1).GetNestedDataSet(); + //SET PATIENT ORIENTATION STRING + strcpy(patientInfos->PatientOrientation, gGetStringValueFromTag(gdcm::Tag(0x0018, 0x5100),gPatSetupNest)); + + + + patientInfos->rtIonPlanPath=QString(patientInfos->filenames.at(i).c_str()); + cout<< "RTPlan import success" <filenames.erase(patientInfos->filenames.begin()+i); + delete fileReader; + continue; + } + } + + // msStructRTStruct.SetFromFile( fileReader->GetFile() ); + // if( msStructRTStruct == gdcm::MediaStorage::RTStructureSetStorage ) { + if( gCheckDICOMModalityToInt(patientInfos->filenames.at(i).c_str()) == RTSTRUCT ){ + if(!patientInfos->rtStructurePath.isEmpty()) { + patientInfos->rtStructurePath.clear(); +// result=PARSER_RTSTRUCT_NOTUNIQUE; + continue; + } else { + patientInfos->rtStructurePath=QString(patientInfos->filenames.at(i).c_str()); + cout<< "RTStruct import success" <filenames.erase(patientInfos->filenames.begin()+i); + delete fileReader; + continue; + } + } + delete fileReader; + } + + + //OK + //if(result == NOERRORS && patientInfos->rtIonPlanPath.isEmpty() ) + // result= PARSER_RTPLAN_MISSING; + //if(result == NOERRORS && patientInfos->rtStructurePath.isEmpty()) + // result=PARSER_RTSTRUCT_MISSING; + + s.SetComputeZSpacing( true ); + s.SetZSpacingTolerance( 1e-2 ); + + if(result==NOERRORS) + if(!s.Sort( patientInfos->filenames ) ) { + result=PARSER_CT_FAILED_SORTING; + } else { + s.GetFilenames().size() == 0 ? result = PARSER_CT_FILES_MISSING : result=NOERRORS; + } + + if(result == NOERRORS){ + patientInfos->CTfiles=s.GetFilenames (); + //cout<< "CT files sorting succeeded" < +#include +#include +#include "gPatientRTGeneralInfos.h" +//#include "wrkDirParser.h" +#include +#include +#include +#include "dicomUtils.h" + + +/*_________________________________________________________- + +Patient loader for gLocalize. +no wrkDir behaviour. just load data +____________________________________________________________*/ + + +#include "connectITKVTK.h" +//#include "itkAnalyzeImageIO.h" +#include "itkChangeInformationImageFilter.h" +#include "itkImage.h" +#include "itkImage.h" +#include "itkImageFileWriter.h" +#include "itkOrientImageFilter.h" +#include "itkPermuteAxesImageFilter.h" +#include "itkRescaleIntensityImageFilter.h" +#include "itkImageSeriesReader.h" +#include "itkGDCMImageIO.h" +#include "itkGDCMSeriesFileNames.h" +#include "gdcmIPPSorter.h" + + +#ifndef TPATIENTORIENTATION +#define TPATIENTORIENTATION + typedef enum { + NOTDEFINED=0, + SUPINE = 1, + PRONE =2 + } t_patientOrientation; +#endif + +#ifndef PARSERVARIABLES +#define PARSERVARIABLES + enum patientErrors { + NOERRORS, + PARSER_FOLDER_NOTEXISTING, + PARSER_FOLDER_EMPTY, + PARSER_RTPLAN_NOTUNIQUE, + PARSER_RTPLAN_MISSING, + PARSER_RTSTRUCT_NOTUNIQUE, + PARSER_RTSTRUCT_MISSING, + PARSER_CT_FILES_MISSING, + PARSER_CT_FAILED_SORTING, + POPULATE_INCONSISTENT_DATASET, + POPULATE_NOTEXISTING, + CLEAR_FOLDER_NOTEXISTING + }; + + + enum patientParseOption{ + NOCLEAN, + CLEAN + }; + enum patientCleanMode{ + JUSTCLEAR, + ISLOADINGCHECK + }; +#endif + +#ifndef _T_REFERENCE_ +#define _T_REFERENCE_ +typedef enum{ + DCMREF=0, + RTREF=1, + GOTSREF=2 +}t_reference; + +#endif + + typedef itk::Image myImageType; + + +class DicomScanService { +public: + DicomScanService(); + ~DicomScanService(); + + struct Callbacks { + std::function folderIsEmpty; + std::function statusInfo; + std::function parse_result; + std::function loadFail; + std::function loadEnd; + std::function loadedRTIso; + std::function loadedVolBBox; + std::function referenceChange; + std::function virtualIsoTested; + }; + + void setCallbacks(Callbacks cb) { callbacks = std::move(cb); } + + +public: + void load(QString p_loadDir); + void changeRef(int selectedRef); + +private: + QString loadDir; + gPatientRTGeneralInfos* patientInfos; + void loadDICOM(std::vector CTfilenames); + void connectToVTK(); + void actualizeOut(); + void parse(QString p_loadDir); + t_patientOrientation m_patientOrientation; + vtkSmartPointer vol3D; + + myImageType::Pointer myImage; + itk::ImageSeriesReader::Pointer rDICOM; + itk::GDCMImageIO::Pointer iGDCMimage ; + itk::GDCMSeriesFileNames::Pointer myDICOMseries; + myImageType::DirectionType imageDir; + myImageType::PointType origin; + myImageType::SizeType sizeOfImage; + myImageType::PointType trueOffset; + myImageType::PointType trueOffset_rot; + myImageType::PointType trueOffset_rot_prev; + myImageType::DirectionType directionToWCS; + + bool initialized; + + vtkSmartPointer out; + itk::VTKImageExport ::Pointer in; + + RTPlan * readRT; + double* m_rtIsocenter; + bool virtualIso; + t_reference m_reference; + + +private: + Callbacks callbacks; +}; + +#endif diff --git a/DicomScanWorker.cpp b/DicomScanWorker.cpp new file mode 100644 index 0000000..9ce4469 --- /dev/null +++ b/DicomScanWorker.cpp @@ -0,0 +1,35 @@ +#include "DicomScanWorker.h" + +DicomScanWorker::DicomScanWorker(QObject* parent) + : QObject(parent) +{ + DicomScanService::Callbacks cb; + cb.folderIsEmpty = [&]() { emit folderIsEmpty(); }; + cb.statusInfo = [&](QString msg) { emit statusInfo(msg); }; + cb.parse_result = [&](int r, gPatientRTGeneralInfos* infos) { emit parse_result(r, infos); }; + cb.loadFail = [&](QString msg) { emit loadFail(msg); }; + cb.loadEnd = [&](vtkImageData* vol) { emit loadEnd(vol); }; + cb.loadedRTIso = [&](double* iso) { emit loadedRTIso(iso); }; + cb.loadedVolBBox = [&](double* bbox, double* spacing, int* dim) { emit loadedVolBBox(bbox, spacing, dim); }; + cb.referenceChange = [&](double dx, double dy, double dz) { emit referenceChange(dx, dy, dz); }; + cb.virtualIsoTested = [&](bool b) { emit virtualIsoTested(b); }; + svc.setCallbacks(std::move(cb)); +} + +void DicomScanWorker::load(QString p_loadDir) +{ + m_abort.store(false); + if (m_abort.load()) { emit aborted(); return; } + svc.load(p_loadDir); + if (m_abort.load()) emit aborted(); +} + +void DicomScanWorker::changeRef(int selectedRef) +{ + svc.changeRef(selectedRef); +} + +void DicomScanWorker::abort() +{ + m_abort.store(true); +} diff --git a/DicomScanWorker.h b/DicomScanWorker.h new file mode 100644 index 0000000..715ad2e --- /dev/null +++ b/DicomScanWorker.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +#include "DicomScanService.h" + +class vtkImageData; +class gPatientRTGeneralInfos; + +// Qt wrapper around DicomScanService (runs in a worker thread). +class DicomScanWorker : public QObject +{ + Q_OBJECT +public: + explicit DicomScanWorker(QObject* parent = nullptr); + +public slots: + void load(QString p_loadDir); + void changeRef(int selectedRef); + void abort(); + +signals: + void folderIsEmpty(); + void statusInfo(QString msg); + void parse_result(int result, gPatientRTGeneralInfos* patientInfos); + void loadFail(QString msg); + void loadEnd(vtkImageData* vol); + void loadedRTIso(double* iso); + void loadedVolBBox(double* bbox, double* spacing, int* dimension); + void referenceChange(double dx, double dy, double dz); + void virtualIsoTested(bool virtualIso); + void aborted(); + +private: + std::atomic_bool m_abort{false}; + DicomScanService svc; +}; diff --git a/SkullRemovalService.cpp b/SkullRemovalService.cpp new file mode 100644 index 0000000..9ea172e --- /dev/null +++ b/SkullRemovalService.cpp @@ -0,0 +1,138 @@ +#include "SkullRemovalService.h" + +#include "connectITKVTK.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +using InternalPixelType = float; +constexpr unsigned int Dimension = 3; + +using InternalImageType = itk::Image; +using MaskPixelType = unsigned char; +using MaskImageType = itk::Image; + +using ImportFilterType = itk::VTKImageImport; +using CastingINVFilterType = itk::CastImageFilter; +using CurvatureFlowImageFilterType = itk::CurvatureFlowImageFilter; +using ConnectedThresholdImageFilterType = itk::ConnectedThresholdImageFilter; +using CastingFilterType = itk::CastImageFilter; + +using StructuringElementType = itk::BinaryBallStructuringElement; +using DilateFilterType = itk::BinaryDilateImageFilter; +using ErodeFilterType = itk::BinaryErodeImageFilter; +using MaskNegatedFilterType = itk::MaskNegatedImageFilter; +} // namespace + +vtkImageData* SkullRemovalService::run(vtkImageData* inputVolume, + double thr_low, + double thr_up, + std::atomic_bool* abortFlag, + std::function progressCb) +{ + if (!inputVolume) + throw std::runtime_error("SkullRemovalService::run: inputVolume is null"); + + auto isAborted = [&]() -> bool { return abortFlag && abortFlag->load(); }; + + if (progressCb) progressCb("Skull masking: preparing pipeline", 0.0); + if (isAborted()) return nullptr; + + // VTK -> ITK + vtkSmartPointer vtkExporter = vtkSmartPointer::New(); + vtkExporter->SetInputData(inputVolume); + + auto itkImporter = ImportFilterType::New(); + ConnectPipelines(vtkExporter.GetPointer(), itkImporter); + + // Pipeline (mirrors old gSkullRemoval::runFilter in a pure form) + auto smoothing = CurvatureFlowImageFilterType::New(); + auto connectedThreshold = ConnectedThresholdImageFilterType::New(); + auto caster_mask = CastingFilterType::New(); + auto binaryDilateFilter = DilateFilterType::New(); + auto binaryErodeFilter = ErodeFilterType::New(); + auto maskNegatedFilter = MaskNegatedFilterType::New(); + + smoothing->SetNumberOfIterations(10); + smoothing->SetTimeStep(0.125); + + smoothing->SetInput(itkImporter->GetOutput()); + connectedThreshold->SetInput(smoothing->GetOutput()); + + connectedThreshold->SetLower(static_cast(thr_low)); + connectedThreshold->SetUpper(static_cast(thr_up)); + connectedThreshold->SetReplaceValue(255); + + // Seed at volume center + connectedThreshold->UpdateOutputInformation(); + InternalImageType::Pointer in = itkImporter->GetOutput(); + const auto region = in->GetBufferedRegion(); + const auto size = region.GetSize(); + const auto start = region.GetIndex(); + + InternalImageType::IndexType seed; + seed[0] = start[0] + static_cast(size[0] / 2); + seed[1] = start[1] + static_cast(size[1] / 2); + seed[2] = start[2] + static_cast(size[2] / 2); + connectedThreshold->SetSeed(seed); + + caster_mask->SetInput(connectedThreshold->GetOutput()); + + StructuringElementType kernel; + kernel.SetRadius(10); + kernel.CreateStructuringElement(); + + binaryDilateFilter->SetKernel(kernel); + binaryErodeFilter->SetKernel(kernel); + binaryDilateFilter->SetDilateValue(255); + binaryErodeFilter->SetErodeValue(255); + + binaryDilateFilter->SetInput(caster_mask->GetOutput()); + binaryErodeFilter->SetInput(binaryDilateFilter->GetOutput()); + + maskNegatedFilter->SetInput1(itkImporter->GetOutput()); + maskNegatedFilter->SetInput2(binaryErodeFilter->GetOutput()); + + if (progressCb) progressCb("Skull masking: running", 0.5); + if (isAborted()) return nullptr; + + try { + maskNegatedFilter->Update(); + } catch (const itk::ExceptionObject& ex) { + if (isAborted()) return nullptr; + throw std::runtime_error(std::string("SkullRemovalService: ITK exception: ") + ex.GetDescription()); + } + + if (isAborted()) return nullptr; + + // ITK -> VTK + vtkSmartPointer vtkImporter = vtkSmartPointer::New(); + auto itkExporter = itk::VTKImageExport::New(); + itkExporter->SetInput(maskNegatedFilter->GetOutput()); + ConnectPipelines(itkExporter, vtkImporter); + + vtkImporter->Update(); + + vtkImageData* out = vtkImageData::New(); + out->DeepCopy(vtkImporter->GetOutput()); + + if (progressCb) progressCb("Skull masking: done", 1.0); + return out; +} diff --git a/SkullRemovalService.h b/SkullRemovalService.h new file mode 100644 index 0000000..c75ffc6 --- /dev/null +++ b/SkullRemovalService.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +class vtkImageData; + +// Pure skull removal / masking pipeline (no Qt). +class SkullRemovalService +{ +public: + SkullRemovalService() = default; + + // Returns a NEW vtkImageData* (caller owns; use vtkSmartPointer if you prefer). + // Throws std::runtime_error on failure (unless aborted). + vtkImageData* run(vtkImageData* inputVolume, + double thr_low, + double thr_up, + std::atomic_bool* abortFlag = nullptr, + std::function progressCb = {}); +}; diff --git a/SkullRemovalWorker.cpp b/SkullRemovalWorker.cpp new file mode 100644 index 0000000..7bff4a3 --- /dev/null +++ b/SkullRemovalWorker.cpp @@ -0,0 +1,44 @@ +#include "SkullRemovalWorker.h" +#include "SkullRemovalService.h" + +#include + +#include + +SkullRemovalWorker::SkullRemovalWorker(QObject* parent) + : QObject(parent) +{ +} + +void SkullRemovalWorker::run(vtkImageData* inputVolume, double thr_low, double thr_up) +{ + m_abort.store(false); + emit skull_mask_upd("Skull masking...", 0.0); + + SkullRemovalService svc; + try { + vtkImageData* out = svc.run( + inputVolume, thr_low, thr_up, &m_abort, + [&](const std::string& msg, double p) { emit skull_mask_upd(QString::fromStdString(msg), p * 100.0); }); + + if (m_abort.load()) { + if (out) out->Delete(); + emit aborted(); + return; + } + + emit skull_mask_upd("Skull masking done", 100.0); + emit skull_mask_end(out); // receiver should Delete() if needed (or deep copy) + } catch (const std::exception& e) { + if (m_abort.load()) { + emit aborted(); + return; + } + emit errMsg(QString(e.what())); + } +} + +void SkullRemovalWorker::abort() +{ + m_abort.store(true); +} diff --git a/SkullRemovalWorker.h b/SkullRemovalWorker.h new file mode 100644 index 0000000..c0d89c6 --- /dev/null +++ b/SkullRemovalWorker.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class vtkImageData; + +class SkullRemovalWorker : public QObject +{ + Q_OBJECT +public: + explicit SkullRemovalWorker(QObject* parent = nullptr); + +public slots: + void run(vtkImageData* inputVolume, double thr_low, double thr_up); + void abort(); + +signals: + void skull_mask_end(vtkImageData* maskedVolume); + void errMsg(QString msg); + void skull_mask_upd(QString msg, double val); + void aborted(); + +private: + std::atomic_bool m_abort{false}; +}; diff --git a/VolumePrepService.cpp b/VolumePrepService.cpp new file mode 100644 index 0000000..06541cf --- /dev/null +++ b/VolumePrepService.cpp @@ -0,0 +1,12 @@ +#include "VolumePrepService.h" + +#include + +vtkImageData* VolumePrepService::prepare(vtkImageData* input) +{ + if (!input) return nullptr; + vtkImageData* out = vtkImageData::New(); + out->DeepCopy(input); + out->Modified(); + return out; +} diff --git a/VolumePrepService.h b/VolumePrepService.h new file mode 100644 index 0000000..4413e39 --- /dev/null +++ b/VolumePrepService.h @@ -0,0 +1,10 @@ +#pragma once + +class vtkImageData; + +// Simple volume preparation (deep copy / detaching from loader thread). +class VolumePrepService +{ +public: + vtkImageData* prepare(vtkImageData* input); +}; diff --git a/VolumePrepWorker.cpp b/VolumePrepWorker.cpp new file mode 100644 index 0000000..24da7d0 --- /dev/null +++ b/VolumePrepWorker.cpp @@ -0,0 +1,31 @@ +#include "VolumePrepWorker.h" +#include "VolumePrepService.h" + +#include + +VolumePrepWorker::VolumePrepWorker(QObject* parent) : QObject(parent) {} + +void VolumePrepWorker::run(vtkImageData* input) +{ + m_abort.store(false); + emit progress(0.0); + + if (m_abort.load()) { emit aborted(); return; } + + VolumePrepService svc; + vtkImageData* out = svc.prepare(input); + + if (m_abort.load()) { + if (out) out->Delete(); + emit aborted(); + return; + } + + emit progress(100.0); + emit finished(out); +} + +void VolumePrepWorker::abort() +{ + m_abort.store(true); +} diff --git a/VolumePrepWorker.h b/VolumePrepWorker.h new file mode 100644 index 0000000..9286c76 --- /dev/null +++ b/VolumePrepWorker.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class vtkImageData; + +class VolumePrepWorker : public QObject +{ + Q_OBJECT +public: + explicit VolumePrepWorker(QObject* parent = nullptr); + +public slots: + void run(vtkImageData* input); + void abort(); + +signals: + void finished(vtkImageData* prepared); + void error(QString msg); + void progress(double percent); + void aborted(); + +private: + std::atomic_bool m_abort{false}; +}; diff --git a/mainw.cpp b/mainw.cpp index efd6675..c41d7dc 100644 --- a/mainw.cpp +++ b/mainw.cpp @@ -84,6 +84,12 @@ MainWindow::MainWindow(QString p_loadPath){ loadPatient->moveToThread(loadPatient_thread); loadPatient_thread->start(); + // Volume preparation worker (detaches volumes from the loader thread) + volumePrep = new VolumePrepWorker; + volumePrep_thread = new QThread; + volumePrep->moveToThread(volumePrep_thread); + volumePrep_thread->start(); + qRegisterMetaType< gPatientRTGeneralInfos* >("gPatientRTGeneralInfos *"); qRegisterMetaType< std::vector >("std::vector"); qRegisterMetaType< std::string >("std::string"); @@ -94,7 +100,8 @@ MainWindow::MainWindow(QString p_loadPath){ qRegisterMetaType("MarkerList"); qRegisterMetaType("LocalizationParams"); - connect(loadPatient, SIGNAL(loadEnd(vtkImageData*)), Visualizer,SLOT(loadVolume (vtkImageData* ))); + connect(loadPatient, SIGNAL(loadEnd(vtkImageData*)), volumePrep, SLOT(run(vtkImageData* ))); + connect(volumePrep, SIGNAL(finished(vtkImageData*)), Visualizer, SLOT(loadVolume (vtkImageData* ))); connect(loadPatient, SIGNAL(folderIsEmpty()),this,SLOT(onParsedEmptyFolder())); connect(loadPatient, SIGNAL(parse_result(int , gPatientRTGeneralInfos* )),this,SLOT(onParsedOK(int , gPatientRTGeneralInfos* ))); connect(loadPatient, SIGNAL(loadedRTIso(double*)),this,SLOT(onRTIsoAvailable(double*))); @@ -562,7 +569,7 @@ void MainWindow::call_skullMask(){ Q_ARG(QString, "Skull masking..." ), Q_ARG(int, 3000)); - QMetaObject::invokeMethod(skullRemoval, "runFilter", Qt::QueuedConnection, + QMetaObject::invokeMethod(skullRemoval, "run", Qt::QueuedConnection, Q_ARG(vtkImageData*, Visualizer->getVolume()), Q_ARG(double, regGrownParameters[1]), Q_ARG(double,regGrownParameters[0])); diff --git a/mainw.h b/mainw.h index c34e4af..e73cb11 100644 --- a/mainw.h +++ b/mainw.h @@ -9,9 +9,10 @@ class Ui_MainWindow; // forward declaration (defined in ui_mainw.h) class gRen; -class gLoadPatient; +class DicomScanWorker; class LocalizationWorker; -class gSkullRemoval; +class SkullRemovalWorker; +class VolumePrepWorker; class gLoadPatDialog; //#include "wrkDirParser.h" @@ -19,14 +20,15 @@ class gLoadPatDialog; #include "gPatientRTGeneralInfos.h" #include "dicomUtils.h" #include "connectITKVTK.h" -#include "gLoadPatient.h" #include "LocalizationWorker.h" +#include "DicomScanWorker.h" +#include "SkullRemovalWorker.h" +#include "VolumePrepWorker.h" #include "types_qt.h" -#include "gSkullRemoval.h" class gLoadPatDialog: public QDialog{ @@ -65,12 +67,15 @@ private: void connectUi(Ui_MainWindow* Ui); gRen * Visualizer; - gLoadPatient* loadPatient; + DicomScanWorker* loadPatient; QThread* loadPatient_thread; + VolumePrepWorker* volumePrep{nullptr}; + QThread* volumePrep_thread{nullptr}; + LocalizationWorker* localize_marker{nullptr}; QThread* localize_thread; - gSkullRemoval* skullRemoval; + SkullRemovalWorker* skullRemoval; QThread* skull_thread; QStandardItemModel* treeModel;