#include "dicomUtils.h" #include #include #include using std::cout; using std::endl; namespace { static inline std::string Trim(std::string s) { auto notSpace = [](unsigned char c) { return !std::isspace(c); }; s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace)); s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end()); return s; } static inline int ParseIntOr(const std::string& s, int fallback) { const std::string t = Trim(s); if (t.empty()) return fallback; try { size_t idx = 0; int v = std::stoi(t, &idx); (void)idx; return v; } catch (...) { return fallback; } } static inline double ParseDoubleOr(const std::string& s, double fallback) { const std::string t = Trim(s); if (t.empty()) return fallback; try { size_t idx = 0; double v = std::stod(t, &idx); (void)idx; return v; } catch (...) { return fallback; } } static inline int GetTagIntOr(const gdcm::Tag& t, const gdcm::DataSet& ds, int fallback) { return ParseIntOr(gGetStringValueFromTagStr(t, ds), fallback); } static inline double GetTagDoubleOr(const gdcm::Tag& t, const gdcm::DataSet& ds, double fallback) { return ParseDoubleOr(gGetStringValueFromTagStr(t, ds), fallback); } // Parse a DICOM "DS" multi-value string with 3 components separated by '\\'. // Returns {BADVALUE,BADVALUE,BADVALUE} if parsing fails. std::array Parse3Doubles(const std::string& s) { std::array out{BADVALUE, BADVALUE, BADVALUE}; if (s.empty()) return out; std::stringstream ss(s); std::string token; for (int i = 0; i < 3; ++i) { if (!std::getline(ss, token, '\\')) return out; try { out[static_cast(i)] = std::stod(token); } catch (...) { return {BADVALUE, BADVALUE, BADVALUE}; } } return out; } } // namespace //FUNCTION DECLARATION NECESSARY FOR COPY/PASTE FROM vtkGDCMImageReader // const char *gGetStringValueFromTag(const gdcm::Tag& t, const gdcm::DataSet& ds); const char *gGetStringValueFromTag(const gdcm::Tag& t, const gdcm::DataSet& ds) { // NOTE: legacy API returning pointer into a static buffer. // Keep for older call sites. static std::string buffer; buffer = gGetStringValueFromTagStr(t, ds); return buffer.c_str(); } std::string gGetStringValueFromTagStr(const gdcm::Tag& t, const gdcm::DataSet& ds) { if (!ds.FindDataElement(t)) return {}; const gdcm::DataElement& de = ds.GetDataElement(t); const gdcm::ByteValue* bv = de.GetByteValue(); if (!bv) return {}; std::string s(bv->GetPointer(), bv->GetLength()); // DICOM strings are often space/\0 padded. while (!s.empty() && (s.back() == '\0' || s.back() == ' ' || s.back() == '\r' || s.back() == '\n' || s.back() == '\t')) s.pop_back(); return s; } //GENERAL INDEPENDENT FUNCTION TO CHECK FOR FILE DICOM MODALITY (PLAN, STRUCT, IMAGE) REQUIRES CORRECT PATH const char * gCheckDICOMModality(const char *filename) { gdcm::Reader gR; gR.SetFileName(filename);//str.toAscii().data()); if(!gR.Read()) return("NODCMFILE"); const gdcm::File &file = gR.GetFile(); const gdcm::DataSet &ds = file.GetDataSet(); return(gGetStringValueFromTag( gdcm::Tag(0x0008,0x0060), ds) ); //RETURN MODALITY }; int gCheckDICOMModalityToInt(const char *filename){ const char * tmp = gCheckDICOMModality(filename); //cout<< tmp< sqiPS = gSetupSequence.GetValueAsSQ(); if (sqiPS && sqiPS->GetNumberOfItems() >= 1) { const gdcm::DataSet& gPatSetupNest = sqiPS->GetItem(1).GetNestedDataSet(); PatientOrientation = gGetStringValueFromTagStr(gdcm::Tag(0x0018, 0x5100), gPatSetupNest); } } const gdcm::DataElement &gBeamSequence=ds.GetDataElement(gdcm::Tag(0x300a,(IsCarbon==false ? 0x03a2 : 0x00b0))); // (THESE ARE THE BEAMS) gdcm::SmartPointer sqi = gBeamSequence.GetValueAsSQ(); if(!sqi || !sqi->GetNumberOfItems()) { LastError = "Beam sequence not found in RT Plan file"; cout<<"usteria" <(sqi->GetNumberOfItems()); NumberOfBeams = expectedBeams; // Own and manage beams with RAII. Beams.clear(); Beams.reserve(static_cast(expectedBeams)); //ITERATES ON BEAM NUMBER for(int i = 0; i < expectedBeams; i++) { const gdcm::Item & item = sqi->GetItem(i+1); //ITEM START FROM 1 const gdcm::DataSet& gBeamNestedds = item.GetNestedDataSet(); //THIS IS BEAM DATASET auto beam = std::make_unique(); IonBeamProperties* b = beam.get(); //READ BEAM DATA b->BeamNumber = GetTagIntOr(gdcm::Tag(0x300a, 0x00c0), gBeamNestedds, 0); b->BeamName = gGetStringValueFromTagStr(gdcm::Tag(0x300a, 0x00c2), gBeamNestedds); if(IsCarbon == false) b->SupportType = gGetStringValueFromTagStr(gdcm::Tag(0x300a, 0x0350), gBeamNestedds); else b->SupportType = "TABLE"; b->SupportId = gGetStringValueFromTagStr(gdcm::Tag(0x300a, 0x0352), gBeamNestedds); //gBeams[i]->gSetSupportType("CHAIR"); //READ GEOMETRY //READ SEQUENCE OF CONTROL POINTS //v04Build27 DO DIFFERENTLY IF PROTONS OR CARBONS const gdcm::Tag cpTag(0x300a, IsCarbon==false ? 0x03a8 : 0x0111); if (!gBeamNestedds.FindDataElement(cpTag)) { // Skip this beam if control points are missing. continue; } const gdcm::DataElement &gCPSequence=gBeamNestedds.GetDataElement(cpTag); gdcm::SmartPointer sqicp = gCPSequence.GetValueAsSQ(); if (!sqicp || sqicp->GetNumberOfItems() < 1) { continue; } const gdcm::DataSet& gCPNestedds = sqicp->GetItem(1).GetNestedDataSet(); //GANTRY ANGLE (NEEDED FOR CHAIR ?) b->GantryAngle = GetTagIntOr(gdcm::Tag(0x300a, 0x011e), gCPNestedds, 0); // ISOCENTER POSITION (this is beam position, not patient/volume center). // DICOM tag (300A,012C) is a multi-value DS string: "x\\y\\z". { const std::string isoStr = gGetStringValueFromTagStr(gdcm::Tag(0x300a, 0x012c), gCPNestedds); const auto iso = Parse3Doubles(isoStr); b->IsocenterPosition[0] = iso[0]; b->IsocenterPosition[1] = iso[1]; b->IsocenterPosition[2] = iso[2]; } // Beams[i]->IsocenterPosition[](iso[0],iso[1],iso[2]); //SUPPORT ANGLE (YAW ANGLE OF SUPPORT WITH RESPECT TO FIXED REFERENCE SYSTEM) AROUND Z AXIS (VERTICAL) //POSITIVE COUNTER-CLOCKWISE WHEN VIEWED FROM TOP OF Z-Axis b->TableYawAngle = static_cast(GetTagDoubleOr(gdcm::Tag(0x300a, 0x0122), gCPNestedds, 0.0)); /*if(i==1) gBeams[i]->gSetTableYawAngle(0); */ //TABLE TOP PITCH (ROTATION AROUND X AXIS, i.e. LATERAL AXIS OF TABLE AND BEAM DIRECTION IN FIXED REF SYS) ////v04Build27 IF PROTONS READ IT FROM PLAN; IF CARBONS SET DEFAULT VALUE (0.0) AND FORGET CHAIR if(IsCarbon==false) { gdcm::Attribute <0x300a, 0x0140> pa; pa.SetFromDataElement(gCPNestedds.GetDataElement( pa.GetTag())); b->TablePitchAngle = pa.GetValue(); } else b->TablePitchAngle = 0.0; //v04 Build23 SET SUPPORT TYPE CORRECTLY IF PITCH IS 90 (IT IS CHAIR); DO IT FOR ALL BEAMS // if(i==0) // { if(Beams[i]->TablePitchAngle()>45) // strcpy (Beams[i]->SupportType,("CHAIR")); // } // else if (i>0) // { if(Beams[0]->SupportChair()==true) // Beams[i]->gSetSupportType("CHAIR"); // } //TABLE TOP ROLL (ROTATION AROUND Y AXIS, i.e. LONGITUDINAL AXIS OF TABLE) //v04Build27 IF PROTONS READ IT FROM PLAN; IF CARBONS SET DEFAULT VALUE (0.0) if(IsCarbon==false) { gdcm::Attribute <0x300a, 0x0144> ra; ra.SetFromDataElement(gCPNestedds.GetDataElement(ra.GetTag())); b->TableRollAngle = ra.GetValue(); } else b->TableRollAngle = 0.0; //WHAT ABOUT TABLE TOP VERTICAL POSITION, LOGITUDINAL AND LATERAL POSITION ? USEFUL SOMEHOW ? // THEY ARE READ AND USED TO REALIZE VOLUME CENTER ALIGNMENT IN ROOM ISOCENTER //FIND PATIENT SETUP SEQUENCE (IT IS UNIQUE FOR ALL BEAMS BUT I DO IT BEAM BY BEAM) //v04Build27 DO NOT LOOK FOR PATIENT SETUPSEQUENCE /* if(!ds.FindDataElement(gdcm::Tag(0x300a,0x0180))) throw(gException("CRITICAL: PATIENT SETUP SEQUENCE NOT FOUND IN RT ION PLAN")); const gdcm::DataElement &gPatientSetupSequence=ds.GetDataElement(gdcm::Tag(0x300a,0x0180)); gdcm::SmartPointer sqpt = gPatientSetupSequence.GetValueAsSQ(); int psuseq=sqpt->GetNumberOfItems(); //CHECK NUMBER OF SETUPSEQUENCE VS BEAM NUMBER //READ DATA FROM ITEM 1 OR BEAM IS MORE PATIENT SETUP SEQUENCE ARE FOUND (ATTENTION !!!!) //v03 Build30: READ TABLE POSITIONS FROM SETUPSEQUENCE const gdcm::DataSet& gPSNestedds = sqpt->GetItem((i+1)<=psuseq ? (i+1) : 1).GetNestedDataSet(); //IF PSTIENT SETUP SEQUENCE < THAN BEAMS USE FIRST */ //v04Build27 READ BEAMS ABSOLUTE GEOMETRY NOT FROM SETUP SEQUENCE BUT FROM CONTROL POINT SEQUENCE (NEW VERSION OIS FOR CARBONS) /*float tablelat=atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x01d6),gPSNestedds)); float tablelong=atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x01d4),gPSNestedds)); float tablevert=atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x01d2),gPSNestedds));*/ float deltatablelat = static_cast(GetTagDoubleOr(gdcm::Tag(0x300a, 0x012a), gCPNestedds, 0.0)); float deltatablelong = static_cast(GetTagDoubleOr(gdcm::Tag(0x300a, 0x0129), gCPNestedds, 0.0)); float deltatablevert = static_cast(GetTagDoubleOr(gdcm::Tag(0x300a, 0x0128), gCPNestedds, 0.0)); //v03 (Build 30) NOW ADD TO TREATMENT BEMA EVENTUAL RELATIVE SHIFTS BETWEEN SETUP SEQUENCE AND STORE IN BEAM STRUCTURE ONLY TO TREATMETN BEAMS // if(i==0) //THIS IS SETUP BEAM DO NOTHING //v04Build26 TREAT ALL BEAMS THE SAME WAY (SET-UP+BEAMS)(NEW VERSION OF OIS) //v04Build27 USE PPS GEOMETRY READ FROM POINT SEQUENCE { b->TableLatDispl = deltatablelat; b->TableLongDispl = deltatablelong; b->TableVertDispl = deltatablevert; } /* else { gBeams[i]->gSetTableLatDispl(tablelat); gBeams[i]->gSetTableLongDispl(tablelong); gBeams[i]->gSetTableVertDispl(tablevert); }*/ //v04Build26 DO NOT ADD DELTASHIFTS (NEW VERSION OF OIS) //v04Build1: CHECK FOR VIRTUAL ISO //ADD DELTASHIFTS TO TABLE DISPLACEMENTS //v04Build26 DO NOT ADD DELTASHIFTS (NEW VERSION OF OIS) /*gBeams[i]->gSetTableLatDispl(tablelat+atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x012a),gCPNestedds))); gBeams[i]->gSetTableLongDispl(tablelong+atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x0129),gCPNestedds))); gBeams[i]->gSetTableVertDispl(tablevert+atof(gGetStringValueFromTag(gdcm::Tag(0x300a, 0x0128),gCPNestedds))); //UPDATE ISOCENTER POSITION ON TREATMENT BEAM USING CT REFERENCE (LONG IS Z COORDINATE; Y IS VERTICAL; Z I) //v04Build26 DO NOT ADD DELTASHIFTS (NEW VERSION OF OIS) gBeams[i]->gSetIsocenterPosition(iso[0]-deltatablelat,iso[1]-deltatablevert,iso[2]-deltatablelong); //SIGNS AND COORDINATE TO BE CHECKED (OK for LONG) } */ //v04Build28 CORRECT ISO POSITION IN CASE OF VIRTUAL ISO /* if(i<0) //DO THIS ONLY FOR TREATMENT BEAMS gBeams[i]->gSetIsocenterPosition(iso[0]-(gBeams[i]->gGetTableLatDispl()-gBeams[0]->gGetTableLatDispl()),iso[1]-(gBeams[i]->gGetTableVertDispl()-gBeams[0]->gGetTableVertDispl()),iso[2]-(gBeams[i]->gGetTableLongDispl()-gBeams[0]->gGetTableLongDispl())); //SIGNS AND COORDINATE TO BE CHECKED (OK for LONG) */ //UPDATE AT FIRST CNAO PATIENT SET TABLE POSITION FROM PLAN AT THE ISOCENTER POSITION FOR EVERY BEAM /* gBeams[i]->gSetTableLatDispl(gBeams[i]->gGetIsocenterPosition()[0]); gBeams[i]->gSetTableLongDispl(gBeams[i]->gGetIsocenterPosition()[1]);gui gBeams[i]->gSetTableVertDispl(gBeams[i]->gGetIsocenterPosition()[2]); */ // FILL gPPS CLASS WITH CURRENT PPS CONFIGURATION UPDATING CONVENTIONS FROM PLAN TO PPS (-90 FOR CONVENTION CT-PPS) // Beams[i]->gGetCurrentPPS()->gSetPPSXPlan(gBeams[i]->gGetTableLatDispl()); // Beams[i]->gGetCurrentPPS()->gSetPPSYPlan(gBeams[i]->gGetTableLongDispl()); // Beams[i]->gGetCurrentPPS()->gSetPPSYOrig(gBeams[i]->gGetTableLongDispl()); //SAVE ALSO AN ORIGINAL COPY FROM PLAN OF LONGITUFINAL POSITION (FOR INDEXING DIALOG) // Beams[i]->gGetCurrentPPS()->gSetPPSZPlan(gBeams[i]->gGetTableVertDispl()); // //v03 Build35: ADD TO TREATMENT BEAMS YAW THE YAW VALUE OF SETUP BEAM TO RECOVER ABSOLUTE VALUES FROM RELATIVE ONES AS SENT BY OIS if(i>0) // THIS IS TREATMENT BEAM { //v04Build03 SET PROPER DELTA ANGLES FOR BEAM ROTATION (IF DELTA IS NEGATIVE SET IT NEGATIVE) double yaw = b->TableYawAngle; if(yaw>180) yaw=yaw-360; //V04Build26 NEW VERSION OF MOSAIQ OIS DO NOT USE RELATIVE ANGLES //yaw=yaw+gBeams[0]->gGetTableYawAngle(); b->TableYawAngle = yaw; double pitch = b->TablePitchAngle; if(pitch>180) pitch=pitch-360; //V04Build26 NEW VERSION OF MOSAIQ OIS DO NOT USE RELATIVE ANGLES //pitch=pitch+gBeams[0]->gGetTablePitchAngle(); //v04Build23 SET ALL PITCH AT 90 (NOT STABLE) IT IS FOR THE CHAIR if(pitch>150) pitch=90; b->TablePitchAngle = pitch; double roll = b->TableRollAngle; if(roll>180) roll=roll-360; //V04Build26 NEW VERSION OF MOSAIQ OIS DO NOT USE RELATIVE ANGLES //roll=roll+gBeams[0]->gGetTableRollAngle(); b->TableRollAngle = roll; } //HERE 90 DEGREES IS SUMMED IN ORDER TO SOLVE TEH AMBIGUITY BETWEEN TPS REFERENCE AND ROOM/ODEVIS REFERENCE //0 DEGREE YAW IN TPS BECOMES +90 YAW IN ROOM/PPS MEANING BEAM IS FROM LATERAL //THIS WE DO IT JUST FOR THE USER INTERFACE DISPLAYING DEGREES OF YAW ROTATION EXPECTED OR TO BE INSERTED IN THE PPS //v04Build24 ADJUST PPS YAW ANGLE FOR PPS AND CHAIR // double yaw=gBeams[0]->gIsSupportChair()==false ? gBeams[i]->gGetTableYawAngle()+90.0 : gBeams[i]->gGetTableYawAngle()-90.0; // //NOW SET +- 180 YAW ROTATION CONVENTION // if(yaw>180) // yaw=yaw-360; // gBeams[i]->gGetCurrentPPS()->gSetPPSRotateDegreePlan(yaw); //CONVERSION FROM CT BASE PLAN TO PPS (ANGLE RANGES -180 +180) // // //v04Build23 ATTENZIONE ! // if(gBeams[0]->gIsSupportChair()==false) gBeams[i]->gGetCurrentPPS()->gSetPPSPitchDegreePlan(gBeams[i]->gGetTablePitchAngle()); // else gBeams[i]->gGetCurrentPPS()->gSetPPSPitchDegreePlan(gBeams[i]->gGetTablePitchAngle()-90); //NEED TO SUBTRACT 90 DEGRESS FOR PPS CONFIGURATION : THERE IS THE CHAIR // // gBeams[i]->gGetCurrentPPS()->gSetPPSRollDegreePlan(gBeams[i]->gGetTableRollAngle()); // // // // //SET CURRENT PPS CONFIGURATION WITH PLAN CONFIGURATION (THERE I TAKE CARE OF THE CHAIR) // gBeams[i]->gGetCurrentPPS()->gSetPlanConfig(); /* //UPDATE TABLE DISPLACEMENT ACCORDING TO SIEMENS TPS USING THE ISOCENTER CO-ORDINATESIF DEFINE W.R.T. INDEXING POINT //MIGHT NEED TO SUBTRACT COORDINATES OF STRUCTURE VOLUMECENTER IF RELATIVE REFERENCE SYSTEM IS NOT DEFINED IN TPS gBeams[i]->gSetTableLatDispl(gBeams[i]->gGetIsocenterPosition()[0]); gBeams[i]->gSetTableLongDispl(gBeams[i]->gGetIsocenterPosition()[1]); gBeams[i]->gSetTableVertDispl(gBeams[i]->gGetIsocenterPosition()[2]);*/ // gBeams[i]->gSetIsocenterPosition(iso[0]- gBeams[i]->gGetCurrentPPS()->getPPSXPlan(),iso[1]- gBeams[i]->gGetCurrentPPS()->getPPSYPlan(),iso[2]- gBeams[i]->gGetCurrentPPS()->getPPSZPlan()); // Store the parsed beam. Beams.push_back(std::move(beam)); } // Keep legacy field in sync with actual parsed beams. NumberOfBeams = static_cast(Beams.size()); return true; };